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
29pub fn resolve_font(
30    registry: &FontRegistry,
31    font_family: Option<&str>,
32    font_weight: Option<u32>,
33    font_bold: Option<bool>,
34    font_italic: Option<bool>,
35    font_point_size: Option<u32>,
36    scale_factor: f32,
37) -> Option<ResolvedFont> {
38    let weight = resolve_weight(font_weight, font_bold);
39    let italic = font_italic.unwrap_or(false);
40    let size_px = font_point_size
41        .map(|s| s as f32)
42        .unwrap_or(registry.default_size_px());
43
44    // Try the specified family first
45    if let Some(family) = font_family
46        && let Some(face_id) = registry.query_font(family, weight, italic)
47    {
48        let entry = registry.get(face_id)?;
49        return Some(ResolvedFont {
50            font_face_id: face_id,
51            size_px,
52            face_index: entry.face_index,
53            swash_cache_key: entry.swash_cache_key,
54            scale_factor,
55            weight,
56        });
57    }
58
59    // Fall back to default font, but still try to match weight/italic
60    // by querying for a variant of the default font's family.
61    let default_id = registry.default_font()?;
62    if (weight != 400 || italic)
63        && let Some(variant_id) = registry.query_variant(default_id, weight, italic)
64    {
65        let variant_entry = registry.get(variant_id)?;
66        return Some(ResolvedFont {
67            font_face_id: variant_id,
68            size_px,
69            face_index: variant_entry.face_index,
70            swash_cache_key: variant_entry.swash_cache_key,
71            scale_factor,
72            weight,
73        });
74    }
75    let entry = registry.get(default_id)?;
76    Some(ResolvedFont {
77        font_face_id: default_id,
78        size_px,
79        face_index: entry.face_index,
80        swash_cache_key: entry.swash_cache_key,
81        scale_factor,
82        weight,
83    })
84}
85
86/// Check if a font has a glyph for the given character.
87/// Used for glyph fallback -trying other registered fonts when
88/// the primary font doesn't cover a character.
89pub fn font_has_glyph(registry: &FontRegistry, face_id: FontFaceId, ch: char) -> bool {
90    let entry = match registry.get(face_id) {
91        Some(e) => e,
92        None => return false,
93    };
94    let font_ref = match swash::FontRef::from_index(entry.bytes(), entry.face_index as usize) {
95        Some(f) => f,
96        None => return false,
97    };
98    font_ref.charmap().map(ch) != 0
99}
100
101/// Find a fallback font that has the given character.
102///
103/// Explicitly-registered faces are tried before OS-discovered ones, so a
104/// host's bundled fallback fonts win over arbitrary system fonts; system
105/// fonts (whose bytes load lazily on this scan) are the last resort.
106pub fn find_fallback_font(
107    registry: &FontRegistry,
108    ch: char,
109    exclude: FontFaceId,
110) -> Option<FontFaceId> {
111    let covers = |entry: &crate::font::registry::FontEntry| {
112        swash::FontRef::from_index(entry.bytes(), entry.face_index as usize)
113            .is_some_and(|font_ref| font_ref.charmap().map(ch) != 0)
114    };
115
116    // Pass 1: explicitly-registered faces. Pass 2: system faces.
117    for system_pass in [false, true] {
118        for (face_id, entry) in registry.all_entries() {
119            if face_id == exclude || entry.is_system != system_pass {
120                continue;
121            }
122            if covers(entry) {
123                return Some(face_id);
124            }
125        }
126    }
127    None
128}
129
130/// Convert TextFormat weight fields to a u16 weight value for fontdb.
131fn resolve_weight(font_weight: Option<u32>, font_bold: Option<bool>) -> u16 {
132    if let Some(w) = font_weight {
133        return w.min(1000) as u16;
134    }
135    if font_bold == Some(true) {
136        return 700;
137    }
138    400 // Normal weight
139}