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}