diff --git a/app/src/ui/markdown.rs b/app/src/ui/markdown.rs index a380363..a2ab1eb 100644 --- a/app/src/ui/markdown.rs +++ b/app/src/ui/markdown.rs @@ -210,7 +210,7 @@ pub fn render_markdown(src: &str, base_size: f32, theme: &Theme) -> Rendered { TagEnd::CodeBlock => { spans.push( SpanStyle::new(range.clone()) - .family(Family::Monospace) + .family(MONOSPACE) .color(theme.code.clone()), ); 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); spans.push( SpanStyle::new(start..out.len()) - .family(Family::Monospace) + .family(MONOSPACE) .color(theme.code.clone()), ); } @@ -291,7 +291,7 @@ pub(crate) fn highlight_into( }; spans.push( SpanStyle::new(range.start + start..range.start + end) - .family(Family::Monospace) + .family(MONOSPACE) .color(syntax_color(span.kind, theme)), ); } @@ -525,7 +525,7 @@ mod tests { let r = block("```brainfuck\nlet x = 1;\n```"); assert_eq!(r.text, "let x = 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())); } diff --git a/app/src/ui/mod.rs b/app/src/ui/mod.rs index 7d2e8d6..d4b3456 100644 --- a/app/src/ui/mod.rs +++ b/app/src/ui/mod.rs @@ -15,12 +15,13 @@ use std::{mem, rc::Rc}; use theme::Theme; 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) { - if ui.is_font_registered(&Family::Icons) { + if ui.is_font_registered(ICON_FAMILY) { 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"); } diff --git a/app/src/ui/row.rs b/app/src/ui/row.rs index 2d74ca1..23252ff 100644 --- a/app/src/ui/row.rs +++ b/app/src/ui/row.rs @@ -198,11 +198,7 @@ where .spans(rendered.spans) .text_align(Align::LEFT) .wrap(!verbatim) - .family(if verbatim { - Family::Monospace - } else { - Family::SansSerif - }) + .family(if verbatim { MONOSPACE } else { SANS_SERIF }) .size(BASE_SIZE) .color(match frame { BlockFrame::Quote => theme.quote_text.clone(), diff --git a/app/src/ui/tool.rs b/app/src/ui/tool.rs index eb8b81c..d769b9a 100644 --- a/app/src/ui/tool.rs +++ b/app/src/ui/tool.rs @@ -80,18 +80,14 @@ fn text(content: impl Into, size: f32, color: PaintId) -> TextBuild } fn disclosure(glyph: &'static str, theme: &Theme) -> TextBuilder { - 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: &mut Rsc, body: TextBuilder, theme: &Theme) -> StrongWidget where Rsc::State: FocusHost, { - let field = body - .family(Family::Monospace) - .size(BODY_SIZE) - .wrap(false) - .add(rsc); + let field = body.family(MONOSPACE).size(BODY_SIZE).wrap(false).add(rsc); field .scrollable(Axis::X, Pin::Start) .pad(dp(RAW_PAD_DP)) diff --git a/docs/PLAN.md b/docs/PLAN.md index 4d40332..70911d7 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -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 event types or manually request redraws. -**Iris ships no fonts.** Applications register their own font bytes on `Ui` -before the first text shape and select them through the same `Family` used by -text widgets. `Family::Icons` is a semantic application-configured role, not a -particular icon set owned by the framework. ai-app owns its Nerd Fonts subset, -its codepoints, its license and the script that rebuilds it; body and monospace -families continue to come from the platform. +**Iris ships no fonts, and font families are application-named strings** +(2026-09-12). Applications register their own font bytes on `Ui` before the +first text shape and select them by the same string used by text widgets. The +public boundaries accept `AsRef`, so an application can use bare strings +or put its own semantic enum in front of them without Iris hardcoding the +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 must find inside an Iris checkout. `cargo iris apk` builds the Rust `cdylib` diff --git a/iris/core/src/primitive/text.rs b/iris/core/src/primitive/text.rs index c49031e..1f45f85 100644 --- a/iris/core/src/primitive/text.rs +++ b/iris/core/src/primitive/text.rs @@ -1,7 +1,7 @@ use crate::{Align, GlyphAtlas, GlyphKey, PaintId, PlacedGlyph, RegionAlign, Textures, util::Vec2}; use parley::{ - Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, FontStyle, FontWeight, - GenericFamily, Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty, + Alignment, AlignmentOptions, FontContext, FontFamily, FontStyle, FontWeight, GenericFamily, + Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty, fontique::{Blob, FontInfoOverride}, }; use std::{collections::HashMap, fmt, ops::Range, sync::Arc}; @@ -24,19 +24,14 @@ pub struct FontDiagnostics { #[derive(Clone, Debug, PartialEq, Eq)] pub enum FontRegistrationError { - UnsupportedFamily(Family), - AlreadyRegistered(Family), + AlreadyRegistered(String), TextAlreadyShaped, - InvalidFont(Family), + InvalidFont(String), } impl fmt::Display for FontRegistrationError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::UnsupportedFamily(family) => write!( - f, - "cannot register font data for {family:?}; use Family::Icons or Family::Named" - ), Self::AlreadyRegistered(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 /// input handler for the sake of one field. pub density: f32, - registered_families: HashMap, + registered_families: HashMap, next_registered_family: u64, shaping_started: bool, } @@ -169,12 +164,10 @@ fn patch_android_monospace(_font_cx: &mut FontContext) {} impl TextData { pub(crate) fn register_font( &mut self, - family: Family, + family: impl AsRef, data: impl AsRef<[u8]> + Send + Sync + 'static, ) -> Result<(), FontRegistrationError> { - if !matches!(family, Family::Icons | Family::Named(_)) { - return Err(FontRegistrationError::UnsupportedFamily(family)); - } + let family = family.as_ref().to_owned(); if self.shaping_started { return Err(FontRegistrationError::TextAlreadyShaped); } @@ -201,23 +194,18 @@ impl TextData { Ok(()) } - pub(crate) fn is_font_registered(&self, family: &Family) -> bool { - self.registered_families.contains_key(family) + pub(crate) fn is_font_registered(&self, family: impl AsRef) -> bool { + self.registered_families.contains_key(family.as_ref()) } /// Cloned rather than borrowed because the caller needs it while the /// layout builder holds `&mut self` -- a `String` per shaped registered /// 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) { - return Family::Named(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(), + return name.clone(); } + family.to_owned() } 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 -/// borrowed `FontFamily<'_>` so that a widget can hold one without a lifetime. -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -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) - } -} +pub const SANS_SERIF: &str = "sans-serif"; +pub const SERIF: &str = "serif"; +pub const MONOSPACE: &str = "monospace"; #[derive(Clone, PartialEq)] pub struct SpanStyle { pub range: Range, pub color: Option, - pub family: Option, + pub family: Option, pub font_size: Option, pub bold: bool, pub italic: bool, @@ -442,8 +408,8 @@ impl SpanStyle { self.color = Some(color); self } - pub fn family(mut self, family: Family) -> Self { - self.family = Some(family); + pub fn family(mut self, family: impl AsRef) -> Self { + self.family = Some(family.as_ref().to_owned()); self } pub fn font_size(mut self, size: f32) -> Self { @@ -469,7 +435,7 @@ pub struct TextAttrs { pub color: PaintId, pub font_size: f32, pub line_height: f32, - pub family: Family, + pub family: String, pub wrap: bool, pub align: RegionAlign, } @@ -483,7 +449,7 @@ impl Default for TextAttrs { color: PaintId::WHITE, font_size: size, line_height: size * LINE_HEIGHT_MULT, - family: Family::SansSerif, + family: SANS_SERIF.to_owned(), wrap: false, align: Align::CENTER_LEFT, } @@ -562,7 +528,7 @@ impl TextBuffer { return; } let base_family = data.resolve_family(&attrs.family); - let span_families: Vec> = self + let span_families: Vec> = self .spans .iter() .map(|span| span.family.as_ref().map(|f| data.resolve_family(f))) @@ -570,7 +536,9 @@ impl TextBuffer { let mut builder = data .layout_cx .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::LineHeight(LineHeight::Absolute( attrs.line_height * density, @@ -582,7 +550,10 @@ impl TextBuffer { builder.push(StyleProperty::Brush(color.clone()), range.clone()); } 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 { builder.push(StyleProperty::FontSize(size * density), range.clone()); @@ -642,8 +613,8 @@ mod tests { fn invalid_font_data_is_reported() { let mut data = TextData::default(); assert_eq!( - data.register_font(Family::Icons, b"not a font" as &'static [u8]), - Err(FontRegistrationError::InvalidFont(Family::Icons)) + data.register_font("icons", b"not a font" as &'static [u8]), + Err(FontRegistrationError::InvalidFont("icons".to_owned())) ); } @@ -653,7 +624,7 @@ mod tests { let mut buffer = TextBuffer::new("ordinary platform text"); buffer.shape(&mut data, &TextAttrs::default(), None, 1.0); 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) ); } diff --git a/iris/core/src/ui/mod.rs b/iris/core/src/ui/mod.rs index a9c158b..b4bead8 100644 --- a/iris/core/src/ui/mod.rs +++ b/iris/core/src/ui/mod.rs @@ -74,13 +74,13 @@ impl Ui { #[track_caller] pub fn register_font( &mut self, - family: crate::Family, + family: impl AsRef, data: impl AsRef<[u8]> + Send + Sync + 'static, ) -> Result<(), crate::FontRegistrationError> { 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) -> bool { self.data.text.is_font_registered(family) } diff --git a/iris/examples/tabs/lib.rs b/iris/examples/tabs/lib.rs index 6d762d8..46f8a04 100644 --- a/iris/examples/tabs/lib.rs +++ b/iris/examples/tabs/lib.rs @@ -92,9 +92,9 @@ where btext("hmm"), btext("a"), ( - btext("'").family(Family::Monospace).align(Align::TOP), - btext("'").family(Family::Monospace), - btext(":gamer mode").family(Family::Monospace), + btext("'").family(MONOSPACE).align(Align::TOP), + btext("'").family(MONOSPACE), + btext(":gamer mode").family(MONOSPACE), rect(PaintId::CYAN).sized((10, 10)).center(), rect(PaintId::RED).sized((100, 100)).center(), rect(PaintId::PURPLE).sized((50, 50)).align(Align::TOP), diff --git a/iris/src/widget/text/build.rs b/iris/src/widget/text/build.rs index 4c835e2..0d442a5 100644 --- a/iris/src/widget/text/build.rs +++ b/iris/src/widget/text/build.rs @@ -20,8 +20,8 @@ impl> TextBuilder { self.attrs.color = color; self } - pub fn family(mut self, family: Family) -> Self { - self.attrs.family = family; + pub fn family(mut self, family: impl AsRef) -> Self { + self.attrs.family = family.as_ref().to_owned(); self } pub fn line_height(mut self, height: f32) -> Self {