iris: load application-owned fonts

This commit is contained in:
iris committed 2026-09-11 14:14:41 -04:00
1 parent d1da066547
commit beaf24c75d
8 files changed
+124 -154

No files matched your search

-5
View File
@@ -1,5 +0,0 @@
pub const OPEN: &str = "\u{F035D}";
pub const CLOSED: &str = "\u{F035F}";
pub const COLLAPSE: &str = "\u{F0360}";
-1
View File
@@ -19,7 +19,6 @@ mod render;
mod ui;
mod widget;
pub mod icon;
pub mod util;
pub use attr::*;
+106 -69
View File
@@ -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<String>,
pub italic_resolved: Option<String>,
pub mono_resolved: Option<String>,
/// 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<String>,
}
#[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<PaintId>,
@@ -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<String>,
registered_families: HashMap<Family, String>,
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<String> {
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<String> {
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<f32>,
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)
);
}
}
+18
View File
@@ -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`.