Skip to main content

text_typeset/font/
resolve.rs

1use crate::font::registry::FontRegistry;
2use crate::types::FontFaceId;
3
4/// A resolved font face with all parameters needed for shaping and rasterization.
5pub struct ResolvedFont {
6    pub font_face_id: FontFaceId,
7    pub size_px: f32,
8    pub face_index: u32,
9    pub swash_cache_key: swash::CacheKey,
10    /// Device pixel ratio applied during shaping and rasterization.
11    /// Shaping happens at `size_px * scale_factor` and results are
12    /// divided by `scale_factor` to produce logical-pixel metrics,
13    /// so downstream layout stays in logical space.
14    pub scale_factor: f32,
15    /// Resolved font weight (CSS-style 100–900). Used as the `wght`
16    /// variation axis when rasterizing variable fonts and as part of
17    /// the glyph cache key so different weights produce separate
18    /// rasterized bitmaps.
19    pub weight: u16,
20}
21
22/// Resolve a font from text formatting parameters.
23///
24/// The resolution order is:
25/// 1. If font_family is set, query by family name (with generic mapping)
26/// 2. Apply font_weight (or font_bold as weight 700)
27/// 3. Apply font_italic
28/// 4. Fall back to the default font if no match
29#[allow(clippy::too_many_arguments)]
30pub fn resolve_font(
31    registry: &FontRegistry,
32    font_family: Option<&str>,
33    font_weight: Option<u32>,
34    font_bold: Option<bool>,
35    font_italic: Option<bool>,
36    font_point_size: Option<u32>,
37    scale_factor: f32,
38    font_scale: f32,
39) -> Option<ResolvedFont> {
40    let weight = resolve_weight(font_weight, font_bold);
41    let italic = font_italic.unwrap_or(false);
42    // `font_scale` is the per-document logical text-magnification factor
43    // (accessibility "grow all text"), distinct from `scale_factor` (HiDPI
44    // raster density). It multiplies the logical point size so advances, line
45    // heights, and content height all grow and the text reflows correctly.
46    let size_px = font_point_size
47        .map(|s| s as f32)
48        .unwrap_or(registry.default_size_px())
49        * font_scale;
50
51    // Try the specified family first.
52    if let Some(family) = font_family {
53        if let Some(face_id) = registry.query_font(family, weight, italic) {
54            let entry = registry.get(face_id)?;
55            return Some(ResolvedFont {
56                font_face_id: face_id,
57                size_px,
58                face_index: entry.face_index,
59                swash_cache_key: entry.swash_cache_key,
60                scale_factor,
61                weight,
62            });
63        }
64        // A family was explicitly requested but is not registered. Warn once
65        // (per family, per process) so the silent fall-back to the default
66        // font is visible — a theme that sets `family = "Roboto"` without
67        // registering Roboto would otherwise just render the default with no
68        // signal. This is the only place that warns; the lower-level
69        // `query_font` is also used for glyph-fallback probing, which misses
70        // by design and must stay quiet.
71        warn_unknown_family(family);
72    }
73
74    // Fall back to default font, but still try to match weight/italic
75    // by querying for a variant of the default font's family.
76    let default_id = registry.default_font()?;
77    if (weight != 400 || italic)
78        && let Some(variant_id) = registry.query_variant(default_id, weight, italic)
79    {
80        let variant_entry = registry.get(variant_id)?;
81        return Some(ResolvedFont {
82            font_face_id: variant_id,
83            size_px,
84            face_index: variant_entry.face_index,
85            swash_cache_key: variant_entry.swash_cache_key,
86            scale_factor,
87            weight,
88        });
89    }
90    let entry = registry.get(default_id)?;
91    Some(ResolvedFont {
92        font_face_id: default_id,
93        size_px,
94        face_index: entry.face_index,
95        swash_cache_key: entry.swash_cache_key,
96        scale_factor,
97        weight,
98    })
99}
100
101/// Warn once per process per family when a requested font family is not
102/// registered, so the silent fall-back to the default font is visible.
103fn warn_unknown_family(family: &str) {
104    use std::collections::HashSet;
105    use std::sync::{LazyLock, Mutex};
106    static WARNED: LazyLock<Mutex<HashSet<String>>> = LazyLock::new(|| Mutex::new(HashSet::new()));
107    if let Ok(mut warned) = WARNED.lock()
108        && warned.insert(family.to_string())
109    {
110        eprintln!(
111            "text-typeset: font family {family:?} is not registered; falling back \
112             to the default font. Register it (e.g. via the host application's font \
113             registrar) before shaping."
114        );
115    }
116}
117
118/// Check if a font has a glyph for the given character.
119/// Used for glyph fallback -trying other registered fonts when
120/// the primary font doesn't cover a character.
121pub fn font_has_glyph(registry: &FontRegistry, face_id: FontFaceId, ch: char) -> bool {
122    let entry = match registry.get(face_id) {
123        Some(e) => e,
124        None => return false,
125    };
126    let font_ref = match swash::FontRef::from_index(entry.bytes(), entry.face_index as usize) {
127        Some(f) => f,
128        None => return false,
129    };
130    font_ref.charmap().map(ch) != 0
131}
132
133/// Find a fallback font that has the given character.
134///
135/// Explicitly-registered faces are tried before OS-discovered ones, so a
136/// host's bundled fallback fonts win over arbitrary system fonts; system
137/// fonts (whose bytes load lazily on this scan) are the last resort.
138pub fn find_fallback_font(
139    registry: &FontRegistry,
140    ch: char,
141    exclude: FontFaceId,
142) -> Option<FontFaceId> {
143    let covers = |entry: &crate::font::registry::FontEntry| {
144        swash::FontRef::from_index(entry.bytes(), entry.face_index as usize)
145            .is_some_and(|font_ref| font_ref.charmap().map(ch) != 0)
146    };
147
148    // Pass 1: explicitly-registered faces. Pass 2: system faces.
149    for system_pass in [false, true] {
150        for (face_id, entry) in registry.all_entries() {
151            if face_id == exclude || entry.is_system != system_pass {
152                continue;
153            }
154            if covers(entry) {
155                return Some(face_id);
156            }
157        }
158    }
159    None
160}
161
162/// Convert TextFormat weight fields to a u16 weight value for fontdb.
163fn resolve_weight(font_weight: Option<u32>, font_bold: Option<bool>) -> u16 {
164    if let Some(w) = font_weight {
165        return w.min(1000) as u16;
166    }
167    if font_bold == Some(true) {
168        return 700;
169    }
170    400 // Normal weight
171}