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 [`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::types::FontFaceId;
48
49/// Outcome of a call to [`TextFontService::atlas_snapshot`].
50///
51/// Bundles the four signals a framework adapter needs to upload
52/// (or skip uploading) the glyph atlas texture to the GPU:
53///
54/// - whether the atlas has pending pixel changes since the last
55/// snapshot,
56/// - its current pixel dimensions,
57/// - a borrow of the raw RGBA pixel buffer, and
58/// - whether any cached glyphs were evicted during this snapshot —
59/// a signal that callers with paint caches holding old atlas
60/// UVs must treat as an invalidation even if the atlas itself
61/// reports clean afterwards (evicted slots may be reused by
62/// future allocations, so any cached UV pointing into them is
63/// stale).
64///
65/// Exposed as a struct rather than a tuple so every caller names
66/// fields explicitly and can't swap positions silently.
67#[derive(Debug)]
68pub struct AtlasSnapshot<'a> {
69 /// True if the atlas texture has pending pixel changes
70 /// since it was last marked clean. The snapshot call clears
71 /// this flag, so the caller must either upload `pixels` now
72 /// or accept a one-frame delay.
73 pub dirty: bool,
74 /// Current atlas texture width in pixels.
75 pub width: u32,
76 /// Current atlas texture height in pixels.
77 pub height: u32,
78 /// Raw RGBA8 pixel buffer backing the atlas texture.
79 pub pixels: &'a [u8],
80 /// True if eviction freed at least one glyph slot during
81 /// this snapshot. Callers that cache glyph positions (e.g.
82 /// framework paint caches indexed by layout key) must
83 /// invalidate when this is true — evicted slots may be
84 /// reused by future allocations and old UVs would then
85 /// point to the wrong glyph.
86 pub glyphs_evicted: bool,
87}
88
89/// Shared font resources for a text-typeset session.
90///
91/// Owns the font registry, the glyph atlas, the glyph cache, and the
92/// `swash` scale context. Construct one per process (or one per window
93/// if you really need isolated atlases) and share it by `Rc<RefCell<_>>`
94/// across every [`DocumentFlow`] that renders into the same atlas.
95///
96/// [`DocumentFlow`]: crate::DocumentFlow
97pub struct TextFontService {
98 pub(crate) font_registry: FontRegistry,
99 pub(crate) atlas: GlyphAtlas,
100 pub(crate) glyph_cache: GlyphCache,
101 pub(crate) scale_context: swash::scale::ScaleContext,
102 pub(crate) scale_factor: f32,
103 /// Bumps every time [`set_scale_factor`](Self::set_scale_factor)
104 /// actually changes the value. `DocumentFlow` snapshots this on
105 /// layout and exposes a dirty check for callers.
106 pub(crate) scale_generation: u64,
107}
108
109impl TextFontService {
110 /// Create an empty service with no fonts registered.
111 ///
112 /// Call [`register_font`](Self::register_font) and
113 /// [`set_default_font`](Self::set_default_font) before any
114 /// [`DocumentFlow`] lays out content against this service.
115 ///
116 /// [`DocumentFlow`]: crate::DocumentFlow
117 pub fn new() -> Self {
118 Self {
119 font_registry: FontRegistry::new(),
120 atlas: GlyphAtlas::new(),
121 glyph_cache: GlyphCache::new(),
122 scale_context: swash::scale::ScaleContext::new(),
123 scale_factor: 1.0,
124 scale_generation: 0,
125 }
126 }
127
128 // ── Font registration ───────────────────────────────────────
129
130 /// Register a font face from raw TTF/OTF/WOFF bytes.
131 ///
132 /// Parses the font's name table to extract family, weight, and
133 /// style, then indexes it via `fontdb` for CSS-spec font matching.
134 /// Returns the first face ID — font collections (`.ttc`) may
135 /// contain multiple faces.
136 ///
137 /// # Panics
138 ///
139 /// Panics if the font data contains no parseable faces.
140 pub fn register_font(&mut self, data: &[u8]) -> FontFaceId {
141 let ids = self.font_registry.register_font(data);
142 ids.into_iter()
143 .next()
144 .expect("font data contained no faces")
145 }
146
147 /// Register a font with explicit metadata, overriding the font's
148 /// name table. Use when the font's internal metadata is unreliable
149 /// or when aliasing a font to a different family name.
150 ///
151 /// # Panics
152 ///
153 /// Panics if the font data contains no parseable faces.
154 pub fn register_font_as(
155 &mut self,
156 data: &[u8],
157 family: &str,
158 weight: u16,
159 italic: bool,
160 ) -> FontFaceId {
161 let ids = self
162 .font_registry
163 .register_font_as(data, family, weight, italic);
164 ids.into_iter()
165 .next()
166 .expect("font data contained no faces")
167 }
168
169 /// Set which face to use as the document default, plus its base
170 /// size in logical pixels. This is the fallback font when a
171 /// fragment's `TextFormat` doesn't specify a family or when the
172 /// requested family isn't found.
173 pub fn set_default_font(&mut self, face: FontFaceId, size_px: f32) {
174 self.font_registry.set_default_font(face, size_px);
175 }
176
177 /// Map a generic family name (e.g. `"serif"`, `"monospace"`) to a
178 /// concrete registered family. When text-document emits a fragment
179 /// whose `font_family` matches a generic, the font resolver looks
180 /// it up through this table before querying fontdb.
181 pub fn set_generic_family(&mut self, generic: &str, family: &str) {
182 self.font_registry.set_generic_family(generic, family);
183 }
184
185 /// Look up the family name of a registered face by id.
186 pub fn font_family_name(&self, face_id: FontFaceId) -> Option<String> {
187 self.font_registry.font_family_name(face_id)
188 }
189
190 /// Borrow the font registry directly — needed by callers that
191 /// want to inspect or extend it beyond the helpers exposed here.
192 pub fn font_registry(&self) -> &FontRegistry {
193 &self.font_registry
194 }
195
196 // ── HiDPI scale factor ──────────────────────────────────────
197
198 /// Set the device pixel ratio for HiDPI rasterization.
199 ///
200 /// Layout stays in logical pixels; glyphs are shaped and
201 /// rasterized at `size_px * scale_factor` so text is crisp on
202 /// HiDPI displays. Orthogonal to [`DocumentFlow::set_zoom`],
203 /// which is a post-layout display transform.
204 ///
205 /// Changing this value invalidates the glyph cache and the
206 /// atlas (both are cleared here) and marks every
207 /// [`DocumentFlow`] that was laid out against this service as
208 /// stale via the `scale_generation` counter. The caller must
209 /// then re-run `layout_full` / `layout_blocks` on every flow
210 /// before the next render — existing shaped advances depended
211 /// on the old ppem rounding.
212 ///
213 /// Clamped to `0.25..=8.0`. Default is `1.0`.
214 ///
215 /// [`DocumentFlow`]: crate::DocumentFlow
216 /// [`DocumentFlow::set_zoom`]: crate::DocumentFlow::set_zoom
217 pub fn set_scale_factor(&mut self, scale_factor: f32) {
218 let sf = scale_factor.clamp(0.25, 8.0);
219 if (self.scale_factor - sf).abs() <= f32::EPSILON {
220 return;
221 }
222 self.scale_factor = sf;
223 // Glyph raster cells were produced at the old physical size
224 // and would be wrong at the new one. Drop the cache outright.
225 self.glyph_cache.entries.clear();
226 // The bucketed allocator still holds the rectangles for those
227 // evicted glyphs; start from a fresh allocator so the space
228 // is actually reclaimed rather than fragmented.
229 self.atlas = GlyphAtlas::new();
230 // Bump the generation so per-widget flows can detect the
231 // invalidation and re-run their layouts.
232 self.scale_generation = self.scale_generation.wrapping_add(1);
233 }
234
235 /// The current scale factor (default `1.0`).
236 pub fn scale_factor(&self) -> f32 {
237 self.scale_factor
238 }
239
240 /// Monotonic counter bumped by every successful
241 /// [`set_scale_factor`](Self::set_scale_factor) call.
242 ///
243 /// `DocumentFlow` snapshots this during layout so the framework
244 /// can ask whether a flow needs to be re-laid out after a HiDPI
245 /// change without having to track the transition itself.
246 pub fn scale_generation(&self) -> u64 {
247 self.scale_generation
248 }
249
250 // ── Atlas ───────────────────────────────────────────────────
251
252 /// Read the glyph atlas state without triggering a render.
253 ///
254 /// Optionally advances the cache generation and runs eviction.
255 /// Returns an [`AtlasSnapshot`] the caller can pattern-match
256 /// by field. The atlas's internal dirty flag is cleared here,
257 /// so the caller must either upload `pixels` during the
258 /// returned borrow or accept a one-frame delay.
259 ///
260 /// When `snapshot.glyphs_evicted` is true, callers that cache
261 /// glyph positions (e.g. paint caches) must invalidate —
262 /// evicted atlas slots may be reused by subsequent allocations
263 /// and old UVs would now point to the wrong glyph.
264 ///
265 /// Only advance the generation on frames where actual text
266 /// work happened; skipping eviction on idle frames prevents
267 /// aging out glyphs that are still visible but not re-measured
268 /// this tick.
269 pub fn atlas_snapshot(&mut self, advance_generation: bool) -> AtlasSnapshot<'_> {
270 let mut glyphs_evicted = false;
271 if advance_generation {
272 self.glyph_cache.advance_generation();
273 let evicted = self.glyph_cache.evict_unused();
274 glyphs_evicted = !evicted.is_empty();
275 for alloc_id in evicted {
276 self.atlas.deallocate(alloc_id);
277 }
278 }
279
280 let dirty = self.atlas.dirty;
281 let width = self.atlas.width;
282 let height = self.atlas.height;
283 if dirty {
284 self.atlas.dirty = false;
285 }
286 AtlasSnapshot {
287 dirty,
288 width,
289 height,
290 pixels: &self.atlas.pixels[..],
291 glyphs_evicted,
292 }
293 }
294
295 /// Mark the given glyph cache keys as used in the current
296 /// generation, preventing them from being evicted. Use this when
297 /// glyph quads are cached externally (e.g. per-widget paint
298 /// caches) and the normal `rasterize_glyph_quad` → `get()` path
299 /// is skipped.
300 pub fn touch_glyphs(&mut self, keys: &[crate::atlas::cache::GlyphCacheKey]) {
301 self.glyph_cache.touch(keys);
302 }
303
304 /// True if the atlas has pending pixel changes since the last
305 /// upload. The atlas is marked clean after every `render()` that
306 /// copies pixels into its `RenderFrame`; this accessor exposes
307 /// the flag for framework paint-cache invalidation decisions.
308 pub fn atlas_dirty(&self) -> bool {
309 self.atlas.dirty
310 }
311
312 /// Current atlas texture width in pixels.
313 pub fn atlas_width(&self) -> u32 {
314 self.atlas.width
315 }
316
317 /// Current atlas texture height in pixels.
318 pub fn atlas_height(&self) -> u32 {
319 self.atlas.height
320 }
321
322 /// Raw atlas pixel buffer (RGBA8).
323 pub fn atlas_pixels(&self) -> &[u8] {
324 &self.atlas.pixels
325 }
326
327 /// Mark the atlas clean after the caller has uploaded its
328 /// contents to the GPU. Paired with `atlas_dirty` + `atlas_pixels`
329 /// for framework adapters that upload directly from the service
330 /// instead of consuming `RenderFrame::atlas_pixels`.
331 pub fn mark_atlas_clean(&mut self) {
332 self.atlas.dirty = false;
333 }
334}
335
336impl Default for TextFontService {
337 fn default() -> Self {
338 Self::new()
339 }
340}