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::font::writing_system_index::{
10    FaceBytes, FaceRef, WritingSystemIndexBuilder, build_from_faces,
11};
12use crate::types::FontFaceId;
13
14/// Summary of one installed font family, for building a font picker.
15///
16/// `name` is the primary (English, where available) family name; faces of
17/// different weights/styles are collapsed into one entry. `monospaced` is
18/// true when any face of the family is monospaced — read straight from
19/// fontdb metadata, so producing a whole [`Vec<FontFamilyInfo>`] loads no
20/// font bytes.
21#[derive(Clone, Debug, PartialEq, Eq)]
22pub struct FontFamilyInfo {
23    /// Primary family name (e.g. `"DejaVu Sans"`).
24    pub name: String,
25    /// True if any face of the family is monospaced.
26    pub monospaced: bool,
27}
28
29/// Byte container owned by a [`FontEntry`]. Erased behind a trait
30/// object so the registry can hold any of `Vec<u8>`, `memmap2::Mmap`,
31/// or `&'static [u8]` (via a wrapper) without copying. fontdb's
32/// `Source::Binary` uses the same shape, so the same `Arc` can be
33/// shared with the database.
34pub type SharedFontData = Arc<dyn AsRef<[u8]> + Sync + Send>;
35
36/// Backing bytes for a [`FontEntry`].
37///
38/// Explicitly registered fonts are [`Resident`](FontData::Resident) — the
39/// caller handed us the bytes. System fonts discovered by
40/// [`load_system_fonts`](FontRegistry::new) are [`Lazy`](FontData::Lazy):
41/// we keep only the file path until the face is actually shaped or queried
42/// for coverage, then read and cache it. This keeps `new()` cheap even
43/// when the OS has hundreds of installed faces.
44enum FontData {
45    Resident(SharedFontData),
46    Lazy {
47        path: PathBuf,
48        cell: OnceLock<SharedFontData>,
49    },
50}
51
52pub struct FontEntry {
53    pub fontdb_id: fontdb::ID,
54    pub face_index: u32,
55    data: FontData,
56    pub swash_cache_key: swash::CacheKey,
57    /// True for faces discovered via the OS font enumeration. Glyph
58    /// fallback prefers explicitly-registered faces and only reaches for
59    /// system faces as a last resort (see
60    /// [`find_fallback_font`](crate::font::resolve::find_fallback_font)).
61    pub is_system: bool,
62    /// Lazily-built HarfBuzz shaping tables for this face. `ShaperData`
63    /// preprocesses the font's GSUB/GPOS/cmap tables and is independent
64    /// of any `FontRef` lifetime, so it can be built once and reused
65    /// across every shape call. Built on first shape via
66    /// [`shaper_data`](Self::shaper_data). `OnceLock` keeps `FontEntry`
67    /// `Sync` (its internal caches are atomic).
68    shaper_data: OnceLock<ShaperData>,
69}
70
71impl FontEntry {
72    /// Borrow the underlying font bytes, loading them from disk on first
73    /// use for lazily-tracked system faces. Returns an empty slice if a
74    /// system font file can no longer be read (uninstalled mid-session);
75    /// shaping then degrades gracefully to `None`/`.notdef`.
76    pub fn bytes(&self) -> &[u8] {
77        match &self.data {
78            // Two `as_ref()` hops: Arc<dyn AsRef<[u8]>> → dyn → &[u8].
79            FontData::Resident(data) => (**data).as_ref(),
80            FontData::Lazy { path, cell } => {
81                let arc = cell.get_or_init(|| {
82                    let bytes = std::fs::read(path).unwrap_or_default();
83                    Arc::new(bytes) as SharedFontData
84                });
85                (**arc).as_ref()
86            }
87        }
88    }
89
90    /// Borrow this face's shaping tables, building them on first use.
91    ///
92    /// `ShaperData::new` reprocesses the font tables; doing it once per
93    /// face (instead of once per shape call) avoids redundant work on
94    /// every relayout/keystroke. The `font` must refer to this same
95    /// face — callers already hold a `FontRef` opened from `bytes()`.
96    pub fn shaper_data(&self, font: &FontRef) -> &ShaperData {
97        self.shaper_data.get_or_init(|| ShaperData::new(font))
98    }
99}
100
101pub struct FontRegistry {
102    fontdb: Database,
103    fonts: Vec<Option<FontEntry>>,
104    /// fontdb face ID → our `FontFaceId`, so `query_font` doesn't scan
105    /// `fonts` linearly (it can hold hundreds of system faces).
106    fontdb_index: HashMap<fontdb::ID, FontFaceId>,
107    generic_families: HashMap<String, String>,
108    default_font: Option<FontFaceId>,
109    default_size_px: f32,
110}
111
112impl Default for FontRegistry {
113    fn default() -> Self {
114        Self::new()
115    }
116}
117
118impl FontRegistry {
119    /// Create a registry pre-populated with the operating system's fonts.
120    ///
121    /// OS faces participate in family lookup and glyph fallback so that
122    /// arbitrary documents (CJK, emoji, scripts the host didn't bundle)
123    /// still render. Their bytes load lazily on first use, but the
124    /// enumeration itself scans the OS font directories — a one-time
125    /// startup cost. Use [`new_without_system_fonts`](Self::new_without_system_fonts)
126    /// when the host ships a controlled font set and wants neither the
127    /// scan nor implicit OS fonts.
128    pub fn new() -> Self {
129        let mut registry = Self::new_without_system_fonts();
130        registry.load_system_fonts();
131        registry
132    }
133
134    /// Create an empty registry with no system fonts. Fonts enter only
135    /// via the `register_font*` methods.
136    pub fn new_without_system_fonts() -> Self {
137        Self {
138            fontdb: Database::new(),
139            fonts: Vec::new(),
140            fontdb_index: HashMap::new(),
141            generic_families: HashMap::new(),
142            default_font: None,
143            default_size_px: 16.0,
144        }
145    }
146
147    /// Enumerate OS fonts into `fontdb` and register a lazy [`FontEntry`]
148    /// for each so they join family lookup and the fallback chain. Bytes
149    /// load on first access; only the file path is kept until then.
150    fn load_system_fonts(&mut self) {
151        self.fontdb.load_system_fonts();
152
153        // Collect first: `faces()` borrows `self.fontdb` immutably while
154        // the loop needs `&mut self` to push entries.
155        let discovered: Vec<(fontdb::ID, u32, PathBuf)> = self
156            .fontdb
157            .faces()
158            .filter_map(|f| match &f.source {
159                Source::File(path) => Some((f.id, f.index, path.clone())),
160                Source::SharedFile(path, _) => Some((f.id, f.index, path.clone())),
161                Source::Binary(_) => None,
162            })
163            .collect();
164
165        for (fontdb_id, face_index, path) in discovered {
166            if self.fontdb_index.contains_key(&fontdb_id) {
167                continue;
168            }
169            let entry = FontEntry {
170                fontdb_id,
171                face_index,
172                data: FontData::Lazy {
173                    path,
174                    cell: OnceLock::new(),
175                },
176                swash_cache_key: swash::CacheKey::new(),
177                is_system: true,
178                shaper_data: OnceLock::new(),
179            };
180            let face_id = FontFaceId(self.fonts.len() as u32);
181            self.fonts.push(Some(entry));
182            self.fontdb_index.insert(fontdb_id, face_id);
183        }
184    }
185
186    /// Register a font from raw bytes. Returns IDs for all faces found
187    /// (font collections may contain multiple faces).
188    ///
189    /// This copies the bytes into an owned `Vec<u8>`. For large fonts
190    /// where the caller already holds the data in a shareable
191    /// container (e.g. an `Arc<Mmap>` for a system emoji font), use
192    /// [`register_font_shared`](Self::register_font_shared) instead to
193    /// avoid the copy.
194    pub fn register_font(&mut self, data: &[u8]) -> Vec<FontFaceId> {
195        let arc_data: SharedFontData = Arc::new(data.to_vec());
196        self.register_font_shared(arc_data)
197    }
198
199    /// Register a font from a pre-built shared byte container,
200    /// avoiding the copy that [`register_font`](Self::register_font)
201    /// would perform.
202    ///
203    /// The shared container is held by every resulting [`FontEntry`]
204    /// and by fontdb's `Source::Binary`. Drop it after the registry
205    /// drops the entries.
206    pub fn register_font_shared(&mut self, data: SharedFontData) -> Vec<FontFaceId> {
207        let source = Source::Binary(data.clone());
208        let fontdb_ids = self.fontdb.load_font_source(source);
209
210        let mut face_ids = Vec::new();
211        for fontdb_id in fontdb_ids {
212            let face_index = self.fontdb.face(fontdb_id).map(|f| f.index).unwrap_or(0);
213
214            let swash_cache_key = swash::CacheKey::new();
215            let entry = FontEntry {
216                fontdb_id,
217                face_index,
218                data: FontData::Resident(data.clone()),
219                swash_cache_key,
220                is_system: false,
221                shaper_data: OnceLock::new(),
222            };
223
224            let face_id = FontFaceId(self.fonts.len() as u32);
225            self.fonts.push(Some(entry));
226            self.fontdb_index.insert(fontdb_id, face_id);
227            face_ids.push(face_id);
228        }
229        face_ids
230    }
231
232    /// Register a font with explicit metadata, overriding the font's name table.
233    pub fn register_font_as(
234        &mut self,
235        data: &[u8],
236        family: &str,
237        weight: u16,
238        italic: bool,
239    ) -> Vec<FontFaceId> {
240        let arc_data: SharedFontData = Arc::new(data.to_vec());
241        self.register_font_shared_as(arc_data, family, weight, italic)
242    }
243
244    /// Like [`register_font_as`](Self::register_font_as) but takes a
245    /// pre-built shared byte container, avoiding the copy.
246    pub fn register_font_shared_as(
247        &mut self,
248        data: SharedFontData,
249        family: &str,
250        weight: u16,
251        italic: bool,
252    ) -> Vec<FontFaceId> {
253        let source = Source::Binary(data.clone());
254        let fontdb_ids = self.fontdb.load_font_source(source);
255
256        let mut face_ids = Vec::new();
257        for fontdb_id in fontdb_ids {
258            // Override metadata in fontdb
259            if let Some(face_info) = self.fontdb.face(fontdb_id) {
260                let mut info = face_info.clone();
261                info.families = vec![(family.to_string(), fontdb::Language::English_UnitedStates)];
262                info.weight = Weight(weight);
263                info.style = if italic { Style::Italic } else { Style::Normal };
264                // Remove old and re-add with new metadata
265                let face_index = info.index;
266                self.fontdb.remove_face(fontdb_id);
267                let new_id = self.fontdb.push_face_info(info);
268
269                let swash_cache_key = swash::CacheKey::new();
270                let entry = FontEntry {
271                    fontdb_id: new_id,
272                    face_index,
273                    data: FontData::Resident(data.clone()),
274                    swash_cache_key,
275                    is_system: false,
276                    shaper_data: OnceLock::new(),
277                };
278
279                let face_id = FontFaceId(self.fonts.len() as u32);
280                self.fonts.push(Some(entry));
281                self.fontdb_index.insert(new_id, face_id);
282                face_ids.push(face_id);
283            }
284        }
285        face_ids
286    }
287
288    pub fn set_default_font(&mut self, face: FontFaceId, size_px: f32) {
289        self.default_font = Some(face);
290        self.default_size_px = size_px;
291    }
292
293    pub fn default_font(&self) -> Option<FontFaceId> {
294        self.default_font
295    }
296
297    pub fn default_size_px(&self) -> f32 {
298        self.default_size_px
299    }
300
301    pub fn set_generic_family(&mut self, generic: &str, family: &str) {
302        self.generic_families
303            .insert(generic.to_string(), family.to_string());
304    }
305
306    /// Resolve a family name, mapping generic names (serif, monospace, etc.)
307    /// to their configured concrete family names.
308    pub fn resolve_family_name<'a>(&'a self, family: &'a str) -> &'a str {
309        self.generic_families
310            .get(family)
311            .map(|s| s.as_str())
312            .unwrap_or(family)
313    }
314
315    /// Query fontdb for a font matching the given criteria.
316    pub fn query_font(&self, family: &str, weight: u16, italic: bool) -> Option<FontFaceId> {
317        let resolved = self.resolve_family_name(family);
318        let style = if italic { Style::Italic } else { Style::Normal };
319
320        let query = Query {
321            families: &[Family::Name(resolved)],
322            weight: Weight(weight),
323            style,
324            ..Query::default()
325        };
326
327        let fontdb_id = self.fontdb.query(&query)?;
328        self.fontdb_id_to_face_id(fontdb_id)
329    }
330
331    /// Look up the family name of a registered font.
332    pub fn font_family_name(&self, face_id: FontFaceId) -> Option<String> {
333        let entry = self.get(face_id)?;
334        let face_info = self.fontdb.face(entry.fontdb_id)?;
335        face_info.families.first().map(|(name, _)| name.clone())
336    }
337
338    /// Look up a FontEntry by FontFaceId.
339    pub fn get(&self, face_id: FontFaceId) -> Option<&FontEntry> {
340        self.fonts
341            .get(face_id.0 as usize)
342            .and_then(|opt| opt.as_ref())
343    }
344
345    /// Query for a variant (different weight/style) of an existing registered font.
346    /// Looks up the family name of `base_face` and queries fontdb for a match.
347    pub fn query_variant(
348        &self,
349        base_face: FontFaceId,
350        weight: u16,
351        italic: bool,
352    ) -> Option<FontFaceId> {
353        let entry = self.get(base_face)?;
354        let face_info = self.fontdb.face(entry.fontdb_id)?;
355        let family_name = face_info.families.first().map(|(name, _)| name.as_str())?;
356        self.query_font(family_name, weight, italic)
357    }
358
359    /// Find our FontFaceId for a fontdb ID.
360    fn fontdb_id_to_face_id(&self, fontdb_id: fontdb::ID) -> Option<FontFaceId> {
361        self.fontdb_index.get(&fontdb_id).copied()
362    }
363
364    /// Enumerate every installed font family, deduplicated (case-insensitive)
365    /// and sorted (case-insensitive Unicode).
366    ///
367    /// Cheap: it reads only fontdb metadata (family name + monospaced flag),
368    /// so no font file is opened. This is the item source for a font picker.
369    /// Multiple faces of one family (Regular/Bold/Italic) collapse into a
370    /// single entry, whose `monospaced` is true if any face is monospaced.
371    pub fn families(&self) -> Vec<FontFamilyInfo> {
372        // key = lowercased name (dedupe); value = (display name, monospaced).
373        let mut map: HashMap<String, (String, bool)> = HashMap::new();
374        for face in self.fontdb.faces() {
375            // `families[0]` is the English (US) name where present.
376            let Some((name, _)) = face.families.first() else {
377                continue;
378            };
379            let entry = map
380                .entry(name.to_lowercase())
381                .or_insert_with(|| (name.clone(), false));
382            entry.1 |= face.monospaced;
383        }
384        let mut out: Vec<FontFamilyInfo> = map
385            .into_values()
386            .map(|(name, monospaced)| FontFamilyInfo { name, monospaced })
387            .collect();
388        out.sort_by(|a, b| {
389            a.name
390                .to_lowercase()
391                .cmp(&b.name.to_lowercase())
392                .then_with(|| a.name.cmp(&b.name))
393        });
394        out
395    }
396
397    /// Enumerate installed font family names, deduplicated and sorted — the
398    /// simple projection of [`families`](Self::families).
399    pub fn family_names(&self) -> Vec<String> {
400        self.families().into_iter().map(|f| f.name).collect()
401    }
402
403    /// True if any face of the named family is monospaced. Reads only fontdb
404    /// metadata (no bytes loaded). Case-insensitive on the family name.
405    pub fn family_is_monospaced(&self, family: &str) -> bool {
406        let target = family.to_lowercase();
407        self.fontdb.faces().any(|f| {
408            f.monospaced
409                && f.families
410                    .first()
411                    .is_some_and(|(n, _)| n.to_lowercase() == target)
412        })
413    }
414
415    /// Build a `Send` snapshot of every family's face byte-sources, ready to
416    /// be moved to a background thread and turned into a writing-system
417    /// coverage map (see [`WritingSystemIndexBuilder`]).
418    ///
419    /// Cheap on the calling thread: it clones file paths / shares `Arc`s but
420    /// reads no font bytes. The expensive OS/2 parsing happens in
421    /// [`WritingSystemIndexBuilder::build`], which the caller runs
422    /// off-thread.
423    /// The coverage map is keyed by the **lowercased** family name so it
424    /// aligns with [`families`](Self::families)' case-insensitive dedup —
425    /// callers look up by `name.to_lowercase()`. (Faces of one family can
426    /// disagree on name casing; a case-sensitive key would split or miss.)
427    pub fn writing_system_index_builder(&self) -> WritingSystemIndexBuilder {
428        build_from_faces(self.fontdb.faces().filter_map(|face| {
429            let (family, _) = face.families.first()?;
430            let bytes = match &face.source {
431                Source::File(path) => FaceBytes::Path(path.clone()),
432                Source::SharedFile(_, data) => FaceBytes::Shared(data.clone()),
433                Source::Binary(data) => FaceBytes::Shared(data.clone()),
434            };
435            Some((
436                family.to_lowercase(),
437                FaceRef {
438                    bytes,
439                    index: face.index,
440                },
441            ))
442        }))
443    }
444
445    /// Iterate all registered font entries for glyph fallback.
446    pub fn all_entries(&self) -> impl Iterator<Item = (FontFaceId, &FontEntry)> {
447        self.fonts
448            .iter()
449            .enumerate()
450            .filter_map(|(i, opt)| opt.as_ref().map(|entry| (FontFaceId(i as u32), entry)))
451    }
452}