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 enumeration (font picker) ──────────────────────────
268
269    /// Enumerate every installed font family, deduplicated and sorted.
270    ///
271    /// Cheap (fontdb metadata only — no font bytes loaded). Collapses
272    /// weight/style faces into one entry per family, with `monospaced`
273    /// true when any face of the family is monospaced. The item source for
274    /// a font picker.
275    pub fn families(&self) -> Vec<crate::font::FontFamilyInfo> {
276        self.font_registry.families()
277    }
278
279    /// Enumerate installed font family names, deduplicated and sorted — the
280    /// simple projection of [`families`](Self::families).
281    pub fn family_names(&self) -> Vec<String> {
282        self.font_registry.family_names()
283    }
284
285    /// True if any face of the named family is monospaced (fontdb metadata,
286    /// no bytes loaded). Case-insensitive on the family name.
287    pub fn family_is_monospaced(&self, family: &str) -> bool {
288        self.font_registry.family_is_monospaced(family)
289    }
290
291    /// Build a `Send` snapshot of every family's face byte-sources, to be
292    /// moved to a background thread and turned into a writing-system
293    /// coverage map (see [`WritingSystemIndexBuilder`]).
294    ///
295    /// Cheap on the calling thread; the expensive per-face OS/2 parsing runs
296    /// in [`WritingSystemIndexBuilder::build`] off-thread.
297    ///
298    /// [`WritingSystemIndexBuilder`]: crate::font::WritingSystemIndexBuilder
299    /// [`WritingSystemIndexBuilder::build`]: crate::font::WritingSystemIndexBuilder::build
300    pub fn writing_system_index_builder(&self) -> crate::font::WritingSystemIndexBuilder {
301        self.font_registry.writing_system_index_builder()
302    }
303
304    // ── Font metrics ────────────────────────────────────────────
305
306    /// Line height (in logical pixels) for the registry's default
307    /// font + size, computed as `ascent + descent + leading`.
308    ///
309    /// Useful for callers that need to size a widget against the
310    /// intrinsic line height *before* any content has been laid out
311    /// (an empty editor that wants to report a `min_lines`-tall
312    /// intrinsic size, for example). Returns `0.0` when no default
313    /// font is registered or the face cannot be opened.
314    ///
315    /// Does not apply any per-block `line_height_multiplier` —
316    /// multipliers live on `BlockFormat`, not on the font, and have
317    /// no meaning when there's no block to multiply against. Use
318    /// [`measure_line_height`](Self::measure_line_height) when you
319    /// already have a [`TextFormat`].
320    pub fn default_line_height(&self) -> f32 {
321        self.measure_line_height(&TextFormat::default())
322    }
323
324    /// Line height (in logical pixels) for an explicit
325    /// [`TextFormat`], computed as `ascent + descent + leading` of
326    /// the resolved font at the resolved size. Fields left as `None`
327    /// fall back to the registry's defaults — same resolution path
328    /// as live inline runs (see `font::resolve::resolve_font`).
329    ///
330    /// Returns `0.0` when the format cannot be resolved (no default
331    /// font registered, requested family missing and no fallback,
332    /// face fails to open).
333    pub fn measure_line_height(&self, format: &TextFormat) -> f32 {
334        let font_point_size = format.font_size.map(|s| s as u32);
335        let resolved = match resolve_font(
336            &self.font_registry,
337            format.font_family.as_deref(),
338            format.font_weight,
339            format.font_bold,
340            format.font_italic,
341            font_point_size,
342            self.scale_factor,
343            1.0, // explicit format size is already scaled by the caller
344        ) {
345            Some(r) => r,
346            None => return 0.0,
347        };
348        match font_metrics_px(&self.font_registry, &resolved) {
349            Some(m) => m.ascent + m.descent + m.leading,
350            None => 0.0,
351        }
352    }
353
354    // ── HiDPI scale factor ──────────────────────────────────────
355
356    /// Set the device pixel ratio for HiDPI rasterization.
357    ///
358    /// Layout stays in logical pixels; glyphs are shaped and
359    /// rasterized at `size_px * scale_factor` so text is crisp on
360    /// HiDPI displays. Orthogonal to [`DocumentFlow::set_zoom`],
361    /// which is a post-layout display transform.
362    ///
363    /// Changing this value invalidates the glyph cache and the
364    /// atlas (both are cleared here) and marks every
365    /// [`DocumentFlow`] that was laid out against this service as
366    /// stale via the `scale_generation` counter. The caller must
367    /// then re-run `layout_full` / `layout_blocks` on every flow
368    /// before the next render — existing shaped advances depended
369    /// on the old ppem rounding.
370    ///
371    /// Clamped to `0.25..=8.0`. Default is `1.0`.
372    ///
373    /// [`DocumentFlow`]: crate::DocumentFlow
374    /// [`DocumentFlow::set_zoom`]: crate::DocumentFlow::set_zoom
375    pub fn set_scale_factor(&mut self, scale_factor: f32) {
376        let sf = scale_factor.clamp(0.25, 8.0);
377        if (self.scale_factor - sf).abs() <= f32::EPSILON {
378            return;
379        }
380        self.scale_factor = sf;
381        // Glyph raster cells were produced at the old physical size
382        // and would be wrong at the new one. Drop the cache outright.
383        self.glyph_cache.entries.clear();
384        // The bucketed allocator still holds the rectangles for those
385        // evicted glyphs; start from a fresh allocator so the space
386        // is actually reclaimed rather than fragmented.
387        self.atlas = GlyphAtlas::new();
388        // Bump the generation so per-widget flows can detect the
389        // invalidation and re-run their layouts.
390        self.scale_generation = self.scale_generation.wrapping_add(1);
391        // Wholesale atlas reset — anything keyed on the old eviction
392        // epoch must refuse to reuse cached atlas coordinates.
393        self.eviction_epoch = self.eviction_epoch.wrapping_add(1);
394    }
395
396    /// The current scale factor (default `1.0`).
397    pub fn scale_factor(&self) -> f32 {
398        self.scale_factor
399    }
400
401    /// Monotonic counter bumped by every successful
402    /// [`set_scale_factor`](Self::set_scale_factor) call.
403    ///
404    /// `DocumentFlow` snapshots this during layout so the framework
405    /// can ask whether a flow needs to be re-laid out after a HiDPI
406    /// change without having to track the transition itself.
407    pub fn scale_generation(&self) -> u64 {
408        self.scale_generation
409    }
410
411    /// Monotonic counter bumped every time the atlas drops entries —
412    /// LRU eviction triggered by [`atlas_snapshot`](Self::atlas_snapshot),
413    /// LRU eviction at the start of every full
414    /// [`render`](crate::DocumentFlow::render) (inside
415    /// `build_render_frame`), or wholesale reset by
416    /// [`set_scale_factor`](Self::set_scale_factor).
417    ///
418    /// This is the single source of truth for "retained glyph quads may
419    /// be stale": frameworks should compare it against a last-seen value
420    /// once per frame and invalidate every retained paint cache when it
421    /// moves, regardless of which path moved it.
422    ///
423    /// Per-widget [`crate::DocumentFlow`]s stamp this value on every full
424    /// [`render`](crate::DocumentFlow::render) and refuse to reuse
425    /// their cached glyph quads on subsequent
426    /// [`render_cursor_only`](crate::DocumentFlow::render_cursor_only)
427    /// or [`render_block_only`](crate::DocumentFlow::render_block_only)
428    /// calls when the epoch has advanced — at that point any baked-in
429    /// atlas pixel coordinates in those cached quads may reference
430    /// slots now owned by unrelated glyphs.
431    pub fn eviction_epoch(&self) -> u64 {
432        self.eviction_epoch
433    }
434
435    // ── Atlas ───────────────────────────────────────────────────
436
437    /// Read the glyph atlas state without triggering a render.
438    ///
439    /// Optionally advances the cache generation and runs eviction.
440    /// Returns an [`AtlasSnapshot`] the caller can pattern-match
441    /// by field. The atlas's internal dirty flag is cleared here,
442    /// so the caller must either upload `pixels` during the
443    /// returned borrow or accept a one-frame delay.
444    ///
445    /// When `snapshot.glyphs_evicted` is true, callers that cache
446    /// glyph positions (e.g. paint caches) must invalidate —
447    /// evicted atlas slots may be reused by subsequent allocations
448    /// and old UVs would now point to the wrong glyph.
449    ///
450    /// Only advance the generation on frames where actual text
451    /// work happened; skipping eviction on idle frames prevents
452    /// aging out glyphs that are still visible but not re-measured
453    /// this tick.
454    pub fn atlas_snapshot(&mut self, advance_generation: bool) -> AtlasSnapshot<'_> {
455        let mut glyphs_evicted = false;
456        if advance_generation {
457            self.glyph_cache.advance_generation();
458            let evicted = self.glyph_cache.evict_unused();
459            glyphs_evicted = !evicted.is_empty();
460            if glyphs_evicted {
461                self.eviction_epoch = self.eviction_epoch.wrapping_add(1);
462            }
463            for glyph in evicted {
464                self.atlas.deallocate(glyph.alloc_id);
465                #[cfg(debug_assertions)]
466                self.atlas.debug_poison_rect(
467                    glyph.atlas_x,
468                    glyph.atlas_y,
469                    glyph.width,
470                    glyph.height,
471                );
472            }
473        }
474
475        let dirty = self.atlas.dirty;
476        let width = self.atlas.width;
477        let height = self.atlas.height;
478        if dirty {
479            self.atlas.dirty = false;
480        }
481        AtlasSnapshot {
482            dirty,
483            width,
484            height,
485            pixels: &self.atlas.pixels[..],
486            glyphs_evicted,
487        }
488    }
489
490    /// Mark the given glyph cache keys as used in the current
491    /// generation, preventing them from being evicted. Use this when
492    /// glyph quads are cached externally (e.g. per-widget paint
493    /// caches) and the normal `rasterize_glyph_quad` → `get()` path
494    /// is skipped.
495    pub fn touch_glyphs(&mut self, keys: &[crate::atlas::cache::GlyphCacheKey]) {
496        self.glyph_cache.touch(keys);
497    }
498
499    /// Current atlas rectangle (`[x, y, w, h]`, atlas pixel coordinates)
500    /// for a cached glyph, without refreshing its LRU timestamp.
501    ///
502    /// Returns `None` when the glyph is not (or no longer) resident.
503    /// Intended for debug-build validation of externally retained glyph
504    /// quads: a quad whose baked rect no longer matches the live rect is
505    /// sampling pixels that belong to another glyph.
506    pub fn peek_glyph_rect(&self, key: &crate::atlas::cache::GlyphCacheKey) -> Option<[u32; 4]> {
507        self.glyph_cache
508            .peek(key)
509            .map(|g| [g.atlas_x, g.atlas_y, g.width, g.height])
510    }
511
512    /// Test hook: overwrite the atlas rectangle recorded for a cached
513    /// glyph. Lets corruption-detector tests simulate a glyph whose atlas
514    /// slot moved underneath a retained quad. Returns `false` when the
515    /// key is not resident.
516    #[doc(hidden)]
517    pub fn debug_set_glyph_rect(
518        &mut self,
519        key: &crate::atlas::cache::GlyphCacheKey,
520        rect: [u32; 4],
521    ) -> bool {
522        match self.glyph_cache.entries.get_mut(key) {
523            Some(glyph) => {
524                glyph.atlas_x = rect[0];
525                glyph.atlas_y = rect[1];
526                glyph.width = rect[2];
527                glyph.height = rect[3];
528                true
529            }
530            None => false,
531        }
532    }
533
534    /// True if the atlas has pending pixel changes since the last
535    /// upload. The atlas is marked clean after every `render()` that
536    /// copies pixels into its `RenderFrame`; this accessor exposes
537    /// the flag for framework paint-cache invalidation decisions.
538    pub fn atlas_dirty(&self) -> bool {
539        self.atlas.dirty
540    }
541
542    /// Current atlas texture width in pixels.
543    pub fn atlas_width(&self) -> u32 {
544        self.atlas.width
545    }
546
547    /// Current atlas texture height in pixels.
548    pub fn atlas_height(&self) -> u32 {
549        self.atlas.height
550    }
551
552    /// Raw atlas pixel buffer (RGBA8).
553    pub fn atlas_pixels(&self) -> &[u8] {
554        &self.atlas.pixels
555    }
556
557    /// Mark the atlas clean after the caller has uploaded its
558    /// contents to the GPU. Paired with `atlas_dirty` + `atlas_pixels`
559    /// for framework adapters that upload directly from the service
560    /// instead of consuming `RenderFrame::atlas_pixels`.
561    pub fn mark_atlas_clean(&mut self) {
562        self.atlas.dirty = false;
563    }
564}
565
566impl Default for TextFontService {
567    fn default() -> Self {
568        Self::new()
569    }
570}