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 5395b94..0000000 Binary files a/core/assets/fonts/nerd_icons.ttf and /dev/null differ diff --git a/core/build-icon-font.sh b/core/build-icon-font.sh deleted file mode 100755 index dc46542..0000000 --- a/core/build-icon-font.sh +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env bash -# Rebuilds iris/core/assets/fonts/nerd_icons.ttf. -# -# iris draws its icons as glyphs in a Nerd Fonts subset it ships, rather -# than as ordinary Unicode out of whatever the platform resolved. Unicode's -# own geometric shapes are what this replaced: `tool.rs` set its disclosure -# mark with U+25B8/25BE/25B4, and once iris stopped bundling fonts -# (2026-09-07) Iris's phone drew an empty box for them and this VM drew a -# dot. UI_RULES: "don't rely on characters the platform might not have -- -# ship the glyph or the asset rather than hoping." -# -# The whole symbols font is 3 MB for the handful below, so what is -# committed is a subset. Add a codepoint to GLYPHS *and* to -# `iris/core/src/icon.rs` (the two lists have to agree -- a codepoint in -# the Rust that this script did not subset is a glyph that silently isn't -# there), then run this and commit the result. -# -# Needs python3 and network access; fontTools is fetched into a temporary -# venv, so nothing has to be installed on the machine. -# -# The Mono face makes icon metrics stable; the Material Design family keeps -# their meanings conventional. -set -euo pipefail - -GLYPHS=( - U+F035D # md-menu_down -- a card that is open - U+F035F # md-menu_right -- a card that opens - U+F0360 # md-menu_up -- collapse this group again -) - -url=https://github.com/ryanoasis/nerd-fonts/releases/latest/download/NerdFontsSymbolsOnly.zip -here="$(cd "$(dirname "$0")" && pwd)" -out="$here/assets/fonts/nerd_icons.ttf" -work="$(mktemp -d)" -trap 'rm -rf "$work"' EXIT - -echo "Fetching $url" -curl -fsSL -o "$work/nf.zip" "$url" -python3 -c 'import sys,zipfile; zipfile.ZipFile(sys.argv[1]).extractall(sys.argv[2])' "$work/nf.zip" "$work" - -python3 -m venv "$work/venv" -"$work/venv/bin/pip" -q install fonttools - -unicodes="$(IFS=,; echo "${GLYPHS[*]}")" -mkdir -p "$(dirname "$out")" -# The Mono face, where every glyph is one em wide and one em tall, so two -# icons at the same font size are the same size without either being given -# one, which makes an icon's box predictable beside a line of text. -"$work/venv/bin/pyftsubset" "$work/SymbolsNerdFontMono-Regular.ttf" \ - --unicodes="$unicodes" \ - --layout-features= \ - --drop-tables+=DSIG \ - --output-file="$out" -cp "$work/LICENSE" "$here/assets/fonts/NERD_FONTS_LICENSE.txt" - -echo "Wrote $out ($(stat -c %s "$out") bytes) with ${#GLYPHS[@]} glyphs" diff --git a/core/src/icon.rs b/core/src/icon.rs deleted file mode 100644 index 728f641..0000000 --- a/core/src/icon.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub const OPEN: &str = "\u{F035D}"; - -pub const CLOSED: &str = "\u{F035F}"; - -pub const COLLAPSE: &str = "\u{F0360}"; diff --git a/core/src/lib.rs b/core/src/lib.rs index f93a844..3122f82 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -19,7 +19,6 @@ mod render; mod ui; mod widget; -pub mod icon; pub mod util; pub use attr::*; diff --git a/core/src/primitive/text.rs b/core/src/primitive/text.rs index 011aa41..c49031e 100644 --- a/core/src/primitive/text.rs +++ b/core/src/primitive/text.rs @@ -2,18 +2,15 @@ use crate::{Align, GlyphAtlas, GlyphKey, PaintId, PlacedGlyph, RegionAlign, Text use parley::{ Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, FontStyle, FontWeight, GenericFamily, Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty, - fontique::Blob, + fontique::{Blob, FontInfoOverride}, }; -use std::ops::Range; -use std::sync::Arc; +use std::{collections::HashMap, fmt, ops::Range, sync::Arc}; use swash::{ FontRef, scale::{Render, ScaleContext, Source, StrikeWith}, zeno::{Format, Vector}, }; -const NERD_ICONS: &[u8] = include_bytes!("../../assets/fonts/nerd_icons.ttf"); - #[derive(Clone, Debug)] pub struct FontDiagnostics { pub families_found: usize, @@ -23,13 +20,42 @@ pub struct FontDiagnostics { pub bold_resolved: Option, 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, ) }