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