From beaf24c75d17f16a4b9c341940aab3c9c20a87cb Mon Sep 17 00:00:00 2001 From: iris <2+iris@noreply.localhost> Date: Fri, 11 Sep 2026 14:14:41 -0400 Subject: [PATCH] iris: load application-owned fonts --- core/assets/fonts/NERD_FONTS_LICENSE.txt | 21 --- core/assets/fonts/nerd_icons.ttf | Bin 992 -> 0 bytes core/build-icon-font.sh | 56 -------- core/src/icon.rs | 5 - core/src/lib.rs | 1 - core/src/primitive/text.rs | 175 ++++++++++++++--------- core/src/ui/mod.rs | 18 +++ src/android/render.rs | 2 - 8 files changed, 124 insertions(+), 154 deletions(-) delete mode 100644 core/assets/fonts/NERD_FONTS_LICENSE.txt delete mode 100644 core/assets/fonts/nerd_icons.ttf delete mode 100755 core/build-icon-font.sh delete mode 100644 core/src/icon.rs diff --git a/core/assets/fonts/NERD_FONTS_LICENSE.txt b/core/assets/fonts/NERD_FONTS_LICENSE.txt deleted file mode 100644 index 06eb073..0000000 --- a/core/assets/fonts/NERD_FONTS_LICENSE.txt +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2014 Ryan L McIntyre - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/core/assets/fonts/nerd_icons.ttf b/core/assets/fonts/nerd_icons.ttf deleted file mode 100644 index 5395b949d8da6b3bf5ef0416ee777cefc40cca2e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 992 zcmb7DO=uHQ5dQY#E=KI=C_Z4NwK*Isbm_V&0x4UL>~aUbmAZWWxMP?9`jv(Rg)pfjVM9BGYAWW1t7)_5>njSff!p6)1J1>O-Er}Zo)^5$JF!p@s3 zlvVmvdOt9NJ7gM`^l_Pz>ITjdrMlv-Z47i17x7@ATYtsasGv8nXV}9pTtOQhY~vo* zdF~CwaM_d3-FGpKB1)Jg-^CJRJmz2Qzlcl!=s@~9, pub italic_resolved: Option, pub mono_resolved: Option, - /// The family the bundled icon font registered under, or `None` if - /// registering it failed. Reported rather than assumed: it is the one - /// font iris ships, so `None` is a broken build and must not look - /// like a device that happens to lack a face. - pub icon_family: Option, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum FontRegistrationError { + UnsupportedFamily(Family), + AlreadyRegistered(Family), + TextAlreadyShaped, + InvalidFont(Family), +} + +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:?}") + } + Self::TextAlreadyShaped => write!( + f, + "cannot register font data after text has been shaped; register application fonts before the first draw" + ), + Self::InvalidFont(family) => { + write!( + f, + "font data registered for {family:?} contains no usable fonts" + ) + } + } + } +} + +impl std::error::Error for FontRegistrationError {} + pub struct TextData { pub font_cx: FontContext, pub layout_cx: LayoutContext, @@ -46,41 +72,28 @@ 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, - /// The family name [`NERD_ICONS`] registered under, which is what - /// [`Family::Icons`] resolves to. `None` only if registering the - /// bundled font failed, which is a broken build rather than a - /// platform difference -- said in the startup diagnostics rather than - /// silently drawn as tofu. - pub icon_family: Option, + registered_families: HashMap, + next_registered_family: u64, + shaping_started: bool, } impl Default for TextData { fn default() -> Self { let mut font_cx = FontContext::new(); patch_android_monospace(&mut font_cx); - let icon_family = register_icon_font(&mut font_cx); Self { font_cx, layout_cx: LayoutContext::new(), scale_cx: ScaleContext::new(), atlas: GlyphAtlas::default(), density: 1.0, - icon_family, + registered_families: HashMap::new(), + next_registered_family: 0, + shaping_started: false, } } } -fn register_icon_font(font_cx: &mut FontContext) -> Option { - let blob = Blob::new(Arc::new(NERD_ICONS)); - let id = font_cx - .collection - .register_fonts(blob, None) - .into_iter() - .map(|(id, _)| id) - .next()?; - font_cx.collection.family_name(id).map(str::to_string) -} - /// So this reads `fonts.xml` itself (already on-device, already the /// authority Compose's own `Typeface.MONOSPACE` resolves through) for the /// filename that declaration names, then finds which of fontique's @@ -154,16 +167,56 @@ fn android_monospace_font_filename() -> Option { fn patch_android_monospace(_font_cx: &mut FontContext) {} impl TextData { + pub(crate) fn register_font( + &mut self, + family: Family, + data: impl AsRef<[u8]> + Send + Sync + 'static, + ) -> Result<(), FontRegistrationError> { + if !matches!(family, Family::Icons | Family::Named(_)) { + return Err(FontRegistrationError::UnsupportedFamily(family)); + } + if self.shaping_started { + return Err(FontRegistrationError::TextAlreadyShaped); + } + if self.registered_families.contains_key(&family) { + return Err(FontRegistrationError::AlreadyRegistered(family)); + } + + // Parley selects fonts by family rather than by a face handle. Give + // application data a private family name so selecting it cannot find + // a system font that happens to carry the same embedded metadata. + let private_name = format!("__iris_registered_font_{}__", self.next_registered_family); + let fonts = self.font_cx.collection.register_fonts( + Blob::new(Arc::new(data)), + Some(FontInfoOverride { + family_name: Some(&private_name), + ..Default::default() + }), + ); + if fonts.is_empty() { + return Err(FontRegistrationError::InvalidFont(family)); + } + self.next_registered_family += 1; + self.registered_families.insert(family, private_name); + Ok(()) + } + + pub(crate) fn is_font_registered(&self, family: &Family) -> bool { + self.registered_families.contains_key(family) + } + /// Cloned rather than borrowed because the caller needs it while the - /// layout builder holds `&mut self` -- a `String` per shaped icon run, - /// paid only when the layout is rebuilt. + /// 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 { + if let Some(name) = self.registered_families.get(family) { + return Family::Named(name.clone()); + } match family { - Family::Icons => self - .icon_family - .clone() - .map_or(Family::Icons, Family::Named), - other => other.clone(), + Family::Icons => panic!( + "Family::Icons has no font; register application font data with Ui::register_font before the first draw" + ), + _ => family.clone(), } } @@ -236,7 +289,6 @@ impl TextData { bold_resolved, italic_resolved, mono_resolved, - icon_family: self.icon_family.clone(), } } @@ -339,15 +391,13 @@ 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, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum Family { SansSerif, Serif, Monospace, - /// The bundled icon font -- see [`crate::icon`] for what is in it. - /// Named as an intention rather than as a font name because only - /// [`TextData`] knows what the file registered as; it resolves this - /// during shaping ([`TextData::resolve_family`]). + /// The icon font supplied by the application through + /// [`crate::Ui::register_font`]. Icons, Named(String), } @@ -507,6 +557,7 @@ impl TextBuffer { width: Option, density: f32, ) { + data.shaping_started = true; if self.shaped.as_ref() == Some(&(attrs.clone(), width, density)) { return; } @@ -586,38 +637,24 @@ pub struct RenderedText { #[cfg(test)] mod tests { use super::*; - use crate::icon; #[test] - fn every_icon_is_in_the_bundled_font() { - let font = FontRef::from_index(NERD_ICONS, 0).expect("the bundled icon font parses"); - let charmap = font.charmap(); - for (name, glyph) in [ - ("OPEN", icon::OPEN), - ("CLOSED", icon::CLOSED), - ("COLLAPSE", icon::COLLAPSE), - ] { - let mut chars = glyph.chars(); - let ch = chars.next().expect("an icon is one character"); - assert!(chars.next().is_none(), "{name} is more than one character"); - assert_ne!( - charmap.map(ch), - 0, - "{name} (U+{:04X}) is not in nerd_icons.ttf -- add it to \ - build-icon-font.sh's GLYPHS and rerun the script", - ch as u32 - ); - } + 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)) + ); } #[test] - fn the_icon_family_registers_and_resolves() { - let data = TextData::default(); - let family = data.resolve_family(&Family::Icons); - assert!( - matches!(family, Family::Named(_)), - "the bundled icon font did not register: {:?}", - data.icon_family + fn registering_after_shaping_is_reported() { + let mut data = TextData::default(); + 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]), + Err(FontRegistrationError::TextAlreadyShaped) ); } } diff --git a/core/src/ui/mod.rs b/core/src/ui/mod.rs index c7575ad..a9c158b 100644 --- a/core/src/ui/mod.rs +++ b/core/src/ui/mod.rs @@ -66,6 +66,24 @@ pub struct Ui { } impl Ui { + /// Register application-owned font data for a semantic or named family. + /// + /// This must happen before the first text shape. Existing text layouts + /// cache their resolved faces, so accepting a later registration would + /// leave already-shaped widgets displaying the old result. + #[track_caller] + pub fn register_font( + &mut self, + family: crate::Family, + 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 { + self.data.text.is_font_registered(family) + } + /// A read-only handle to the retained result of the last completed frame. /// The handle is owned so a caller may keep its read guard while mutating /// unrelated resources on the `Rsc` that owns this `Ui`. diff --git a/src/android/render.rs b/src/android/render.rs index 7dcda75..ccd9f89 100644 --- a/src/android/render.rs +++ b/src/android/render.rs @@ -321,7 +321,6 @@ impl AndroidRenderer { mono={default_mono_family:?}\n\ fonts resolved: regular={regular:?} bold={bold:?} italic={italic:?} \ mono={mono:?}\n\ - icon font: {icons:?}\n\ wgpu errors since surface creation:\n {errors_text}\n\n\ {frame_report}", name = self.adapter_name, @@ -338,7 +337,6 @@ impl AndroidRenderer { bold = font.bold_resolved, italic = font.italic_resolved, mono = font.mono_resolved, - icons = font.icon_family, ) }