Skip to main content

valo_fontmgr/
lib.rs

1//! Installed fonts, found the way Skia's `SkFontMgr` finds them.
2//!
3//! A [`FontManager`] answers the questions a text stack asks the platform: which faces a
4//! family has, which face covers a character in a language, and which face is the
5//! platform's own user-interface font. Each platform answers through its font API
6//! (CoreText on Apple systems) or, where there is no API yet, a scan of its font
7//! directories, or the configuration the platform publishes instead of an API (Android);
8//! every answer is a [`Typeface`], the font file's bytes and the face's index in them,
9//! which any font parser reads. The crate carries no text stack of its own, so a
10//! host that only forwards fonts depends on it alone; `valo-system-fonts` turns a manager
11//! into valo's `FontSource`. On wasm the trait and types build with no backend, for a
12//! program whose host answers across a boundary.
13
14use std::any::Any;
15use std::sync::Arc;
16
17#[cfg(not(target_arch = "wasm32"))]
18mod android;
19#[cfg(any(target_os = "macos", target_os = "ios"))]
20mod coretext;
21mod files;
22#[cfg(not(target_arch = "wasm32"))]
23mod scan;
24mod sfnt;
25
26#[cfg(not(target_arch = "wasm32"))]
27pub use android::Android;
28#[cfg(any(target_os = "macos", target_os = "ios"))]
29pub use coretext::CoreText;
30#[cfg(not(target_arch = "wasm32"))]
31pub use scan::Scan;
32pub use sfnt::assemble;
33pub use skrifa::raw::types::Tag;
34
35/// `FontData` is a font file's bytes, shared by every face read from them.
36pub type FontData = Arc<dyn AsRef<[u8]> + Send + Sync>;
37
38/// `Slant` is a face's posture: upright, a designed italic, or a slanted upright.
39#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
40pub enum Slant {
41    #[default]
42    Upright,
43    Italic,
44    Oblique,
45}
46
47/// `Style` is a face's position within its family, in CSS terms (Skia's `SkFontStyle`).
48///
49/// Matching compares styles by width first, then slant, then weight, the CSS order.
50#[derive(Clone, Copy, Debug, PartialEq)]
51pub struct Style {
52    /// `weight` is the CSS font weight, conventionally 100 to 900.
53    pub weight: u16,
54    /// `width` is the CSS font-width percentage; 100 is normal.
55    pub width: f32,
56    pub slant: Slant,
57}
58
59/// `NORMAL_WIDTH` is the CSS normal font-width percentage.
60pub const NORMAL_WIDTH: f32 = 100.0;
61
62impl Default for Style {
63    fn default() -> Self {
64        Style {
65            weight: 400,
66            width: NORMAL_WIDTH,
67            slant: Slant::Upright,
68        }
69    }
70}
71
72impl Style {
73    /// `distance` orders candidates for a requested style: width, then slant, then weight.
74    fn distance(self, requested: Style) -> (u32, u8, u16) {
75        let width = ((self.width - requested.width).abs() * 10.0) as u32;
76        let slant = match (requested.slant, self.slant) {
77            (a, b) if a == b => 0,
78            (Slant::Italic, Slant::Oblique) | (Slant::Oblique, Slant::Italic) => 1,
79            _ => 2,
80        };
81        (width, slant, self.weight.abs_diff(requested.weight))
82    }
83}
84
85/// `Typeface` is one face the platform matched: a font file, the face's index in it, and
86/// how the platform names and styles that face.
87///
88/// The bytes are shared, so many typefaces from one collection file carry one copy. A
89/// variable font is one typeface; its instances are the parser's to enumerate.
90#[derive(Clone)]
91pub struct Typeface {
92    data: FontData,
93    index: u32,
94    family: String,
95    style: Style,
96}
97
98impl Typeface {
99    pub fn new(data: FontData, index: u32, family: impl Into<String>, style: Style) -> Typeface {
100        Typeface {
101            data,
102            index,
103            family: family.into(),
104            style,
105        }
106    }
107
108    /// `data` is the whole font file.
109    pub fn data(&self) -> &[u8] {
110        (*self.data).as_ref()
111    }
112
113    /// `shared_data` is the file's bytes as a font parser shares them.
114    pub fn shared_data(&self) -> FontData {
115        Arc::clone(&self.data)
116    }
117
118    /// `index` is the face's position in a collection file; 0 in a single-face file.
119    pub fn index(&self) -> u32 {
120        self.index
121    }
122
123    /// `family` is the name the platform files the face under.
124    pub fn family(&self) -> &str {
125        &self.family
126    }
127
128    pub fn style(&self) -> Style {
129        self.style
130    }
131
132    /// `covers` reports whether the face has a glyph for `character`.
133    pub fn covers(&self, character: char) -> bool {
134        files::covers(self.data(), self.index, character)
135    }
136
137    /// `tables` is what a platform serves of this face: its tables in the file's order,
138    /// without the outline and color-glyph tables, which stay with the platform and are
139    /// rasterized on request. A consumer rebuilds a font from them with [`assemble`].
140    /// Empty when the file does not parse.
141    pub fn tables(&self) -> Vec<(Tag, &[u8])> {
142        sfnt::served_tables(self.data(), self.index).unwrap_or_default()
143    }
144}
145
146impl std::fmt::Debug for Typeface {
147    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148        f.debug_struct("Typeface")
149            .field("family", &self.family)
150            .field("index", &self.index)
151            .field("style", &self.style)
152            .finish_non_exhaustive()
153    }
154}
155
156/// `FontManager` is the platform's font lookup, in the shape of Skia's `SkFontMgr`.
157///
158/// Answers are typefaces the caller parses; nothing is shaped or rasterised here. A
159/// manager may read font files lazily and cache them, hence `&mut self`.
160pub trait FontManager: Send {
161    /// `family` returns every face the platform has under `family`; empty for a family
162    /// it does not know. Names compare the way the platform compares them.
163    fn family(&mut self, family: &str) -> Vec<Typeface>;
164
165    /// `match_family_style` returns the family's face nearest `style`, Skia's
166    /// `matchFamilyStyle`.
167    fn match_family_style(&mut self, family: &str, style: Style) -> Option<Typeface> {
168        nearest(self.family(family), style)
169    }
170
171    /// `match_character` returns a face that covers `character`, Skia's
172    /// `matchFamilyStyleCharacter`: preferring the languages in `locales` (BCP 47, most
173    /// preferred first), then `family`, then `style`. `None` when nothing installed
174    /// covers it.
175    fn match_character(
176        &mut self,
177        family: Option<&str>,
178        style: Style,
179        locales: &[&str],
180        character: char,
181    ) -> Option<Typeface>;
182
183    /// `system_font` returns the platform's own user-interface face for text at `size`
184    /// (in points) nearest `style`: what a program means by "the system font". `None`
185    /// where the platform has no such notion.
186    fn system_font(&mut self, size: f32, style: Style) -> Option<Typeface>;
187
188    /// `families` lists the installed family names.
189    fn families(&self) -> Vec<String>;
190
191    /// `face_count` is how many installed faces the platform reports; 0 means the
192    /// manager found nothing and every answer will be empty.
193    fn face_count(&self) -> usize;
194
195    /// `watch` calls `on_change` whenever the installed fonts change, from whichever
196    /// thread the platform reports on, for as long as the returned [`Watch`] lives.
197    /// `None` where the platform cannot say.
198    fn watch(&mut self, on_change: Box<dyn Fn() + Send + Sync>) -> Option<Watch>;
199}
200
201/// A boxed manager is a manager, as std makes a `Box<W: Write + ?Sized>` a writer: what
202/// [`platform`] answers can go wherever a [`FontManager`] is expected, behind a lock shared
203/// by several users among them.
204impl<M: FontManager + ?Sized> FontManager for Box<M> {
205    fn family(&mut self, family: &str) -> Vec<Typeface> {
206        (**self).family(family)
207    }
208
209    fn match_family_style(&mut self, family: &str, style: Style) -> Option<Typeface> {
210        (**self).match_family_style(family, style)
211    }
212
213    fn match_character(
214        &mut self,
215        family: Option<&str>,
216        style: Style,
217        locales: &[&str],
218        character: char,
219    ) -> Option<Typeface> {
220        (**self).match_character(family, style, locales, character)
221    }
222
223    fn system_font(&mut self, size: f32, style: Style) -> Option<Typeface> {
224        (**self).system_font(size, style)
225    }
226
227    fn families(&self) -> Vec<String> {
228        (**self).families()
229    }
230
231    fn face_count(&self) -> usize {
232        (**self).face_count()
233    }
234
235    fn watch(&mut self, on_change: Box<dyn Fn() + Send + Sync>) -> Option<Watch> {
236        (**self).watch(on_change)
237    }
238}
239
240/// `Watch` is a subscription to the installed fonts changing; drop it to stop.
241pub struct Watch {
242    _subscription: Box<dyn Any + Send>,
243}
244
245impl Watch {
246    pub fn new(subscription: impl Any + Send) -> Watch {
247        Watch {
248            _subscription: Box::new(subscription),
249        }
250    }
251}
252
253/// `nearest` picks the face closest to `style`: width first, then slant, then weight,
254/// the CSS order.
255pub fn nearest(faces: Vec<Typeface>, style: Style) -> Option<Typeface> {
256    faces
257        .into_iter()
258        .min_by_key(|face| face.style().distance(style))
259}
260
261/// `platform` is this platform's font manager: CoreText on macOS and iOS, the font
262/// configuration on Android, a directory scan elsewhere.
263#[cfg(not(target_arch = "wasm32"))]
264pub fn platform() -> Box<dyn FontManager> {
265    #[cfg(any(target_os = "macos", target_os = "ios"))]
266    {
267        Box::new(CoreText::new())
268    }
269    #[cfg(target_os = "android")]
270    {
271        Box::new(Android::new())
272    }
273    #[cfg(not(any(target_os = "macos", target_os = "ios", target_os = "android")))]
274    {
275        Box::new(Scan::load())
276    }
277}
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282
283    fn face(family: &str, weight: u16, width: f32, slant: Slant) -> Typeface {
284        Typeface::new(
285            Arc::new(Vec::new()),
286            0,
287            family,
288            Style {
289                weight,
290                width,
291                slant,
292            },
293        )
294    }
295
296    #[test]
297    fn nearest_prefers_width_then_slant_then_weight() {
298        let faces = vec![
299            face("a", 400, 100.0, Slant::Italic),
300            face("b", 700, 100.0, Slant::Upright),
301            face("c", 400, 75.0, Slant::Upright),
302        ];
303        let regular = Style::default();
304        assert_eq!(nearest(faces.clone(), regular).unwrap().family(), "b");
305        let italic = Style {
306            slant: Slant::Italic,
307            ..regular
308        };
309        assert_eq!(nearest(faces.clone(), italic).unwrap().family(), "a");
310        let condensed = Style {
311            width: 75.0,
312            ..regular
313        };
314        assert_eq!(nearest(faces, condensed).unwrap().family(), "c");
315    }
316
317    #[test]
318    fn oblique_stands_in_for_italic_before_upright() {
319        let faces = vec![
320            face("upright", 400, 100.0, Slant::Upright),
321            face("oblique", 400, 100.0, Slant::Oblique),
322        ];
323        let italic = Style {
324            slant: Slant::Italic,
325            ..Style::default()
326        };
327        assert_eq!(nearest(faces, italic).unwrap().family(), "oblique");
328    }
329}