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 1.0, // explicit format size is already scaled by the caller
307 ) {
308 Some(r) => r,
309 None => return 0.0,
310 };
311 match font_metrics_px(&self.font_registry, &resolved) {
312 Some(m) => m.ascent + m.descent + m.leading,
313 None => 0.0,
314 }
315 }
316
317 // ── HiDPI scale factor ──────────────────────────────────────
318
319 /// Set the device pixel ratio for HiDPI rasterization.
320 ///
321 /// Layout stays in logical pixels; glyphs are shaped and
322 /// rasterized at `size_px * scale_factor` so text is crisp on
323 /// HiDPI displays. Orthogonal to [`DocumentFlow::set_zoom`],
324 /// which is a post-layout display transform.
325 ///
326 /// Changing this value invalidates the glyph cache and the
327 /// atlas (both are cleared here) and marks every
328 /// [`DocumentFlow`] that was laid out against this service as
329 /// stale via the `scale_generation` counter. The caller must
330 /// then re-run `layout_full` / `layout_blocks` on every flow
331 /// before the next render — existing shaped advances depended
332 /// on the old ppem rounding.
333 ///
334 /// Clamped to `0.25..=8.0`. Default is `1.0`.
335 ///
336 /// [`DocumentFlow`]: crate::DocumentFlow
337 /// [`DocumentFlow::set_zoom`]: crate::DocumentFlow::set_zoom
338 pub fn set_scale_factor(&mut self, scale_factor: f32) {
339 let sf = scale_factor.clamp(0.25, 8.0);
340 if (self.scale_factor - sf).abs() <= f32::EPSILON {
341 return;
342 }
343 self.scale_factor = sf;
344 // Glyph raster cells were produced at the old physical size
345 // and would be wrong at the new one. Drop the cache outright.
346 self.glyph_cache.entries.clear();
347 // The bucketed allocator still holds the rectangles for those
348 // evicted glyphs; start from a fresh allocator so the space
349 // is actually reclaimed rather than fragmented.
350 self.atlas = GlyphAtlas::new();
351 // Bump the generation so per-widget flows can detect the
352 // invalidation and re-run their layouts.
353 self.scale_generation = self.scale_generation.wrapping_add(1);
354 // Wholesale atlas reset — anything keyed on the old eviction
355 // epoch must refuse to reuse cached atlas coordinates.
356 self.eviction_epoch = self.eviction_epoch.wrapping_add(1);
357 }
358
359 /// The current scale factor (default `1.0`).
360 pub fn scale_factor(&self) -> f32 {
361 self.scale_factor
362 }
363
364 /// Monotonic counter bumped by every successful
365 /// [`set_scale_factor`](Self::set_scale_factor) call.
366 ///
367 /// `DocumentFlow` snapshots this during layout so the framework
368 /// can ask whether a flow needs to be re-laid out after a HiDPI
369 /// change without having to track the transition itself.
370 pub fn scale_generation(&self) -> u64 {
371 self.scale_generation
372 }
373
374 /// Monotonic counter bumped every time the atlas drops entries —
375 /// LRU eviction triggered by [`atlas_snapshot`](Self::atlas_snapshot),
376 /// LRU eviction at the start of every full
377 /// [`render`](crate::DocumentFlow::render) (inside
378 /// `build_render_frame`), or wholesale reset by
379 /// [`set_scale_factor`](Self::set_scale_factor).
380 ///
381 /// This is the single source of truth for "retained glyph quads may
382 /// be stale": frameworks should compare it against a last-seen value
383 /// once per frame and invalidate every retained paint cache when it
384 /// moves, regardless of which path moved it.
385 ///
386 /// Per-widget [`crate::DocumentFlow`]s stamp this value on every full
387 /// [`render`](crate::DocumentFlow::render) and refuse to reuse
388 /// their cached glyph quads on subsequent
389 /// [`render_cursor_only`](crate::DocumentFlow::render_cursor_only)
390 /// or [`render_block_only`](crate::DocumentFlow::render_block_only)
391 /// calls when the epoch has advanced — at that point any baked-in
392 /// atlas pixel coordinates in those cached quads may reference
393 /// slots now owned by unrelated glyphs.
394 pub fn eviction_epoch(&self) -> u64 {
395 self.eviction_epoch
396 }
397
398 // ── Atlas ───────────────────────────────────────────────────
399
400 /// Read the glyph atlas state without triggering a render.
401 ///
402 /// Optionally advances the cache generation and runs eviction.
403 /// Returns an [`AtlasSnapshot`] the caller can pattern-match
404 /// by field. The atlas's internal dirty flag is cleared here,
405 /// so the caller must either upload `pixels` during the
406 /// returned borrow or accept a one-frame delay.
407 ///
408 /// When `snapshot.glyphs_evicted` is true, callers that cache
409 /// glyph positions (e.g. paint caches) must invalidate —
410 /// evicted atlas slots may be reused by subsequent allocations
411 /// and old UVs would now point to the wrong glyph.
412 ///
413 /// Only advance the generation on frames where actual text
414 /// work happened; skipping eviction on idle frames prevents
415 /// aging out glyphs that are still visible but not re-measured
416 /// this tick.
417 pub fn atlas_snapshot(&mut self, advance_generation: bool) -> AtlasSnapshot<'_> {
418 let mut glyphs_evicted = false;
419 if advance_generation {
420 self.glyph_cache.advance_generation();
421 let evicted = self.glyph_cache.evict_unused();
422 glyphs_evicted = !evicted.is_empty();
423 if glyphs_evicted {
424 self.eviction_epoch = self.eviction_epoch.wrapping_add(1);
425 }
426 for glyph in evicted {
427 self.atlas.deallocate(glyph.alloc_id);
428 #[cfg(debug_assertions)]
429 self.atlas.debug_poison_rect(
430 glyph.atlas_x,
431 glyph.atlas_y,
432 glyph.width,
433 glyph.height,
434 );
435 }
436 }
437
438 let dirty = self.atlas.dirty;
439 let width = self.atlas.width;
440 let height = self.atlas.height;
441 if dirty {
442 self.atlas.dirty = false;
443 }
444 AtlasSnapshot {
445 dirty,
446 width,
447 height,
448 pixels: &self.atlas.pixels[..],
449 glyphs_evicted,
450 }
451 }
452
453 /// Mark the given glyph cache keys as used in the current
454 /// generation, preventing them from being evicted. Use this when
455 /// glyph quads are cached externally (e.g. per-widget paint
456 /// caches) and the normal `rasterize_glyph_quad` → `get()` path
457 /// is skipped.
458 pub fn touch_glyphs(&mut self, keys: &[crate::atlas::cache::GlyphCacheKey]) {
459 self.glyph_cache.touch(keys);
460 }
461
462 /// Current atlas rectangle (`[x, y, w, h]`, atlas pixel coordinates)
463 /// for a cached glyph, without refreshing its LRU timestamp.
464 ///
465 /// Returns `None` when the glyph is not (or no longer) resident.
466 /// Intended for debug-build validation of externally retained glyph
467 /// quads: a quad whose baked rect no longer matches the live rect is
468 /// sampling pixels that belong to another glyph.
469 pub fn peek_glyph_rect(&self, key: &crate::atlas::cache::GlyphCacheKey) -> Option<[u32; 4]> {
470 self.glyph_cache
471 .peek(key)
472 .map(|g| [g.atlas_x, g.atlas_y, g.width, g.height])
473 }
474
475 /// Test hook: overwrite the atlas rectangle recorded for a cached
476 /// glyph. Lets corruption-detector tests simulate a glyph whose atlas
477 /// slot moved underneath a retained quad. Returns `false` when the
478 /// key is not resident.
479 #[doc(hidden)]
480 pub fn debug_set_glyph_rect(
481 &mut self,
482 key: &crate::atlas::cache::GlyphCacheKey,
483 rect: [u32; 4],
484 ) -> bool {
485 match self.glyph_cache.entries.get_mut(key) {
486 Some(glyph) => {
487 glyph.atlas_x = rect[0];
488 glyph.atlas_y = rect[1];
489 glyph.width = rect[2];
490 glyph.height = rect[3];
491 true
492 }
493 None => false,
494 }
495 }
496
497 /// True if the atlas has pending pixel changes since the last
498 /// upload. The atlas is marked clean after every `render()` that
499 /// copies pixels into its `RenderFrame`; this accessor exposes
500 /// the flag for framework paint-cache invalidation decisions.
501 pub fn atlas_dirty(&self) -> bool {
502 self.atlas.dirty
503 }
504
505 /// Current atlas texture width in pixels.
506 pub fn atlas_width(&self) -> u32 {
507 self.atlas.width
508 }
509
510 /// Current atlas texture height in pixels.
511 pub fn atlas_height(&self) -> u32 {
512 self.atlas.height
513 }
514
515 /// Raw atlas pixel buffer (RGBA8).
516 pub fn atlas_pixels(&self) -> &[u8] {
517 &self.atlas.pixels
518 }
519
520 /// Mark the atlas clean after the caller has uploaded its
521 /// contents to the GPU. Paired with `atlas_dirty` + `atlas_pixels`
522 /// for framework adapters that upload directly from the service
523 /// instead of consuming `RenderFrame::atlas_pixels`.
524 pub fn mark_atlas_clean(&mut self) {
525 self.atlas.dirty = false;
526 }
527}
528
529impl Default for TextFontService {
530 fn default() -> Self {
531 Self::new()
532 }
533}