valo_system_fonts/lib.rs
1//! The platform's installed fonts as valo's [`FontSource`].
2//!
3//! [`SystemFonts`] adapts a [`FontManager`], the platform's font lookup in Skia's shape,
4//! to the two questions valo's font collection asks a source: the faces of a family, and
5//! a face covering a character. The manager decides how the platform is asked (CoreText,
6//! a directory scan, or a host across a boundary); this crate parses the answers into
7//! [`Font`]s and picks the instance nearest the request, so it builds anywhere
8//! `valo-text` does, wasm included.
9
10pub use valo_fontmgr::{self as fontmgr, Watch};
11use valo_fontmgr::{FontManager, Slant, Style, Typeface};
12use valo_text::{FaceSet, Font, FontAttrs, FontDemand, FontSource};
13
14/// `SystemFonts` is a [`FontSource`] over the platform's font manager.
15///
16/// Returned [`Font`] values share their file's bytes. For automatic resolution during
17/// paragraph building, add it to a [`valo_text::FontCollection`]; [`satisfy`] is for a
18/// host that resolves demands out of band.
19///
20/// [`satisfy`]: SystemFonts::satisfy
21pub struct SystemFonts {
22 manager: Box<dyn FontManager>,
23 /// The languages fallback prefers, BCP 47, most preferred first.
24 locales: Vec<String>,
25}
26
27impl SystemFonts {
28 /// `load` uses this platform's own font manager. It may block while the platform
29 /// indexes its fonts, so load once and keep it.
30 #[cfg(not(target_arch = "wasm32"))]
31 pub fn load() -> Self {
32 Self::with_manager(valo_fontmgr::platform())
33 }
34
35 /// `with_manager` answers through `manager`, for a host that supplies its own.
36 pub fn with_manager(manager: Box<dyn FontManager>) -> Self {
37 SystemFonts {
38 manager,
39 locales: Vec::new(),
40 }
41 }
42
43 /// `set_locales` tells character fallback which languages to prefer (BCP 47, most
44 /// preferred first); none prefers whatever the platform does.
45 pub fn set_locales(&mut self, locales: Vec<String>) {
46 self.locales = locales;
47 }
48
49 /// `manager` is the platform lookup underneath, for questions valo does not ask.
50 pub fn manager(&mut self) -> &mut dyn FontManager {
51 &mut *self.manager
52 }
53
54 /// `face_count` is how many installed faces the platform reports.
55 pub fn face_count(&self) -> usize {
56 self.manager.face_count()
57 }
58
59 /// `system_family` is every weight of the platform's user-interface font for text at
60 /// `size` points, one [`Font`] per weight the platform has; empty where the platform
61 /// has no such notion. Registered under a name of the caller's, it stands in for
62 /// "the system font".
63 pub fn system_family(&mut self, size: f32) -> Vec<Font> {
64 let mut fonts: Vec<Font> = Vec::new();
65 for weight in (100..=900).step_by(100) {
66 let attrs = FontAttrs {
67 weight,
68 ..FontAttrs::default()
69 };
70 let Some(typeface) = self.manager.system_font(size, style_of(attrs)) else {
71 continue;
72 };
73 let Some(font) = nearest_instance(fonts_of(typeface), attrs) else {
74 continue;
75 };
76 if !fonts.iter().any(|known| known.attrs() == font.attrs()) {
77 fonts.push(font);
78 }
79 }
80 fonts
81 }
82
83 /// `satisfy` returns a face-set clone extended to answer a font demand, or `None`
84 /// when no installed font answers any of it.
85 pub fn satisfy(&mut self, faces: &FaceSet, demand: &FontDemand) -> Option<FaceSet> {
86 faces.grown_by(self, demand)
87 }
88}
89
90impl FontSource for SystemFonts {
91 fn family(&mut self, name: &str) -> Vec<Font> {
92 self.manager
93 .family(name)
94 .into_iter()
95 .flat_map(fonts_of)
96 .collect()
97 }
98
99 fn face_for_codepoint(&mut self, codepoint: char, attrs: FontAttrs) -> Option<Font> {
100 let locales: Vec<&str> = self.locales.iter().map(String::as_str).collect();
101 let typeface = self
102 .manager
103 .match_character(None, style_of(attrs), &locales, codepoint)?;
104 nearest_instance(fonts_of(typeface), attrs).filter(|font| font.covers(codepoint))
105 }
106}
107
108/// Every registrable instance of a typeface: one for a static face, one per named
109/// instance of a variable one.
110fn fonts_of(typeface: Typeface) -> Vec<Font> {
111 Font::instances_from_data(typeface.shared_data(), typeface.index())
112}
113
114/// A variable face answers with its instance nearest the request (a bold span's
115/// fallback arrives bold); static fonts pass through unchanged.
116fn nearest_instance(instances: Vec<Font>, attrs: FontAttrs) -> Option<Font> {
117 instances.into_iter().min_by_key(|face| {
118 (
119 face.attrs().italic != attrs.italic,
120 face.attrs().weight.abs_diff(attrs.weight),
121 )
122 })
123}
124
125fn style_of(attrs: FontAttrs) -> Style {
126 Style {
127 weight: attrs.weight,
128 width: attrs.stretch,
129 slant: if attrs.italic {
130 Slant::Italic
131 } else {
132 Slant::Upright
133 },
134 }
135}