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}