Skip to main content

text_typeset/font/
registry.rs

1use std::collections::HashMap;
2use std::path::PathBuf;
3use std::sync::Arc;
4use std::sync::OnceLock;
5
6use fontdb::{Database, Family, Query, Source, Style, Weight};
7use harfrust::{FontRef, ShaperData};
8
9use crate::types::FontFaceId;
10
11/// Byte container owned by a [`FontEntry`]. Erased behind a trait
12/// object so the registry can hold any of `Vec<u8>`, `memmap2::Mmap`,
13/// or `&'static [u8]` (via a wrapper) without copying. fontdb's
14/// `Source::Binary` uses the same shape, so the same `Arc` can be
15/// shared with the database.
16pub type SharedFontData = Arc<dyn AsRef<[u8]> + Sync + Send>;
17
18/// Backing bytes for a [`FontEntry`].
19///
20/// Explicitly registered fonts are [`Resident`](FontData::Resident) — the
21/// caller handed us the bytes. System fonts discovered by
22/// [`load_system_fonts`](FontRegistry::new) are [`Lazy`](FontData::Lazy):
23/// we keep only the file path until the face is actually shaped or queried
24/// for coverage, then read and cache it. This keeps `new()` cheap even
25/// when the OS has hundreds of installed faces.
26enum FontData {
27    Resident(SharedFontData),
28    Lazy {
29        path: PathBuf,
30        cell: OnceLock<SharedFontData>,
31    },
32}
33
34pub struct FontEntry {
35    pub fontdb_id: fontdb::ID,
36    pub face_index: u32,
37    data: FontData,
38    pub swash_cache_key: swash::CacheKey,
39    /// True for faces discovered via the OS font enumeration. Glyph
40    /// fallback prefers explicitly-registered faces and only reaches for
41    /// system faces as a last resort (see
42    /// [`find_fallback_font`](crate::font::resolve::find_fallback_font)).
43    pub is_system: bool,
44    /// Lazily-built HarfBuzz shaping tables for this face. `ShaperData`
45    /// preprocesses the font's GSUB/GPOS/cmap tables and is independent
46    /// of any `FontRef` lifetime, so it can be built once and reused
47    /// across every shape call. Built on first shape via
48    /// [`shaper_data`](Self::shaper_data). `OnceLock` keeps `FontEntry`
49    /// `Sync` (its internal caches are atomic).
50    shaper_data: OnceLock<ShaperData>,
51}
52
53impl FontEntry {
54    /// Borrow the underlying font bytes, loading them from disk on first
55    /// use for lazily-tracked system faces. Returns an empty slice if a
56    /// system font file can no longer be read (uninstalled mid-session);
57    /// shaping then degrades gracefully to `None`/`.notdef`.
58    pub fn bytes(&self) -> &[u8] {
59        match &self.data {
60            // Two `as_ref()` hops: Arc<dyn AsRef<[u8]>> → dyn → &[u8].
61            FontData::Resident(data) => (**data).as_ref(),
62            FontData::Lazy { path, cell } => {
63                let arc = cell.get_or_init(|| {
64                    let bytes = std::fs::read(path).unwrap_or_default();
65                    Arc::new(bytes) as SharedFontData
66                });
67                (**arc).as_ref()
68            }
69        }
70    }
71
72    /// Borrow this face's shaping tables, building them on first use.
73    ///
74    /// `ShaperData::new` reprocesses the font tables; doing it once per
75    /// face (instead of once per shape call) avoids redundant work on
76    /// every relayout/keystroke. The `font` must refer to this same
77    /// face — callers already hold a `FontRef` opened from `bytes()`.
78    pub fn shaper_data(&self, font: &FontRef) -> &ShaperData {
79        self.shaper_data.get_or_init(|| ShaperData::new(font))
80    }
81}
82
83pub struct FontRegistry {
84    fontdb: Database,
85    fonts: Vec<Option<FontEntry>>,
86    /// fontdb face ID → our `FontFaceId`, so `query_font` doesn't scan
87    /// `fonts` linearly (it can hold hundreds of system faces).
88    fontdb_index: HashMap<fontdb::ID, FontFaceId>,
89    generic_families: HashMap<String, String>,
90    default_font: Option<FontFaceId>,
91    default_size_px: f32,
92}
93
94impl Default for FontRegistry {
95    fn default() -> Self {
96        Self::new()
97    }
98}
99
100impl FontRegistry {
101    /// Create a registry pre-populated with the operating system's fonts.
102    ///
103    /// OS faces participate in family lookup and glyph fallback so that
104    /// arbitrary documents (CJK, emoji, scripts the host didn't bundle)
105    /// still render. Their bytes load lazily on first use, but the
106    /// enumeration itself scans the OS font directories — a one-time
107    /// startup cost. Use [`new_without_system_fonts`](Self::new_without_system_fonts)
108    /// when the host ships a controlled font set and wants neither the
109    /// scan nor implicit OS fonts.
110    pub fn new() -> Self {
111        let mut registry = Self::new_without_system_fonts();
112        registry.load_system_fonts();
113        registry
114    }
115
116    /// Create an empty registry with no system fonts. Fonts enter only
117    /// via the `register_font*` methods.
118    pub fn new_without_system_fonts() -> Self {
119        Self {
120            fontdb: Database::new(),
121            fonts: Vec::new(),
122            fontdb_index: HashMap::new(),
123            generic_families: HashMap::new(),
124            default_font: None,
125            default_size_px: 16.0,
126        }
127    }
128
129    /// Enumerate OS fonts into `fontdb` and register a lazy [`FontEntry`]
130    /// for each so they join family lookup and the fallback chain. Bytes
131    /// load on first access; only the file path is kept until then.
132    fn load_system_fonts(&mut self) {
133        self.fontdb.load_system_fonts();
134
135        // Collect first: `faces()` borrows `self.fontdb` immutably while
136        // the loop needs `&mut self` to push entries.
137        let discovered: Vec<(fontdb::ID, u32, PathBuf)> = self
138            .fontdb
139            .faces()
140            .filter_map(|f| match &f.source {
141                Source::File(path) => Some((f.id, f.index, path.clone())),
142                Source::SharedFile(path, _) => Some((f.id, f.index, path.clone())),
143                Source::Binary(_) => None,
144            })
145            .collect();
146
147        for (fontdb_id, face_index, path) in discovered {
148            if self.fontdb_index.contains_key(&fontdb_id) {
149                continue;
150            }
151            let entry = FontEntry {
152                fontdb_id,
153                face_index,
154                data: FontData::Lazy {
155                    path,
156                    cell: OnceLock::new(),
157                },
158                swash_cache_key: swash::CacheKey::new(),
159                is_system: true,
160                shaper_data: OnceLock::new(),
161            };
162            let face_id = FontFaceId(self.fonts.len() as u32);
163            self.fonts.push(Some(entry));
164            self.fontdb_index.insert(fontdb_id, face_id);
165        }
166    }
167
168    /// Register a font from raw bytes. Returns IDs for all faces found
169    /// (font collections may contain multiple faces).
170    ///
171    /// This copies the bytes into an owned `Vec<u8>`. For large fonts
172    /// where the caller already holds the data in a shareable
173    /// container (e.g. an `Arc<Mmap>` for a system emoji font), use
174    /// [`register_font_shared`](Self::register_font_shared) instead to
175    /// avoid the copy.
176    pub fn register_font(&mut self, data: &[u8]) -> Vec<FontFaceId> {
177        let arc_data: SharedFontData = Arc::new(data.to_vec());
178        self.register_font_shared(arc_data)
179    }
180
181    /// Register a font from a pre-built shared byte container,
182    /// avoiding the copy that [`register_font`](Self::register_font)
183    /// would perform.
184    ///
185    /// The shared container is held by every resulting [`FontEntry`]
186    /// and by fontdb's `Source::Binary`. Drop it after the registry
187    /// drops the entries.
188    pub fn register_font_shared(&mut self, data: SharedFontData) -> Vec<FontFaceId> {
189        let source = Source::Binary(data.clone());
190        let fontdb_ids = self.fontdb.load_font_source(source);
191
192        let mut face_ids = Vec::new();
193        for fontdb_id in fontdb_ids {
194            let face_index = self.fontdb.face(fontdb_id).map(|f| f.index).unwrap_or(0);
195
196            let swash_cache_key = swash::CacheKey::new();
197            let entry = FontEntry {
198                fontdb_id,
199                face_index,
200                data: FontData::Resident(data.clone()),
201                swash_cache_key,
202                is_system: false,
203                shaper_data: OnceLock::new(),
204            };
205
206            let face_id = FontFaceId(self.fonts.len() as u32);
207            self.fonts.push(Some(entry));
208            self.fontdb_index.insert(fontdb_id, face_id);
209            face_ids.push(face_id);
210        }
211        face_ids
212    }
213
214    /// Register a font with explicit metadata, overriding the font's name table.
215    pub fn register_font_as(
216        &mut self,
217        data: &[u8],
218        family: &str,
219        weight: u16,
220        italic: bool,
221    ) -> Vec<FontFaceId> {
222        let arc_data: SharedFontData = Arc::new(data.to_vec());
223        self.register_font_shared_as(arc_data, family, weight, italic)
224    }
225
226    /// Like [`register_font_as`](Self::register_font_as) but takes a
227    /// pre-built shared byte container, avoiding the copy.
228    pub fn register_font_shared_as(
229        &mut self,
230        data: SharedFontData,
231        family: &str,
232        weight: u16,
233        italic: bool,
234    ) -> Vec<FontFaceId> {
235        let source = Source::Binary(data.clone());
236        let fontdb_ids = self.fontdb.load_font_source(source);
237
238        let mut face_ids = Vec::new();
239        for fontdb_id in fontdb_ids {
240            // Override metadata in fontdb
241            if let Some(face_info) = self.fontdb.face(fontdb_id) {
242                let mut info = face_info.clone();
243                info.families = vec![(family.to_string(), fontdb::Language::English_UnitedStates)];
244                info.weight = Weight(weight);
245                info.style = if italic { Style::Italic } else { Style::Normal };
246                // Remove old and re-add with new metadata
247                let face_index = info.index;
248                self.fontdb.remove_face(fontdb_id);
249                let new_id = self.fontdb.push_face_info(info);
250
251                let swash_cache_key = swash::CacheKey::new();
252                let entry = FontEntry {
253                    fontdb_id: new_id,
254                    face_index,
255                    data: FontData::Resident(data.clone()),
256                    swash_cache_key,
257                    is_system: false,
258                    shaper_data: OnceLock::new(),
259                };
260
261                let face_id = FontFaceId(self.fonts.len() as u32);
262                self.fonts.push(Some(entry));
263                self.fontdb_index.insert(new_id, face_id);
264                face_ids.push(face_id);
265            }
266        }
267        face_ids
268    }
269
270    pub fn set_default_font(&mut self, face: FontFaceId, size_px: f32) {
271        self.default_font = Some(face);
272        self.default_size_px = size_px;
273    }
274
275    pub fn default_font(&self) -> Option<FontFaceId> {
276        self.default_font
277    }
278
279    pub fn default_size_px(&self) -> f32 {
280        self.default_size_px
281    }
282
283    pub fn set_generic_family(&mut self, generic: &str, family: &str) {
284        self.generic_families
285            .insert(generic.to_string(), family.to_string());
286    }
287
288    /// Resolve a family name, mapping generic names (serif, monospace, etc.)
289    /// to their configured concrete family names.
290    pub fn resolve_family_name<'a>(&'a self, family: &'a str) -> &'a str {
291        self.generic_families
292            .get(family)
293            .map(|s| s.as_str())
294            .unwrap_or(family)
295    }
296
297    /// Query fontdb for a font matching the given criteria.
298    pub fn query_font(&self, family: &str, weight: u16, italic: bool) -> Option<FontFaceId> {
299        let resolved = self.resolve_family_name(family);
300        let style = if italic { Style::Italic } else { Style::Normal };
301
302        let query = Query {
303            families: &[Family::Name(resolved)],
304            weight: Weight(weight),
305            style,
306            ..Query::default()
307        };
308
309        let fontdb_id = self.fontdb.query(&query)?;
310        self.fontdb_id_to_face_id(fontdb_id)
311    }
312
313    /// Look up the family name of a registered font.
314    pub fn font_family_name(&self, face_id: FontFaceId) -> Option<String> {
315        let entry = self.get(face_id)?;
316        let face_info = self.fontdb.face(entry.fontdb_id)?;
317        face_info.families.first().map(|(name, _)| name.clone())
318    }
319
320    /// Look up a FontEntry by FontFaceId.
321    pub fn get(&self, face_id: FontFaceId) -> Option<&FontEntry> {
322        self.fonts
323            .get(face_id.0 as usize)
324            .and_then(|opt| opt.as_ref())
325    }
326
327    /// Query for a variant (different weight/style) of an existing registered font.
328    /// Looks up the family name of `base_face` and queries fontdb for a match.
329    pub fn query_variant(
330        &self,
331        base_face: FontFaceId,
332        weight: u16,
333        italic: bool,
334    ) -> Option<FontFaceId> {
335        let entry = self.get(base_face)?;
336        let face_info = self.fontdb.face(entry.fontdb_id)?;
337        let family_name = face_info.families.first().map(|(name, _)| name.as_str())?;
338        self.query_font(family_name, weight, italic)
339    }
340
341    /// Find our FontFaceId for a fontdb ID.
342    fn fontdb_id_to_face_id(&self, fontdb_id: fontdb::ID) -> Option<FontFaceId> {
343        self.fontdb_index.get(&fontdb_id).copied()
344    }
345
346    /// Iterate all registered font entries for glyph fallback.
347    pub fn all_entries(&self) -> impl Iterator<Item = (FontFaceId, &FontEntry)> {
348        self.fonts
349            .iter()
350            .enumerate()
351            .filter_map(|(i, opt)| opt.as_ref().map(|entry| (FontFaceId(i as u32), entry)))
352    }
353}