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}