Skip to main content

valo_fontmgr/
scan.rs

1//! A directory scan as the font manager, for platforms without a font API binding yet.
2//!
3//! fontdb walks the platform's font directories, so this backend knows families, styles
4//! and coverage but nothing the operating system decides: no language preference in
5//! fallback, no user-interface font, no change notification.
6
7use crate::files;
8use crate::{FontData, FontManager, Slant, Style, Typeface, Watch, NORMAL_WIDTH};
9
10/// `Scan` is an index of the fonts found under the platform's font directories.
11///
12/// Creating it walks those directories and may block, so create it once.
13pub struct Scan {
14    database: fontdb::Database,
15}
16
17impl Scan {
18    /// `load` walks the platform's font directories now.
19    pub fn load() -> Scan {
20        let mut database = fontdb::Database::new();
21        database.load_system_fonts();
22        Scan { database }
23    }
24
25    fn typeface(&mut self, id: fontdb::ID) -> Option<Typeface> {
26        let (family, style) = {
27            let face = self.database.face(id)?;
28            (face.families.first()?.0.clone(), style_of(face))
29        };
30        // SAFETY (fontdb's mmap contract): the font file must not change while mapped.
31        // Installed fonts are effectively immutable while in use, the assumption every
32        // mmap-based font stack shares; a font uninstalled mid-run degrades glyphs, it
33        // does not race memory we hand out (the map holds the old pages).
34        let (data, index) = unsafe { self.database.make_shared_face_data(id)? };
35        let data: FontData = data;
36        Some(Typeface::new(data, index, family, style))
37    }
38
39    /// Every face, nearest to `style` first, those of `family` before the rest.
40    fn candidates(&self, family: Option<&str>, style: Style) -> Vec<fontdb::ID> {
41        let mut keyed: Vec<(Rank, fontdb::ID)> = self
42            .database
43            .faces()
44            .map(|face| {
45                let in_family = family.is_some_and(|name| answers_to(face, name));
46                ((!in_family, style_of(face).distance(style)), face.id)
47            })
48            .collect();
49        keyed.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
50        keyed.into_iter().map(|(_, id)| id).collect()
51    }
52}
53
54impl FontManager for Scan {
55    fn family(&mut self, family: &str) -> Vec<Typeface> {
56        let ids: Vec<fontdb::ID> = self
57            .database
58            .faces()
59            .filter(|face| answers_to(face, family))
60            .map(|face| face.id)
61            .collect();
62        ids.into_iter().filter_map(|id| self.typeface(id)).collect()
63    }
64
65    fn match_character(
66        &mut self,
67        family: Option<&str>,
68        style: Style,
69        _locales: &[&str],
70        character: char,
71    ) -> Option<Typeface> {
72        for id in self.candidates(family, style) {
73            let Some(typeface) = self.typeface(id) else {
74                continue;
75            };
76            if files::covers(typeface.data(), typeface.index(), character) {
77                return Some(typeface);
78            }
79        }
80        None
81    }
82
83    fn system_font(&mut self, _size: f32, _style: Style) -> Option<Typeface> {
84        None
85    }
86
87    fn families(&self) -> Vec<String> {
88        let mut names: Vec<String> = self
89            .database
90            .faces()
91            .filter_map(|face| face.families.first().map(|(name, _)| name.clone()))
92            .collect();
93        names.sort();
94        names.dedup();
95        names
96    }
97
98    fn face_count(&self) -> usize {
99        self.database.len()
100    }
101
102    fn watch(&mut self, _on_change: Box<dyn Fn() + Send + Sync>) -> Option<Watch> {
103        None
104    }
105}
106
107/// A face's place in a search: outside the requested family first, then by style.
108type Rank = (bool, (u32, u8, u16));
109
110fn answers_to(face: &fontdb::FaceInfo, name: &str) -> bool {
111    face.families
112        .iter()
113        .any(|(family, _)| family.eq_ignore_ascii_case(name))
114}
115
116fn style_of(face: &fontdb::FaceInfo) -> Style {
117    Style {
118        weight: face.weight.0,
119        width: width_percent(face.stretch),
120        slant: match face.style {
121            fontdb::Style::Normal => Slant::Upright,
122            fontdb::Style::Italic => Slant::Italic,
123            fontdb::Style::Oblique => Slant::Oblique,
124        },
125    }
126}
127
128/// The CSS font-width percentage of a `usWidthClass` value.
129fn width_percent(stretch: fontdb::Stretch) -> f32 {
130    match stretch {
131        fontdb::Stretch::UltraCondensed => 50.0,
132        fontdb::Stretch::ExtraCondensed => 62.5,
133        fontdb::Stretch::Condensed => 75.0,
134        fontdb::Stretch::SemiCondensed => 87.5,
135        fontdb::Stretch::Normal => NORMAL_WIDTH,
136        fontdb::Stretch::SemiExpanded => 112.5,
137        fontdb::Stretch::Expanded => 125.0,
138        fontdb::Stretch::ExtraExpanded => 150.0,
139        fontdb::Stretch::UltraExpanded => 200.0,
140    }
141}