Skip to main content

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 with valo's [`match_style`], so
8//! it builds anywhere `valo-text` does, wasm included.
9
10pub use valo_fontmgr::{self as fontmgr, Watch};
11use valo_fontmgr::{FontManager, Slant, Style, Typeface};
12use valo_text::{match_style, 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) = match_instance(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        match_instance(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/// The instance of `typeface` that [`match_style`] picks for `attrs`: a variable face
115/// answers with its instance nearest the request (a bold span's fallback arrives bold),
116/// a static face with its one font.
117fn match_instance(typeface: Typeface, attrs: FontAttrs) -> Option<Font> {
118    let mut instances = fonts_of(typeface);
119    let nearest = match_style(&instances, attrs)?;
120    let index = instances.iter().position(|font| font == nearest)?;
121    Some(instances.swap_remove(index))
122}
123
124fn style_of(attrs: FontAttrs) -> Style {
125    Style {
126        weight: attrs.weight,
127        width: attrs.stretch,
128        slant: if attrs.italic {
129            Slant::Italic
130        } else {
131            Slant::Upright
132        },
133    }
134}