Skip to main content

odox_ui/
fonts.rs

1//! Handing egui the faces a document's font families resolve to on this
2//! machine; which face that is, is `odox-fonts`' answer.
3//!
4//! The applications carry no fonts. A document names a family — `Liberation Serif`, `Times
5//! New Roman`, `Arial` — and the machine is asked for it, because every platform
6//! this ships on carries a metrically compatible face for the families office
7//! documents use, and three applications carrying a megabyte of fonts each would
8//! be three megabytes spent on a question the operating system has already
9//! answered. The cost is that a document naming a family the machine does not
10//! have is drawn in a fallback, which is what every other application does too.
11//!
12//! Faces are loaded once per document, not once per frame: egui rebuilds its
13//! glyph atlas when the font definitions change, so the families a document uses
14//! are resolved when it opens and handed over in one call.
15//
16// Author: David M. Anderson
17// Built with AI assistance (Claude, Anthropic)
18
19use std::collections::{BTreeMap, BTreeSet};
20use std::sync::Arc;
21
22use eframe::egui::{FontData, FontDefinitions, FontFamily};
23use odox_core::{Element, Ns};
24
25pub use odox_fonts::Variant;
26
27/// The egui font family a document's family name and variant resolve to.
28///
29/// The name is the one the document used, so that two documents naming the same
30/// family share an atlas entry and a document naming a family nobody has still
31/// gets a family that exists and draws in the fallback.
32pub fn family_of(family: &str, variant: Variant) -> FontFamily {
33    let suffix = match (variant.bold, variant.italic) {
34        (false, false) => "",
35        (true, false) => ":bold",
36        (false, true) => ":italic",
37        (true, true) => ":bolditalic",
38    };
39    FontFamily::Name(format!("{family}{suffix}").into())
40}
41
42/// The font definitions for a document, from its parts: the families its
43/// styles name and the characters its text holds.
44pub fn for_document(parts: &[&Element]) -> FontDefinitions {
45    definitions(&families_used(parts), &characters_used(parts))
46}
47
48/// Build the font definitions for a document: egui's own, plus a face for every
49/// family the document names.
50///
51/// The fallback chain behind each face is egui's built-in proportional font and
52/// its emoji fonts, so a glyph the document's own face lacks is still drawn
53/// rather than shown as a box. egui's own fonts are small and cover few
54/// scripts, so a character in `characters` that no face loaded so far has is
55/// drawn from the first face on the machine that has it, added at the end of
56/// every chain where it catches only what nothing before it draws.
57pub fn definitions(families: &BTreeSet<String>, characters: &BTreeSet<char>) -> FontDefinitions {
58    let mut definitions = FontDefinitions::default();
59    let fallback = definitions
60        .families
61        .get(&FontFamily::Proportional)
62        .cloned()
63        .unwrap_or_default();
64
65    let database = odox_fonts::database();
66
67    for family in families {
68        for variant in Variant::ALL {
69            let key = match family_of(family, variant) {
70                FontFamily::Name(name) => name.to_string(),
71                // `family_of` builds a named family and nothing else; the other
72                // arms exist only because `FontFamily` is an enum.
73                other => format!("{other:?}"),
74            };
75            let mut chain = Vec::new();
76            if let Some(face) =
77                odox_fonts::find(&database, family, variant).and_then(|id| load(&database, id))
78            {
79                definitions.font_data.insert(key.clone(), Arc::new(face));
80                chain.push(key.clone());
81            }
82            chain.extend(fallback.iter().cloned());
83            definitions
84                .families
85                .insert(FontFamily::Name(key.into()), chain);
86        }
87    }
88
89    let mut added = Vec::new();
90    for &character in characters {
91        if character.is_whitespace() || character.is_control() {
92            continue;
93        }
94        let drawn = definitions
95            .font_data
96            .values()
97            .any(|face| odox_fonts::covers(&face.font, face.index, character));
98        if drawn {
99            continue;
100        }
101        let Some(face) = odox_fonts::faces_with(&database, character, Variant::default())
102            .next()
103            .and_then(|id| load(&database, id))
104        else {
105            continue;
106        };
107        let key = format!("machine fallback {}", added.len());
108        definitions.font_data.insert(key.clone(), Arc::new(face));
109        added.push(key);
110    }
111    for chain in definitions.families.values_mut() {
112        chain.extend(added.iter().cloned());
113    }
114    definitions
115}
116
117/// One face of the machine's, read.
118fn load(database: &odox_fonts::fontdb::Database, id: odox_fonts::fontdb::ID) -> Option<FontData> {
119    let index = database.face(id)?.index;
120    database.with_face_data(id, |data, face_index| FontData {
121        font: data.to_vec().into(),
122        // A font collection holds several faces in one file and `fontdb` reports
123        // which of them answered; handing over the file without the index draws
124        // the wrong one.
125        index: face_index.max(index),
126        tweak: eframe::egui::FontTweak::default(),
127    })
128}
129
130/// Every character a document's parts hold as text.
131pub fn characters_used(parts: &[&Element]) -> BTreeSet<char> {
132    parts
133        .iter()
134        .flat_map(|part| part.plain_text().chars().collect::<Vec<_>>())
135        .collect()
136}
137
138/// Every font family a document's styles name.
139///
140/// Read from the styles rather than from the text, because a family is named in a
141/// style and used by whatever references it, and because the answer is wanted
142/// before the first frame is drawn.
143pub fn families_used(parts: &[&Element]) -> BTreeSet<String> {
144    let mut families = BTreeSet::new();
145    let mut faces: BTreeMap<String, String> = BTreeMap::new();
146    for root in parts {
147        collect(root, &mut families, &mut faces);
148    }
149    // A style naming a font face rather than a family resolves through the
150    // declarations, and the declarations are what a renderer has to ask the
151    // machine for.
152    let resolved: BTreeSet<String> = families
153        .iter()
154        .map(|name| faces.get(name).cloned().unwrap_or_else(|| name.clone()))
155        .collect();
156    resolved
157}
158
159fn collect(
160    element: &Element,
161    families: &mut BTreeSet<String>,
162    faces: &mut BTreeMap<String, String>,
163) {
164    if element.is(&Ns::Style, "font-face")
165        && let Some(name) = element.attr(&Ns::Style, "name")
166    {
167        let family = element
168            .attr(&Ns::Svg, "font-family")
169            .unwrap_or(name)
170            .trim_matches('\'')
171            .to_owned();
172        faces.insert(name.to_owned(), family);
173    }
174    for (ns, local) in [(Ns::Style, "font-name"), (Ns::Fo, "font-family")] {
175        if let Some(name) = element.attr(&ns, local) {
176            let name = name.trim_matches('\'');
177            if !name.is_empty() {
178                families.insert(name.to_owned());
179            }
180        }
181    }
182    for child in element.elements() {
183        collect(child, families, faces);
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    #[test]
192    fn family_of_names_a_variant_with_a_suffix() {
193        let plain = Variant {
194            bold: false,
195            italic: false,
196        };
197        let bold = Variant {
198            bold: true,
199            italic: false,
200        };
201        let italic = Variant {
202            bold: false,
203            italic: true,
204        };
205        let both = Variant {
206            bold: true,
207            italic: true,
208        };
209        assert_eq!(family_of("Arial", plain), FontFamily::Name("Arial".into()));
210        assert_eq!(
211            family_of("Arial", bold),
212            FontFamily::Name("Arial:bold".into())
213        );
214        assert_eq!(
215            family_of("Arial", italic),
216            FontFamily::Name("Arial:italic".into())
217        );
218        assert_eq!(
219            family_of("Arial", both),
220            FontFamily::Name("Arial:bolditalic".into())
221        );
222    }
223
224    #[test]
225    fn a_character_no_loaded_face_has_is_drawn_from_one_of_the_machines() {
226        let database = odox_fonts::database();
227        if odox_fonts::faces_with(&database, '日', Variant::default())
228            .next()
229            .is_none()
230        {
231            eprintln!("no face on this machine has 日: nothing to check");
232            return;
233        }
234        let families = BTreeSet::from(["Liberation Serif".to_owned()]);
235        let defs = definitions(&families, &BTreeSet::from(['日', 'a']));
236        let chain = &defs.families[&family_of("Liberation Serif", Variant::default())];
237        let added: Vec<&String> = chain
238            .iter()
239            .filter(|key| key.starts_with("machine fallback"))
240            .collect();
241        assert_eq!(added.len(), 1, "one face for the one character missing");
242        assert_eq!(chain.last(), Some(added[0]), "it comes last");
243        let face = &defs.font_data[added[0]];
244        assert!(odox_fonts::covers(&face.font, face.index, '日'));
245    }
246}