Skip to main content

text_typeset/
font_service.rs

1//! Shared font service: the part of text-typeset that can be shared
2//! across many widgets viewing many documents.
3//!
4//! A [`TextFontService`] owns four things:
5//!
6//! - a font registry (parsed faces, family lookup, fallback chain),
7//! - a GPU-bound glyph atlas (RGBA texture with bucketed allocations),
8//! - a glyph cache keyed on `(face, glyph_id, physical_size_px)`,
9//! - a `swash` scale context (reusable rasterizer workspace).
10//!
11//! None of these describe **what** a specific widget is showing — they
12//! describe **how** glyphs are rasterized and cached. Two widgets
13//! viewing the same or different documents in the same window should
14//! share one `TextFontService` so that:
15//!
16//! 1. every glyph for a given `(face, size)` lives in one atlas
17//!    and is uploaded to the GPU exactly once per frame;
18//! 2. every shaped glyph rasterization happens at most once,
19//!    amortized over every widget that ever renders it;
20//! 3. font registration and fallback resolution are consistent
21//!    across the whole UI.
22//!
23//! Per-widget state — viewport, zoom, scroll offset, flow layout,
24//! cursor, colors — lives on a separate [`DocumentFlow`] that borrows
25//! the service at layout and render time.
26//!
27//! [`DocumentFlow`]: crate::DocumentFlow
28//!
29//! # HiDPI invalidation
30//!
31//! [`set_scale_factor`](TextFontService::set_scale_factor) is the one
32//! mutation that invalidates existing work: cached glyphs were
33//! rasterized at the old physical ppem and are wrong at the new one,
34//! and flow layouts stored shaped advances that depended on the
35//! previous ppem rounding. The service clears its own glyph cache
36//! and atlas on the spot, but it cannot reach into per-widget
37//! [`DocumentFlow`] instances. Instead it bumps a monotonic
38//! `scale_generation` counter every time the scale factor changes.
39//! Each `DocumentFlow` remembers the generation it was last laid out
40//! at. Call [`DocumentFlow::layout_dirty_for_scale`] from the
41//! framework side to ask "does this flow need a relayout?" and re-run
42//! `layout_full` when the answer is yes.
43
44use crate::atlas::allocator::GlyphAtlas;
45use crate::atlas::cache::GlyphCache;
46use crate::font::registry::FontRegistry;
47use crate::types::FontFaceId;
48
49/// Outcome of a call to [`TextFontService::atlas_snapshot`].
50///
51/// Bundles the four signals a framework adapter needs to upload
52/// (or skip uploading) the glyph atlas texture to the GPU:
53///
54/// - whether the atlas has pending pixel changes since the last
55///   snapshot,
56/// - its current pixel dimensions,
57/// - a borrow of the raw RGBA pixel buffer, and
58/// - whether any cached glyphs were evicted during this snapshot —
59///   a signal that callers with paint caches holding old atlas
60///   UVs must treat as an invalidation even if the atlas itself
61///   reports clean afterwards (evicted slots may be reused by
62///   future allocations, so any cached UV pointing into them is
63///   stale).
64///
65/// Exposed as a struct rather than a tuple so every caller names
66/// fields explicitly and can't swap positions silently.
67#[derive(Debug)]
68pub struct AtlasSnapshot<'a> {
69    /// True if the atlas texture has pending pixel changes
70    /// since it was last marked clean. The snapshot call clears
71    /// this flag, so the caller must either upload `pixels` now
72    /// or accept a one-frame delay.
73    pub dirty: bool,
74    /// Current atlas texture width in pixels.
75    pub width: u32,
76    /// Current atlas texture height in pixels.
77    pub height: u32,
78    /// Raw RGBA8 pixel buffer backing the atlas texture.
79    pub pixels: &'a [u8],
80    /// True if eviction freed at least one glyph slot during
81    /// this snapshot. Callers that cache glyph positions (e.g.
82    /// framework paint caches indexed by layout key) must
83    /// invalidate when this is true — evicted slots may be
84    /// reused by future allocations and old UVs would then
85    /// point to the wrong glyph.
86    pub glyphs_evicted: bool,
87}
88
89/// Shared font resources for a text-typeset session.
90///
91/// Owns the font registry, the glyph atlas, the glyph cache, and the
92/// `swash` scale context. Construct one per process (or one per window
93/// if you really need isolated atlases) and share it by `Rc<RefCell<_>>`
94/// across every [`DocumentFlow`] that renders into the same atlas.
95///
96/// [`DocumentFlow`]: crate::DocumentFlow
97pub struct TextFontService {
98    pub(crate) font_registry: FontRegistry,
99    pub(crate) atlas: GlyphAtlas,
100    pub(crate) glyph_cache: GlyphCache,
101    pub(crate) scale_context: swash::scale::ScaleContext,
102    pub(crate) scale_factor: f32,
103    /// Bumps every time [`set_scale_factor`](Self::set_scale_factor)
104    /// actually changes the value. `DocumentFlow` snapshots this on
105    /// layout and exposes a dirty check for callers.
106    pub(crate) scale_generation: u64,
107}
108
109impl TextFontService {
110    /// Create an empty service with no fonts registered.
111    ///
112    /// Call [`register_font`](Self::register_font) and
113    /// [`set_default_font`](Self::set_default_font) before any
114    /// [`DocumentFlow`] lays out content against this service.
115    ///
116    /// [`DocumentFlow`]: crate::DocumentFlow
117    pub fn new() -> Self {
118        Self {
119            font_registry: FontRegistry::new(),
120            atlas: GlyphAtlas::new(),
121            glyph_cache: GlyphCache::new(),
122            scale_context: swash::scale::ScaleContext::new(),
123            scale_factor: 1.0,
124            scale_generation: 0,
125        }
126    }
127
128    // ── Font registration ───────────────────────────────────────
129
130    /// Register a font face from raw TTF/OTF/WOFF bytes.
131    ///
132    /// Parses the font's name table to extract family, weight, and
133    /// style, then indexes it via `fontdb` for CSS-spec font matching.
134    /// Returns the first face ID — font collections (`.ttc`) may
135    /// contain multiple faces.
136    ///
137    /// # Panics
138    ///
139    /// Panics if the font data contains no parseable faces.
140    pub fn register_font(&mut self, data: &[u8]) -> FontFaceId {
141        let ids = self.font_registry.register_font(data);
142        ids.into_iter()
143            .next()
144            .expect("font data contained no faces")
145    }
146
147    /// Register a font with explicit metadata, overriding the font's
148    /// name table. Use when the font's internal metadata is unreliable
149    /// or when aliasing a font to a different family name.
150    ///
151    /// # Panics
152    ///
153    /// Panics if the font data contains no parseable faces.
154    pub fn register_font_as(
155        &mut self,
156        data: &[u8],
157        family: &str,
158        weight: u16,
159        italic: bool,
160    ) -> FontFaceId {
161        let ids = self
162            .font_registry
163            .register_font_as(data, family, weight, italic);
164        ids.into_iter()
165            .next()
166            .expect("font data contained no faces")
167    }
168
169    /// Set which face to use as the document default, plus its base
170    /// size in logical pixels. This is the fallback font when a
171    /// fragment's `TextFormat` doesn't specify a family or when the
172    /// requested family isn't found.
173    pub fn set_default_font(&mut self, face: FontFaceId, size_px: f32) {
174        self.font_registry.set_default_font(face, size_px);
175    }
176
177    /// Map a generic family name (e.g. `"serif"`, `"monospace"`) to a
178    /// concrete registered family. When text-document emits a fragment
179    /// whose `font_family` matches a generic, the font resolver looks
180    /// it up through this table before querying fontdb.
181    pub fn set_generic_family(&mut self, generic: &str, family: &str) {
182        self.font_registry.set_generic_family(generic, family);
183    }
184
185    /// Look up the family name of a registered face by id.
186    pub fn font_family_name(&self, face_id: FontFaceId) -> Option<String> {
187        self.font_registry.font_family_name(face_id)
188    }
189
190    /// Borrow the font registry directly — needed by callers that
191    /// want to inspect or extend it beyond the helpers exposed here.
192    pub fn font_registry(&self) -> &FontRegistry {
193        &self.font_registry
194    }
195
196    // ── HiDPI scale factor ──────────────────────────────────────
197
198    /// Set the device pixel ratio for HiDPI rasterization.
199    ///
200    /// Layout stays in logical pixels; glyphs are shaped and
201    /// rasterized at `size_px * scale_factor` so text is crisp on
202    /// HiDPI displays. Orthogonal to [`DocumentFlow::set_zoom`],
203    /// which is a post-layout display transform.
204    ///
205    /// Changing this value invalidates the glyph cache and the
206    /// atlas (both are cleared here) and marks every
207    /// [`DocumentFlow`] that was laid out against this service as
208    /// stale via the `scale_generation` counter. The caller must
209    /// then re-run `layout_full` / `layout_blocks` on every flow
210    /// before the next render — existing shaped advances depended
211    /// on the old ppem rounding.
212    ///
213    /// Clamped to `0.25..=8.0`. Default is `1.0`.
214    ///
215    /// [`DocumentFlow`]: crate::DocumentFlow
216    /// [`DocumentFlow::set_zoom`]: crate::DocumentFlow::set_zoom
217    pub fn set_scale_factor(&mut self, scale_factor: f32) {
218        let sf = scale_factor.clamp(0.25, 8.0);
219        if (self.scale_factor - sf).abs() <= f32::EPSILON {
220            return;
221        }
222        self.scale_factor = sf;
223        // Glyph raster cells were produced at the old physical size
224        // and would be wrong at the new one. Drop the cache outright.
225        self.glyph_cache.entries.clear();
226        // The bucketed allocator still holds the rectangles for those
227        // evicted glyphs; start from a fresh allocator so the space
228        // is actually reclaimed rather than fragmented.
229        self.atlas = GlyphAtlas::new();
230        // Bump the generation so per-widget flows can detect the
231        // invalidation and re-run their layouts.
232        self.scale_generation = self.scale_generation.wrapping_add(1);
233    }
234
235    /// The current scale factor (default `1.0`).
236    pub fn scale_factor(&self) -> f32 {
237        self.scale_factor
238    }
239
240    /// Monotonic counter bumped by every successful
241    /// [`set_scale_factor`](Self::set_scale_factor) call.
242    ///
243    /// `DocumentFlow` snapshots this during layout so the framework
244    /// can ask whether a flow needs to be re-laid out after a HiDPI
245    /// change without having to track the transition itself.
246    pub fn scale_generation(&self) -> u64 {
247        self.scale_generation
248    }
249
250    // ── Atlas ───────────────────────────────────────────────────
251
252    /// Read the glyph atlas state without triggering a render.
253    ///
254    /// Optionally advances the cache generation and runs eviction.
255    /// Returns an [`AtlasSnapshot`] the caller can pattern-match
256    /// by field. The atlas's internal dirty flag is cleared here,
257    /// so the caller must either upload `pixels` during the
258    /// returned borrow or accept a one-frame delay.
259    ///
260    /// When `snapshot.glyphs_evicted` is true, callers that cache
261    /// glyph positions (e.g. paint caches) must invalidate —
262    /// evicted atlas slots may be reused by subsequent allocations
263    /// and old UVs would now point to the wrong glyph.
264    ///
265    /// Only advance the generation on frames where actual text
266    /// work happened; skipping eviction on idle frames prevents
267    /// aging out glyphs that are still visible but not re-measured
268    /// this tick.
269    pub fn atlas_snapshot(&mut self, advance_generation: bool) -> AtlasSnapshot<'_> {
270        let mut glyphs_evicted = false;
271        if advance_generation {
272            self.glyph_cache.advance_generation();
273            let evicted = self.glyph_cache.evict_unused();
274            glyphs_evicted = !evicted.is_empty();
275            for alloc_id in evicted {
276                self.atlas.deallocate(alloc_id);
277            }
278        }
279
280        let dirty = self.atlas.dirty;
281        let width = self.atlas.width;
282        let height = self.atlas.height;
283        if dirty {
284            self.atlas.dirty = false;
285        }
286        AtlasSnapshot {
287            dirty,
288            width,
289            height,
290            pixels: &self.atlas.pixels[..],
291            glyphs_evicted,
292        }
293    }
294
295    /// True if the atlas has pending pixel changes since the last
296    /// upload. The atlas is marked clean after every `render()` that
297    /// copies pixels into its `RenderFrame`; this accessor exposes
298    /// the flag for framework paint-cache invalidation decisions.
299    pub fn atlas_dirty(&self) -> bool {
300        self.atlas.dirty
301    }
302
303    /// Current atlas texture width in pixels.
304    pub fn atlas_width(&self) -> u32 {
305        self.atlas.width
306    }
307
308    /// Current atlas texture height in pixels.
309    pub fn atlas_height(&self) -> u32 {
310        self.atlas.height
311    }
312
313    /// Raw atlas pixel buffer (RGBA8).
314    pub fn atlas_pixels(&self) -> &[u8] {
315        &self.atlas.pixels
316    }
317
318    /// Mark the atlas clean after the caller has uploaded its
319    /// contents to the GPU. Paired with `atlas_dirty` + `atlas_pixels`
320    /// for framework adapters that upload directly from the service
321    /// instead of consuming `RenderFrame::atlas_pixels`.
322    pub fn mark_atlas_clean(&mut self) {
323        self.atlas.dirty = false;
324    }
325}
326
327impl Default for TextFontService {
328    fn default() -> Self {
329        Self::new()
330    }
331}