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 [`crate::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::font::resolve::resolve_font;
48use crate::shaping::shaper::font_metrics_px;
49use crate::types::{FontFaceId, TextFormat};
50
51/// Outcome of a call to [`TextFontService::atlas_snapshot`].
52///
53/// Bundles the four signals a framework adapter needs to upload
54/// (or skip uploading) the glyph atlas texture to the GPU:
55///
56/// - whether the atlas has pending pixel changes since the last
57///   snapshot,
58/// - its current pixel dimensions,
59/// - a borrow of the raw RGBA pixel buffer, and
60/// - whether any cached glyphs were evicted during this snapshot —
61///   a signal that callers with paint caches holding old atlas
62///   UVs must treat as an invalidation even if the atlas itself
63///   reports clean afterwards (evicted slots may be reused by
64///   future allocations, so any cached UV pointing into them is
65///   stale).
66///
67/// Exposed as a struct rather than a tuple so every caller names
68/// fields explicitly and can't swap positions silently.
69#[derive(Debug)]
70pub struct AtlasSnapshot<'a> {
71    /// True if the atlas texture has pending pixel changes
72    /// since it was last marked clean. The snapshot call clears
73    /// this flag, so the caller must either upload `pixels` now
74    /// or accept a one-frame delay.
75    pub dirty: bool,
76    /// Current atlas texture width in pixels.
77    pub width: u32,
78    /// Current atlas texture height in pixels.
79    pub height: u32,
80    /// Raw RGBA8 pixel buffer backing the atlas texture.
81    pub pixels: &'a [u8],
82    /// True if eviction freed at least one glyph slot during
83    /// this snapshot. Callers that cache glyph positions (e.g.
84    /// framework paint caches indexed by layout key) must
85    /// invalidate when this is true — evicted slots may be
86    /// reused by future allocations and old UVs would then
87    /// point to the wrong glyph.
88    pub glyphs_evicted: bool,
89}
90
91/// Shared font resources for a text-typeset session.
92///
93/// Owns the font registry, the glyph atlas, the glyph cache, and the
94/// `swash` scale context. Construct one per process (or one per window
95/// if you really need isolated atlases) and share it by `Rc<RefCell<_>>`
96/// across every [`DocumentFlow`] that renders into the same atlas.
97///
98/// [`DocumentFlow`]: crate::DocumentFlow
99pub struct TextFontService {
100    pub(crate) font_registry: FontRegistry,
101    pub(crate) atlas: GlyphAtlas,
102    pub(crate) glyph_cache: GlyphCache,
103    pub(crate) scale_context: swash::scale::ScaleContext,
104    pub(crate) scale_factor: f32,
105    /// Bumps every time [`set_scale_factor`](Self::set_scale_factor)
106    /// actually changes the value. `DocumentFlow` snapshots this on
107    /// layout and exposes a dirty check for callers.
108    pub(crate) scale_generation: u64,
109    /// Monotonic counter bumped every time the atlas loses entries
110    /// (LRU eviction or full reset on scale-factor change). Per-widget
111    /// `DocumentFlow`s stamp this on every full `render()` and refuse
112    /// to reuse their cached glyph quads (cursor-only / block-only
113    /// paint paths) when the service's current epoch differs — at
114    /// that point any baked-in atlas pixel coordinates in those
115    /// cached quads may reference slots now owned by unrelated
116    /// glyphs.
117    pub(crate) eviction_epoch: u64,
118}
119
120impl TextFontService {
121    /// Create a service whose font registry is pre-populated with the
122    /// operating system's fonts, so arbitrary documents (CJK, emoji,
123    /// scripts the host didn't bundle) still render via glyph fallback.
124    ///
125    /// Still call [`set_default_font`](Self::set_default_font) (and
126    /// usually [`register_font`](Self::register_font) for the primary
127    /// UI font) before any [`DocumentFlow`] lays out content.
128    ///
129    /// Enumerating OS fonts is a one-time startup cost; their bytes load
130    /// lazily on first use. Use
131    /// [`new_without_system_fonts`](Self::new_without_system_fonts) to
132    /// skip the scan and keep a fully host-controlled font set.
133    ///
134    /// [`DocumentFlow`]: crate::DocumentFlow
135    pub fn new() -> Self {
136        Self::with_registry(FontRegistry::new())
137    }
138
139    /// Like [`new`](Self::new) but with no OS font enumeration: only
140    /// fonts added via `register_font*` are available. Use this when the
141    /// host ships a controlled font set and wants neither the startup
142    /// scan nor implicit system fonts.
143    pub fn new_without_system_fonts() -> Self {
144        Self::with_registry(FontRegistry::new_without_system_fonts())
145    }
146
147    fn with_registry(font_registry: FontRegistry) -> Self {
148        Self {
149            font_registry,
150            atlas: GlyphAtlas::new(),
151            glyph_cache: GlyphCache::new(),
152            scale_context: swash::scale::ScaleContext::new(),
153            scale_factor: 1.0,
154            scale_generation: 0,
155            eviction_epoch: 0,
156        }
157    }
158
159    // ── Font registration ───────────────────────────────────────
160
161    /// Register a font face from raw TTF/OTF/WOFF bytes.
162    ///
163    /// Parses the font's name table to extract family, weight, and
164    /// style, then indexes it via `fontdb` for CSS-spec font matching.
165    /// Returns the first face ID — font collections (`.ttc`) may
166    /// contain multiple faces.
167    ///
168    /// # Panics
169    ///
170    /// Panics if the font data contains no parseable faces.
171    pub fn register_font(&mut self, data: &[u8]) -> FontFaceId {
172        let ids = self.font_registry.register_font(data);
173        ids.into_iter()
174            .next()
175            .expect("font data contained no faces")
176    }
177
178    /// Register a font from a pre-built shared byte container,
179    /// avoiding the copy that [`register_font`](Self::register_font)
180    /// would perform.
181    ///
182    /// Pass an `Arc<Mmap>` (or any `Arc<dyn AsRef<[u8]> + Sync + Send>`)
183    /// when the caller already holds the data in a shareable form —
184    /// useful for large system fonts (color emoji) where copying to an
185    /// owned `Vec<u8>` would double the resident memory cost.
186    ///
187    /// # Panics
188    ///
189    /// Panics if the font data contains no parseable faces.
190    pub fn register_font_shared(&mut self, data: crate::font::SharedFontData) -> FontFaceId {
191        let ids = self.font_registry.register_font_shared(data);
192        ids.into_iter()
193            .next()
194            .expect("font data contained no faces")
195    }
196
197    /// Register a font with explicit metadata, overriding the font's
198    /// name table. Use when the font's internal metadata is unreliable
199    /// or when aliasing a font to a different family name.
200    ///
201    /// # Panics
202    ///
203    /// Panics if the font data contains no parseable faces.
204    pub fn register_font_as(
205        &mut self,
206        data: &[u8],
207        family: &str,
208        weight: u16,
209        italic: bool,
210    ) -> FontFaceId {
211        let ids = self
212            .font_registry
213            .register_font_as(data, family, weight, italic);
214        ids.into_iter()
215            .next()
216            .expect("font data contained no faces")
217    }
218
219    /// Like [`register_font_as`](Self::register_font_as) but takes a
220    /// pre-built shared byte container, avoiding the copy.
221    ///
222    /// # Panics
223    ///
224    /// Panics if the font data contains no parseable faces.
225    pub fn register_font_shared_as(
226        &mut self,
227        data: crate::font::SharedFontData,
228        family: &str,
229        weight: u16,
230        italic: bool,
231    ) -> FontFaceId {
232        let ids = self
233            .font_registry
234            .register_font_shared_as(data, family, weight, italic);
235        ids.into_iter()
236            .next()
237            .expect("font data contained no faces")
238    }
239
240    /// Set which face to use as the document default, plus its base
241    /// size in logical pixels. This is the fallback font when a
242    /// fragment's `TextFormat` doesn't specify a family or when the
243    /// requested family isn't found.
244    pub fn set_default_font(&mut self, face: FontFaceId, size_px: f32) {
245        self.font_registry.set_default_font(face, size_px);
246    }
247
248    /// Map a generic family name (e.g. `"serif"`, `"monospace"`) to a
249    /// concrete registered family. When text-document emits a fragment
250    /// whose `font_family` matches a generic, the font resolver looks
251    /// it up through this table before querying fontdb.
252    pub fn set_generic_family(&mut self, generic: &str, family: &str) {
253        self.font_registry.set_generic_family(generic, family);
254    }
255
256    /// Look up the family name of a registered face by id.
257    pub fn font_family_name(&self, face_id: FontFaceId) -> Option<String> {
258        self.font_registry.font_family_name(face_id)
259    }
260
261    /// Borrow the font registry directly — needed by callers that
262    /// want to inspect or extend it beyond the helpers exposed here.
263    pub fn font_registry(&self) -> &FontRegistry {
264        &self.font_registry
265    }
266
267    // ── Font metrics ────────────────────────────────────────────
268
269    /// Line height (in logical pixels) for the registry's default
270    /// font + size, computed as `ascent + descent + leading`.
271    ///
272    /// Useful for callers that need to size a widget against the
273    /// intrinsic line height *before* any content has been laid out
274    /// (an empty editor that wants to report a `min_lines`-tall
275    /// intrinsic size, for example). Returns `0.0` when no default
276    /// font is registered or the face cannot be opened.
277    ///
278    /// Does not apply any per-block `line_height_multiplier` —
279    /// multipliers live on `BlockFormat`, not on the font, and have
280    /// no meaning when there's no block to multiply against. Use
281    /// [`measure_line_height`](Self::measure_line_height) when you
282    /// already have a [`TextFormat`].
283    pub fn default_line_height(&self) -> f32 {
284        self.measure_line_height(&TextFormat::default())
285    }
286
287    /// Line height (in logical pixels) for an explicit
288    /// [`TextFormat`], computed as `ascent + descent + leading` of
289    /// the resolved font at the resolved size. Fields left as `None`
290    /// fall back to the registry's defaults — same resolution path
291    /// as live inline runs (see `font::resolve::resolve_font`).
292    ///
293    /// Returns `0.0` when the format cannot be resolved (no default
294    /// font registered, requested family missing and no fallback,
295    /// face fails to open).
296    pub fn measure_line_height(&self, format: &TextFormat) -> f32 {
297        let font_point_size = format.font_size.map(|s| s as u32);
298        let resolved = match resolve_font(
299            &self.font_registry,
300            format.font_family.as_deref(),
301            format.font_weight,
302            format.font_bold,
303            format.font_italic,
304            font_point_size,
305            self.scale_factor,
306        ) {
307            Some(r) => r,
308            None => return 0.0,
309        };
310        match font_metrics_px(&self.font_registry, &resolved) {
311            Some(m) => m.ascent + m.descent + m.leading,
312            None => 0.0,
313        }
314    }
315
316    // ── HiDPI scale factor ──────────────────────────────────────
317
318    /// Set the device pixel ratio for HiDPI rasterization.
319    ///
320    /// Layout stays in logical pixels; glyphs are shaped and
321    /// rasterized at `size_px * scale_factor` so text is crisp on
322    /// HiDPI displays. Orthogonal to [`DocumentFlow::set_zoom`],
323    /// which is a post-layout display transform.
324    ///
325    /// Changing this value invalidates the glyph cache and the
326    /// atlas (both are cleared here) and marks every
327    /// [`DocumentFlow`] that was laid out against this service as
328    /// stale via the `scale_generation` counter. The caller must
329    /// then re-run `layout_full` / `layout_blocks` on every flow
330    /// before the next render — existing shaped advances depended
331    /// on the old ppem rounding.
332    ///
333    /// Clamped to `0.25..=8.0`. Default is `1.0`.
334    ///
335    /// [`DocumentFlow`]: crate::DocumentFlow
336    /// [`DocumentFlow::set_zoom`]: crate::DocumentFlow::set_zoom
337    pub fn set_scale_factor(&mut self, scale_factor: f32) {
338        let sf = scale_factor.clamp(0.25, 8.0);
339        if (self.scale_factor - sf).abs() <= f32::EPSILON {
340            return;
341        }
342        self.scale_factor = sf;
343        // Glyph raster cells were produced at the old physical size
344        // and would be wrong at the new one. Drop the cache outright.
345        self.glyph_cache.entries.clear();
346        // The bucketed allocator still holds the rectangles for those
347        // evicted glyphs; start from a fresh allocator so the space
348        // is actually reclaimed rather than fragmented.
349        self.atlas = GlyphAtlas::new();
350        // Bump the generation so per-widget flows can detect the
351        // invalidation and re-run their layouts.
352        self.scale_generation = self.scale_generation.wrapping_add(1);
353        // Wholesale atlas reset — anything keyed on the old eviction
354        // epoch must refuse to reuse cached atlas coordinates.
355        self.eviction_epoch = self.eviction_epoch.wrapping_add(1);
356    }
357
358    /// The current scale factor (default `1.0`).
359    pub fn scale_factor(&self) -> f32 {
360        self.scale_factor
361    }
362
363    /// Monotonic counter bumped by every successful
364    /// [`set_scale_factor`](Self::set_scale_factor) call.
365    ///
366    /// `DocumentFlow` snapshots this during layout so the framework
367    /// can ask whether a flow needs to be re-laid out after a HiDPI
368    /// change without having to track the transition itself.
369    pub fn scale_generation(&self) -> u64 {
370        self.scale_generation
371    }
372
373    /// Monotonic counter bumped every time the atlas drops entries —
374    /// LRU eviction triggered by [`atlas_snapshot`](Self::atlas_snapshot),
375    /// LRU eviction at the start of every full
376    /// [`render`](crate::DocumentFlow::render) (inside
377    /// `build_render_frame`), or wholesale reset by
378    /// [`set_scale_factor`](Self::set_scale_factor).
379    ///
380    /// This is the single source of truth for "retained glyph quads may
381    /// be stale": frameworks should compare it against a last-seen value
382    /// once per frame and invalidate every retained paint cache when it
383    /// moves, regardless of which path moved it.
384    ///
385    /// Per-widget [`crate::DocumentFlow`]s stamp this value on every full
386    /// [`render`](crate::DocumentFlow::render) and refuse to reuse
387    /// their cached glyph quads on subsequent
388    /// [`render_cursor_only`](crate::DocumentFlow::render_cursor_only)
389    /// or [`render_block_only`](crate::DocumentFlow::render_block_only)
390    /// calls when the epoch has advanced — at that point any baked-in
391    /// atlas pixel coordinates in those cached quads may reference
392    /// slots now owned by unrelated glyphs.
393    pub fn eviction_epoch(&self) -> u64 {
394        self.eviction_epoch
395    }
396
397    // ── Atlas ───────────────────────────────────────────────────
398
399    /// Read the glyph atlas state without triggering a render.
400    ///
401    /// Optionally advances the cache generation and runs eviction.
402    /// Returns an [`AtlasSnapshot`] the caller can pattern-match
403    /// by field. The atlas's internal dirty flag is cleared here,
404    /// so the caller must either upload `pixels` during the
405    /// returned borrow or accept a one-frame delay.
406    ///
407    /// When `snapshot.glyphs_evicted` is true, callers that cache
408    /// glyph positions (e.g. paint caches) must invalidate —
409    /// evicted atlas slots may be reused by subsequent allocations
410    /// and old UVs would now point to the wrong glyph.
411    ///
412    /// Only advance the generation on frames where actual text
413    /// work happened; skipping eviction on idle frames prevents
414    /// aging out glyphs that are still visible but not re-measured
415    /// this tick.
416    pub fn atlas_snapshot(&mut self, advance_generation: bool) -> AtlasSnapshot<'_> {
417        let mut glyphs_evicted = false;
418        if advance_generation {
419            self.glyph_cache.advance_generation();
420            let evicted = self.glyph_cache.evict_unused();
421            glyphs_evicted = !evicted.is_empty();
422            if glyphs_evicted {
423                self.eviction_epoch = self.eviction_epoch.wrapping_add(1);
424            }
425            for glyph in evicted {
426                self.atlas.deallocate(glyph.alloc_id);
427                #[cfg(debug_assertions)]
428                self.atlas.debug_poison_rect(
429                    glyph.atlas_x,
430                    glyph.atlas_y,
431                    glyph.width,
432                    glyph.height,
433                );
434            }
435        }
436
437        let dirty = self.atlas.dirty;
438        let width = self.atlas.width;
439        let height = self.atlas.height;
440        if dirty {
441            self.atlas.dirty = false;
442        }
443        AtlasSnapshot {
444            dirty,
445            width,
446            height,
447            pixels: &self.atlas.pixels[..],
448            glyphs_evicted,
449        }
450    }
451
452    /// Mark the given glyph cache keys as used in the current
453    /// generation, preventing them from being evicted. Use this when
454    /// glyph quads are cached externally (e.g. per-widget paint
455    /// caches) and the normal `rasterize_glyph_quad` → `get()` path
456    /// is skipped.
457    pub fn touch_glyphs(&mut self, keys: &[crate::atlas::cache::GlyphCacheKey]) {
458        self.glyph_cache.touch(keys);
459    }
460
461    /// Current atlas rectangle (`[x, y, w, h]`, atlas pixel coordinates)
462    /// for a cached glyph, without refreshing its LRU timestamp.
463    ///
464    /// Returns `None` when the glyph is not (or no longer) resident.
465    /// Intended for debug-build validation of externally retained glyph
466    /// quads: a quad whose baked rect no longer matches the live rect is
467    /// sampling pixels that belong to another glyph.
468    pub fn peek_glyph_rect(&self, key: &crate::atlas::cache::GlyphCacheKey) -> Option<[u32; 4]> {
469        self.glyph_cache
470            .peek(key)
471            .map(|g| [g.atlas_x, g.atlas_y, g.width, g.height])
472    }
473
474    /// Test hook: overwrite the atlas rectangle recorded for a cached
475    /// glyph. Lets corruption-detector tests simulate a glyph whose atlas
476    /// slot moved underneath a retained quad. Returns `false` when the
477    /// key is not resident.
478    #[doc(hidden)]
479    pub fn debug_set_glyph_rect(
480        &mut self,
481        key: &crate::atlas::cache::GlyphCacheKey,
482        rect: [u32; 4],
483    ) -> bool {
484        match self.glyph_cache.entries.get_mut(key) {
485            Some(glyph) => {
486                glyph.atlas_x = rect[0];
487                glyph.atlas_y = rect[1];
488                glyph.width = rect[2];
489                glyph.height = rect[3];
490                true
491            }
492            None => false,
493        }
494    }
495
496    /// True if the atlas has pending pixel changes since the last
497    /// upload. The atlas is marked clean after every `render()` that
498    /// copies pixels into its `RenderFrame`; this accessor exposes
499    /// the flag for framework paint-cache invalidation decisions.
500    pub fn atlas_dirty(&self) -> bool {
501        self.atlas.dirty
502    }
503
504    /// Current atlas texture width in pixels.
505    pub fn atlas_width(&self) -> u32 {
506        self.atlas.width
507    }
508
509    /// Current atlas texture height in pixels.
510    pub fn atlas_height(&self) -> u32 {
511        self.atlas.height
512    }
513
514    /// Raw atlas pixel buffer (RGBA8).
515    pub fn atlas_pixels(&self) -> &[u8] {
516        &self.atlas.pixels
517    }
518
519    /// Mark the atlas clean after the caller has uploaded its
520    /// contents to the GPU. Paired with `atlas_dirty` + `atlas_pixels`
521    /// for framework adapters that upload directly from the service
522    /// instead of consuming `RenderFrame::atlas_pixels`.
523    pub fn mark_atlas_clean(&mut self) {
524        self.atlas.dirty = false;
525    }
526}
527
528impl Default for TextFontService {
529    fn default() -> Self {
530        Self::new()
531    }
532}