iris: make font families application-named strings
This commit is contained in:
1 parent
bdc9c914c4
commit
4cbe7baea0
9 files changed
+56
-90
No files matched your search
@@ -210,7 +210,7 @@ pub fn render_markdown(src: &str, base_size: f32, theme: &Theme) -> Rendered {
|
|||||||
TagEnd::CodeBlock => {
|
TagEnd::CodeBlock => {
|
||||||
spans.push(
|
spans.push(
|
||||||
SpanStyle::new(range.clone())
|
SpanStyle::new(range.clone())
|
||||||
.family(Family::Monospace)
|
.family(MONOSPACE)
|
||||||
.color(theme.code.clone()),
|
.color(theme.code.clone()),
|
||||||
);
|
);
|
||||||
if let Some(language) = fence_language.take() {
|
if let Some(language) = fence_language.take() {
|
||||||
@@ -226,7 +226,7 @@ pub fn render_markdown(src: &str, base_size: f32, theme: &Theme) -> Rendered {
|
|||||||
out.push_str(&text);
|
out.push_str(&text);
|
||||||
spans.push(
|
spans.push(
|
||||||
SpanStyle::new(start..out.len())
|
SpanStyle::new(start..out.len())
|
||||||
.family(Family::Monospace)
|
.family(MONOSPACE)
|
||||||
.color(theme.code.clone()),
|
.color(theme.code.clone()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -291,7 +291,7 @@ pub(crate) fn highlight_into(
|
|||||||
};
|
};
|
||||||
spans.push(
|
spans.push(
|
||||||
SpanStyle::new(range.start + start..range.start + end)
|
SpanStyle::new(range.start + start..range.start + end)
|
||||||
.family(Family::Monospace)
|
.family(MONOSPACE)
|
||||||
.color(syntax_color(span.kind, theme)),
|
.color(syntax_color(span.kind, theme)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -525,7 +525,7 @@ mod tests {
|
|||||||
let r = block("```brainfuck\nlet x = 1;\n```");
|
let r = block("```brainfuck\nlet x = 1;\n```");
|
||||||
assert_eq!(r.text, "let x = 1;");
|
assert_eq!(r.text, "let x = 1;");
|
||||||
assert_eq!(r.spans.len(), 1);
|
assert_eq!(r.spans.len(), 1);
|
||||||
assert!(r.spans[0].family == Some(Family::Monospace));
|
assert!(r.spans[0].family.as_deref() == Some(MONOSPACE));
|
||||||
assert_eq!(r.spans[0].color, Some(code_color()));
|
assert_eq!(r.spans[0].color, Some(code_color()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -15,12 +15,13 @@ use std::{mem, rc::Rc};
|
|||||||
use theme::Theme;
|
use theme::Theme;
|
||||||
|
|
||||||
const ICON_FONT: &[u8] = include_bytes!("../../assets/fonts/nerd_icons.ttf");
|
const ICON_FONT: &[u8] = include_bytes!("../../assets/fonts/nerd_icons.ttf");
|
||||||
|
const ICON_FAMILY: &str = "ai-app-icons";
|
||||||
|
|
||||||
pub(crate) fn register_fonts(ui: &mut Ui) {
|
pub(crate) fn register_fonts(ui: &mut Ui) {
|
||||||
if ui.is_font_registered(&Family::Icons) {
|
if ui.is_font_registered(ICON_FAMILY) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ui.register_font(Family::Icons, ICON_FONT)
|
ui.register_font(ICON_FAMILY, ICON_FONT)
|
||||||
.expect("the ai-app icon font must register before text is drawn");
|
.expect("the ai-app icon font must register before text is drawn");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-5
@@ -198,11 +198,7 @@ where
|
|||||||
.spans(rendered.spans)
|
.spans(rendered.spans)
|
||||||
.text_align(Align::LEFT)
|
.text_align(Align::LEFT)
|
||||||
.wrap(!verbatim)
|
.wrap(!verbatim)
|
||||||
.family(if verbatim {
|
.family(if verbatim { MONOSPACE } else { SANS_SERIF })
|
||||||
Family::Monospace
|
|
||||||
} else {
|
|
||||||
Family::SansSerif
|
|
||||||
})
|
|
||||||
.size(BASE_SIZE)
|
.size(BASE_SIZE)
|
||||||
.color(match frame {
|
.color(match frame {
|
||||||
BlockFrame::Quote => theme.quote_text.clone(),
|
BlockFrame::Quote => theme.quote_text.clone(),
|
||||||
|
|||||||
+2
-6
@@ -80,18 +80,14 @@ fn text<Rsc>(content: impl Into<String>, size: f32, color: PaintId) -> TextBuild
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn disclosure<Rsc>(glyph: &'static str, theme: &Theme) -> TextBuilder<Rsc> {
|
fn disclosure<Rsc>(glyph: &'static str, theme: &Theme) -> TextBuilder<Rsc> {
|
||||||
text(glyph, MARK_DP, theme.muted.clone()).family(Family::Icons)
|
text(glyph, MARK_DP, theme.muted.clone()).family(super::ICON_FAMILY)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn raw_block<Rsc: HasEvents>(rsc: &mut Rsc, body: TextBuilder<Rsc>, theme: &Theme) -> StrongWidget
|
fn raw_block<Rsc: HasEvents>(rsc: &mut Rsc, body: TextBuilder<Rsc>, theme: &Theme) -> StrongWidget
|
||||||
where
|
where
|
||||||
Rsc::State: FocusHost,
|
Rsc::State: FocusHost,
|
||||||
{
|
{
|
||||||
let field = body
|
let field = body.family(MONOSPACE).size(BODY_SIZE).wrap(false).add(rsc);
|
||||||
.family(Family::Monospace)
|
|
||||||
.size(BODY_SIZE)
|
|
||||||
.wrap(false)
|
|
||||||
.add(rsc);
|
|
||||||
field
|
field
|
||||||
.scrollable(Axis::X, Pin::Start)
|
.scrollable(Axis::X, Pin::Start)
|
||||||
.pad(dp(RAW_PAD_DP))
|
.pad(dp(RAW_PAD_DP))
|
||||||
|
|||||||
+8
-6
@@ -813,12 +813,14 @@ widget changed. Winit's user-event proxy and Android's posted callback are
|
|||||||
private implementations of that wake; applications do not define platform
|
private implementations of that wake; applications do not define platform
|
||||||
event types or manually request redraws.
|
event types or manually request redraws.
|
||||||
|
|
||||||
**Iris ships no fonts.** Applications register their own font bytes on `Ui`
|
**Iris ships no fonts, and font families are application-named strings**
|
||||||
before the first text shape and select them through the same `Family` used by
|
(2026-09-12). Applications register their own font bytes on `Ui` before the
|
||||||
text widgets. `Family::Icons` is a semantic application-configured role, not a
|
first text shape and select them by the same string used by text widgets. The
|
||||||
particular icon set owned by the framework. ai-app owns its Nerd Fonts subset,
|
public boundaries accept `AsRef<str>`, so an application can use bare strings
|
||||||
its codepoints, its license and the script that rebuilds it; body and monospace
|
or put its own semantic enum in front of them without Iris hardcoding the
|
||||||
families continue to come from the platform.
|
roles. ai-app owns the `ai-app-icons` family, its Nerd Fonts subset, its
|
||||||
|
codepoints, its license and the script that rebuilds it; the CSS generic names
|
||||||
|
`sans-serif` and `monospace` continue to resolve through the platform.
|
||||||
|
|
||||||
`cargo-iris` is an installable Cargo subcommand, rather than a script callers
|
`cargo-iris` is an installable Cargo subcommand, rather than a script callers
|
||||||
must find inside an Iris checkout. `cargo iris apk` builds the Rust `cdylib`
|
must find inside an Iris checkout. `cargo iris apk` builds the Rust `cdylib`
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::{Align, GlyphAtlas, GlyphKey, PaintId, PlacedGlyph, RegionAlign, Textures, util::Vec2};
|
use crate::{Align, GlyphAtlas, GlyphKey, PaintId, PlacedGlyph, RegionAlign, Textures, util::Vec2};
|
||||||
use parley::{
|
use parley::{
|
||||||
Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, FontStyle, FontWeight,
|
Alignment, AlignmentOptions, FontContext, FontFamily, FontStyle, FontWeight, GenericFamily,
|
||||||
GenericFamily, Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty,
|
Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty,
|
||||||
fontique::{Blob, FontInfoOverride},
|
fontique::{Blob, FontInfoOverride},
|
||||||
};
|
};
|
||||||
use std::{collections::HashMap, fmt, ops::Range, sync::Arc};
|
use std::{collections::HashMap, fmt, ops::Range, sync::Arc};
|
||||||
@@ -24,19 +24,14 @@ pub struct FontDiagnostics {
|
|||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub enum FontRegistrationError {
|
pub enum FontRegistrationError {
|
||||||
UnsupportedFamily(Family),
|
AlreadyRegistered(String),
|
||||||
AlreadyRegistered(Family),
|
|
||||||
TextAlreadyShaped,
|
TextAlreadyShaped,
|
||||||
InvalidFont(Family),
|
InvalidFont(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for FontRegistrationError {
|
impl fmt::Display for FontRegistrationError {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::UnsupportedFamily(family) => write!(
|
|
||||||
f,
|
|
||||||
"cannot register font data for {family:?}; use Family::Icons or Family::Named"
|
|
||||||
),
|
|
||||||
Self::AlreadyRegistered(family) => {
|
Self::AlreadyRegistered(family) => {
|
||||||
write!(f, "font data is already registered for {family:?}")
|
write!(f, "font data is already registered for {family:?}")
|
||||||
}
|
}
|
||||||
@@ -72,7 +67,7 @@ pub struct TextData {
|
|||||||
/// truth would mean carrying a `Painter` (or output size) into every
|
/// truth would mean carrying a `Painter` (or output size) into every
|
||||||
/// input handler for the sake of one field.
|
/// input handler for the sake of one field.
|
||||||
pub density: f32,
|
pub density: f32,
|
||||||
registered_families: HashMap<Family, String>,
|
registered_families: HashMap<String, String>,
|
||||||
next_registered_family: u64,
|
next_registered_family: u64,
|
||||||
shaping_started: bool,
|
shaping_started: bool,
|
||||||
}
|
}
|
||||||
@@ -169,12 +164,10 @@ fn patch_android_monospace(_font_cx: &mut FontContext) {}
|
|||||||
impl TextData {
|
impl TextData {
|
||||||
pub(crate) fn register_font(
|
pub(crate) fn register_font(
|
||||||
&mut self,
|
&mut self,
|
||||||
family: Family,
|
family: impl AsRef<str>,
|
||||||
data: impl AsRef<[u8]> + Send + Sync + 'static,
|
data: impl AsRef<[u8]> + Send + Sync + 'static,
|
||||||
) -> Result<(), FontRegistrationError> {
|
) -> Result<(), FontRegistrationError> {
|
||||||
if !matches!(family, Family::Icons | Family::Named(_)) {
|
let family = family.as_ref().to_owned();
|
||||||
return Err(FontRegistrationError::UnsupportedFamily(family));
|
|
||||||
}
|
|
||||||
if self.shaping_started {
|
if self.shaping_started {
|
||||||
return Err(FontRegistrationError::TextAlreadyShaped);
|
return Err(FontRegistrationError::TextAlreadyShaped);
|
||||||
}
|
}
|
||||||
@@ -201,23 +194,18 @@ impl TextData {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn is_font_registered(&self, family: &Family) -> bool {
|
pub(crate) fn is_font_registered(&self, family: impl AsRef<str>) -> bool {
|
||||||
self.registered_families.contains_key(family)
|
self.registered_families.contains_key(family.as_ref())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cloned rather than borrowed because the caller needs it while the
|
/// Cloned rather than borrowed because the caller needs it while the
|
||||||
/// layout builder holds `&mut self` -- a `String` per shaped registered
|
/// layout builder holds `&mut self` -- a `String` per shaped registered
|
||||||
/// run, paid only when the layout is rebuilt.
|
/// run, paid only when the layout is rebuilt.
|
||||||
pub fn resolve_family(&self, family: &Family) -> Family {
|
pub fn resolve_family(&self, family: &str) -> String {
|
||||||
if let Some(name) = self.registered_families.get(family) {
|
if let Some(name) = self.registered_families.get(family) {
|
||||||
return Family::Named(name.clone());
|
return name.clone();
|
||||||
}
|
|
||||||
match family {
|
|
||||||
Family::Icons => panic!(
|
|
||||||
"Family::Icons has no font; register application font data with Ui::register_font before the first draw"
|
|
||||||
),
|
|
||||||
_ => family.clone(),
|
|
||||||
}
|
}
|
||||||
|
family.to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn font_diagnostics(&mut self) -> FontDiagnostics {
|
pub fn font_diagnostics(&mut self) -> FontDiagnostics {
|
||||||
@@ -389,37 +377,15 @@ impl TextData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Which family to ask for. Kept as an owned name rather than parley's
|
pub const SANS_SERIF: &str = "sans-serif";
|
||||||
/// borrowed `FontFamily<'_>` so that a widget can hold one without a lifetime.
|
pub const SERIF: &str = "serif";
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
pub const MONOSPACE: &str = "monospace";
|
||||||
pub enum Family {
|
|
||||||
SansSerif,
|
|
||||||
Serif,
|
|
||||||
Monospace,
|
|
||||||
/// The icon font supplied by the application through
|
|
||||||
/// [`crate::Ui::register_font`].
|
|
||||||
Icons,
|
|
||||||
Named(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Family {
|
|
||||||
fn family(&self) -> FontFamily<'_> {
|
|
||||||
let name = match self {
|
|
||||||
Self::SansSerif => FontFamilyName::Generic(GenericFamily::SansSerif),
|
|
||||||
Self::Serif => FontFamilyName::Generic(GenericFamily::Serif),
|
|
||||||
Self::Monospace => FontFamilyName::Generic(GenericFamily::Monospace),
|
|
||||||
Self::Icons => FontFamilyName::Generic(GenericFamily::SansSerif),
|
|
||||||
Self::Named(name) => FontFamilyName::Named(name.as_str().into()),
|
|
||||||
};
|
|
||||||
FontFamily::Single(name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
pub struct SpanStyle {
|
pub struct SpanStyle {
|
||||||
pub range: Range<usize>,
|
pub range: Range<usize>,
|
||||||
pub color: Option<PaintId>,
|
pub color: Option<PaintId>,
|
||||||
pub family: Option<Family>,
|
pub family: Option<String>,
|
||||||
pub font_size: Option<f32>,
|
pub font_size: Option<f32>,
|
||||||
pub bold: bool,
|
pub bold: bool,
|
||||||
pub italic: bool,
|
pub italic: bool,
|
||||||
@@ -442,8 +408,8 @@ impl SpanStyle {
|
|||||||
self.color = Some(color);
|
self.color = Some(color);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
pub fn family(mut self, family: Family) -> Self {
|
pub fn family(mut self, family: impl AsRef<str>) -> Self {
|
||||||
self.family = Some(family);
|
self.family = Some(family.as_ref().to_owned());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
pub fn font_size(mut self, size: f32) -> Self {
|
pub fn font_size(mut self, size: f32) -> Self {
|
||||||
@@ -469,7 +435,7 @@ pub struct TextAttrs {
|
|||||||
pub color: PaintId,
|
pub color: PaintId,
|
||||||
pub font_size: f32,
|
pub font_size: f32,
|
||||||
pub line_height: f32,
|
pub line_height: f32,
|
||||||
pub family: Family,
|
pub family: String,
|
||||||
pub wrap: bool,
|
pub wrap: bool,
|
||||||
pub align: RegionAlign,
|
pub align: RegionAlign,
|
||||||
}
|
}
|
||||||
@@ -483,7 +449,7 @@ impl Default for TextAttrs {
|
|||||||
color: PaintId::WHITE,
|
color: PaintId::WHITE,
|
||||||
font_size: size,
|
font_size: size,
|
||||||
line_height: size * LINE_HEIGHT_MULT,
|
line_height: size * LINE_HEIGHT_MULT,
|
||||||
family: Family::SansSerif,
|
family: SANS_SERIF.to_owned(),
|
||||||
wrap: false,
|
wrap: false,
|
||||||
align: Align::CENTER_LEFT,
|
align: Align::CENTER_LEFT,
|
||||||
}
|
}
|
||||||
@@ -562,7 +528,7 @@ impl TextBuffer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let base_family = data.resolve_family(&attrs.family);
|
let base_family = data.resolve_family(&attrs.family);
|
||||||
let span_families: Vec<Option<Family>> = self
|
let span_families: Vec<Option<String>> = self
|
||||||
.spans
|
.spans
|
||||||
.iter()
|
.iter()
|
||||||
.map(|span| span.family.as_ref().map(|f| data.resolve_family(f)))
|
.map(|span| span.family.as_ref().map(|f| data.resolve_family(f)))
|
||||||
@@ -570,7 +536,9 @@ impl TextBuffer {
|
|||||||
let mut builder = data
|
let mut builder = data
|
||||||
.layout_cx
|
.layout_cx
|
||||||
.ranged_builder(&mut data.font_cx, &self.text, 1.0, true);
|
.ranged_builder(&mut data.font_cx, &self.text, 1.0, true);
|
||||||
builder.push_default(StyleProperty::FontFamily(base_family.family()));
|
builder.push_default(StyleProperty::FontFamily(FontFamily::from(
|
||||||
|
base_family.as_str(),
|
||||||
|
)));
|
||||||
builder.push_default(StyleProperty::FontSize(attrs.font_size * density));
|
builder.push_default(StyleProperty::FontSize(attrs.font_size * density));
|
||||||
builder.push_default(StyleProperty::LineHeight(LineHeight::Absolute(
|
builder.push_default(StyleProperty::LineHeight(LineHeight::Absolute(
|
||||||
attrs.line_height * density,
|
attrs.line_height * density,
|
||||||
@@ -582,7 +550,10 @@ impl TextBuffer {
|
|||||||
builder.push(StyleProperty::Brush(color.clone()), range.clone());
|
builder.push(StyleProperty::Brush(color.clone()), range.clone());
|
||||||
}
|
}
|
||||||
if let Some(family) = family {
|
if let Some(family) = family {
|
||||||
builder.push(StyleProperty::FontFamily(family.family()), range.clone());
|
builder.push(
|
||||||
|
StyleProperty::FontFamily(FontFamily::from(family.as_str())),
|
||||||
|
range.clone(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if let Some(size) = span.font_size {
|
if let Some(size) = span.font_size {
|
||||||
builder.push(StyleProperty::FontSize(size * density), range.clone());
|
builder.push(StyleProperty::FontSize(size * density), range.clone());
|
||||||
@@ -642,8 +613,8 @@ mod tests {
|
|||||||
fn invalid_font_data_is_reported() {
|
fn invalid_font_data_is_reported() {
|
||||||
let mut data = TextData::default();
|
let mut data = TextData::default();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
data.register_font(Family::Icons, b"not a font" as &'static [u8]),
|
data.register_font("icons", b"not a font" as &'static [u8]),
|
||||||
Err(FontRegistrationError::InvalidFont(Family::Icons))
|
Err(FontRegistrationError::InvalidFont("icons".to_owned()))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -653,7 +624,7 @@ mod tests {
|
|||||||
let mut buffer = TextBuffer::new("ordinary platform text");
|
let mut buffer = TextBuffer::new("ordinary platform text");
|
||||||
buffer.shape(&mut data, &TextAttrs::default(), None, 1.0);
|
buffer.shape(&mut data, &TextAttrs::default(), None, 1.0);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
data.register_font(Family::Icons, b"not a font" as &'static [u8]),
|
data.register_font("icons", b"not a font" as &'static [u8]),
|
||||||
Err(FontRegistrationError::TextAlreadyShaped)
|
Err(FontRegistrationError::TextAlreadyShaped)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,13 +74,13 @@ impl Ui {
|
|||||||
#[track_caller]
|
#[track_caller]
|
||||||
pub fn register_font(
|
pub fn register_font(
|
||||||
&mut self,
|
&mut self,
|
||||||
family: crate::Family,
|
family: impl AsRef<str>,
|
||||||
data: impl AsRef<[u8]> + Send + Sync + 'static,
|
data: impl AsRef<[u8]> + Send + Sync + 'static,
|
||||||
) -> Result<(), crate::FontRegistrationError> {
|
) -> Result<(), crate::FontRegistrationError> {
|
||||||
self.data.text.register_font(family, data)
|
self.data.text.register_font(family, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_font_registered(&self, family: &crate::Family) -> bool {
|
pub fn is_font_registered(&self, family: impl AsRef<str>) -> bool {
|
||||||
self.data.text.is_font_registered(family)
|
self.data.text.is_font_registered(family)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -92,9 +92,9 @@ where
|
|||||||
btext("hmm"),
|
btext("hmm"),
|
||||||
btext("a"),
|
btext("a"),
|
||||||
(
|
(
|
||||||
btext("'").family(Family::Monospace).align(Align::TOP),
|
btext("'").family(MONOSPACE).align(Align::TOP),
|
||||||
btext("'").family(Family::Monospace),
|
btext("'").family(MONOSPACE),
|
||||||
btext(":gamer mode").family(Family::Monospace),
|
btext(":gamer mode").family(MONOSPACE),
|
||||||
rect(PaintId::CYAN).sized((10, 10)).center(),
|
rect(PaintId::CYAN).sized((10, 10)).center(),
|
||||||
rect(PaintId::RED).sized((100, 100)).center(),
|
rect(PaintId::RED).sized((100, 100)).center(),
|
||||||
rect(PaintId::PURPLE).sized((50, 50)).align(Align::TOP),
|
rect(PaintId::PURPLE).sized((50, 50)).align(Align::TOP),
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ impl<State, O, H: WidgetOption<State>> TextBuilder<State, O, H> {
|
|||||||
self.attrs.color = color;
|
self.attrs.color = color;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
pub fn family(mut self, family: Family) -> Self {
|
pub fn family(mut self, family: impl AsRef<str>) -> Self {
|
||||||
self.attrs.family = family;
|
self.attrs.family = family.as_ref().to_owned();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
pub fn line_height(mut self, height: f32) -> Self {
|
pub fn line_height(mut self, height: f32) -> Self {
|
||||||
|
|||||||
Reference in new issue
Block a user