Skip to main content

fret_runtime/
font_catalog.rs

1use serde::{Deserialize, Serialize};
2
3/// Best-effort metadata for a variable font axis.
4///
5/// Floats are stored as raw `f32` bit patterns to keep the struct `Eq` and stable under
6/// serialization while remaining lossless.
7#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(default)]
9pub struct FontVariableAxisInfo {
10    pub tag: String,
11    pub min_bits: u32,
12    pub max_bits: u32,
13    pub default_bits: u32,
14}
15
16impl FontVariableAxisInfo {
17    pub fn min(&self) -> f32 {
18        f32::from_bits(self.min_bits)
19    }
20
21    pub fn max(&self) -> f32 {
22        f32::from_bits(self.max_bits)
23    }
24
25    pub fn default(&self) -> f32 {
26        f32::from_bits(self.default_bits)
27    }
28}
29
30/// Best-effort font family catalog for settings UIs.
31///
32/// This is populated by the runner from the renderer's text backend and is platform-dependent by
33/// design.
34#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(default)]
36pub struct FontCatalog {
37    pub families: Vec<String>,
38    /// Monotonic revision that increments when the effective catalog contents change.
39    ///
40    /// Refresh attempts that yield the same catalog should not bump this revision, to avoid
41    /// spurious invalidation and UI churn.
42    pub revision: u64,
43}
44
45/// Best-effort metadata for a font family entry.
46///
47/// This is populated by the runner from the renderer's text backend and is platform-dependent by
48/// design. Fields are intentionally coarse and should be treated as hints for settings pickers and
49/// diagnostics, not as hard contracts.
50#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
51#[serde(default)]
52pub struct FontCatalogEntry {
53    pub family: String,
54    /// Whether the family appears to contain at least one variable font (any axis present).
55    pub has_variable_axes: bool,
56    /// Known variable axis tags (best-effort), e.g. `wght`, `wdth`, `slnt`, `ital`, `opsz`.
57    pub known_variable_axes: Vec<String>,
58    /// Best-effort variable axis metadata for the family's default face.
59    ///
60    /// Axis tags beyond the known set may be present (e.g. `GRAD` for Roboto Flex).
61    #[serde(default)]
62    pub variable_axes: Vec<FontVariableAxisInfo>,
63    /// Best-effort monospace hint derived from font tables (typically PostScript `isFixedPitch`).
64    pub is_monospace_candidate: bool,
65}
66
67/// Best-effort catalog metadata (entries + revision).
68///
69/// The revision is expected to be monotonic and should generally match `FontCatalog.revision` when
70/// both are set by the runner.
71#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
72#[serde(default)]
73pub struct FontCatalogMetadata {
74    pub entries: Vec<FontCatalogEntry>,
75    /// Monotonic revision that increments when the effective entry list changes.
76    pub revision: u64,
77}
78
79#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
80pub enum BundledFontBaselineSource {
81    #[default]
82    None,
83    BundledProfile,
84}
85
86/// Best-effort snapshot of the framework-owned bundled font baseline at startup.
87///
88/// This is intentionally separate from the renderer-derived live font catalog:
89/// - it records which framework profile/bundle the runner chose as the bundled baseline,
90/// - it is stable across platform capability differences,
91/// - and it lets diagnostics distinguish "no bundled baseline was installed" from
92///   "the renderer catalog later gained more families via system font scan or user injection".
93#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
94#[serde(default)]
95pub struct BundledFontBaselineSnapshot {
96    pub source: BundledFontBaselineSource,
97    pub profile_name: Option<String>,
98    pub asset_bundle: Option<String>,
99    pub asset_keys: Vec<String>,
100    pub provided_roles: Vec<String>,
101    pub guaranteed_generic_families: Vec<String>,
102}
103
104impl BundledFontBaselineSnapshot {
105    pub fn none() -> Self {
106        Self::default()
107    }
108
109    pub fn bundled_profile(
110        profile_name: impl Into<String>,
111        asset_bundle: impl Into<String>,
112        asset_keys: Vec<String>,
113        provided_roles: Vec<String>,
114        guaranteed_generic_families: Vec<String>,
115    ) -> Self {
116        Self {
117            source: BundledFontBaselineSource::BundledProfile,
118            profile_name: Some(profile_name.into()),
119            asset_bundle: Some(asset_bundle.into()),
120            asset_keys,
121            provided_roles,
122            guaranteed_generic_families,
123        }
124    }
125}
126
127#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
128pub enum RendererFontSourceLane {
129    #[default]
130    BundledStartup,
131    AssetRequest,
132}
133
134#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
135#[serde(default)]
136pub struct RendererFontSourceRecord {
137    pub source_lane: RendererFontSourceLane,
138    #[serde(default, skip_serializing_if = "Option::is_none")]
139    pub asset_request: Option<fret_assets::AssetRequest>,
140    pub byte_hash: u64,
141    pub byte_len: u64,
142    pub added_face_count: u64,
143}
144
145impl RendererFontSourceRecord {
146    pub fn bundled_startup(
147        asset_request: fret_assets::AssetRequest,
148        byte_hash: u64,
149        byte_len: u64,
150        added_face_count: u64,
151    ) -> Self {
152        Self {
153            source_lane: RendererFontSourceLane::BundledStartup,
154            asset_request: Some(asset_request),
155            byte_hash,
156            byte_len,
157            added_face_count,
158        }
159    }
160
161    pub fn asset_request(
162        asset_request: fret_assets::AssetRequest,
163        byte_hash: u64,
164        byte_len: u64,
165        added_face_count: u64,
166    ) -> Self {
167        Self {
168            source_lane: RendererFontSourceLane::AssetRequest,
169            asset_request: Some(asset_request),
170            byte_hash,
171            byte_len,
172            added_face_count,
173        }
174    }
175}
176
177/// Best-effort snapshot of font sources currently known to the runner-owned renderer environment.
178///
179/// This is intentionally source-oriented rather than family-oriented:
180/// - `FontCatalogMetadata` remains the best-effort family picker surface,
181/// - `TextFontStackKey` remains the cross-surface invalidation key,
182/// - and this snapshot records where the renderer's currently approved font bytes came from.
183///
184/// `revision` is monotonic and should advance whenever the effective renderer text environment
185/// changes in a way that can affect downstream consumers such as future SVG-text bridges.
186#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
187#[serde(default)]
188pub struct RendererFontEnvironmentSnapshot {
189    pub revision: u64,
190    #[serde(default, skip_serializing_if = "Option::is_none")]
191    pub text_font_stack_key: Option<u64>,
192    #[serde(default)]
193    pub sources: Vec<RendererFontSourceRecord>,
194}
195
196impl RendererFontEnvironmentSnapshot {
197    pub fn note_text_font_stack_key(&mut self, text_font_stack_key: u64) -> bool {
198        if self.text_font_stack_key == Some(text_font_stack_key) {
199            return false;
200        }
201
202        self.text_font_stack_key = Some(text_font_stack_key);
203        self.revision = self.revision.saturating_add(1);
204        true
205    }
206
207    pub fn extend_sources_unique(
208        &mut self,
209        sources: impl IntoIterator<Item = RendererFontSourceRecord>,
210    ) -> bool {
211        let mut changed = false;
212        for source in sources {
213            if self.sources.iter().any(|existing| existing == &source) {
214                continue;
215            }
216            self.sources.push(source);
217            changed = true;
218        }
219        changed
220    }
221}
222
223/// Best-effort snapshot of the most recently observed shipped SVG text bridge diagnostics.
224///
225/// This is populated by the runner from the renderer-owned bridge path. `revision == None` means
226/// no text-bearing shipped SVG parse has been observed in the current renderer environment yet.
227#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
228#[serde(default)]
229pub struct RendererSvgTextBridgeDiagnosticsSnapshot {
230    #[serde(default, skip_serializing_if = "Option::is_none")]
231    pub revision: Option<u64>,
232    #[serde(default)]
233    pub selection_misses: Vec<RendererSvgTextFontSelectionMissRecord>,
234    #[serde(default)]
235    pub fallback_records: Vec<RendererSvgTextFontFallbackRecord>,
236    #[serde(default)]
237    pub missing_glyphs: Vec<RendererSvgTextMissingGlyphRecord>,
238}
239
240#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
241pub struct RendererSvgTextFontSelectionMissRecord {
242    pub requested_families: Vec<String>,
243    pub weight: u16,
244    pub style: String,
245    pub stretch: String,
246}
247
248#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
249pub struct RendererSvgTextFontFallbackRecord {
250    pub text: String,
251    pub from_family: String,
252    pub to_family: String,
253}
254
255#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
256pub struct RendererSvgTextMissingGlyphRecord {
257    pub text: String,
258    pub resolved_family: String,
259}
260
261/// Stable key representing the current effective text font stack / fallback configuration.
262///
263/// Runners should update this whenever the renderer text backend changes in a way that can affect
264/// shaping/metrics: font family overrides, user font loading, web font injection, etc.
265#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
266pub struct TextFontStackKey(pub u64);
267
268/// Best-effort status for the runner-owned system font rescan pipeline (native-only).
269///
270/// Desktop runners may run a one-time async system font rescan at startup to populate font
271/// catalogs. Diagnostics and perf scripts can use this state to avoid including that one-time
272/// work inside measured windows.
273#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
274pub struct SystemFontRescanState {
275    /// True while the runner is performing a background system font rescan.
276    pub in_flight: bool,
277    /// True when another rescan was requested while a rescan was already in flight.
278    pub pending: bool,
279}