rust_widgets 2.9.3

Pure Rust cross-platform native GUI library with hardware-adaptive rendering, 180 widgets, touch/gesture support, i18n, and SVG-pipeline-accurate output
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
// SPDX-FileCopyrightText: Copyright (c) 2026 Mike Li/Mikewolfli/Wei Li(mikewolfli@163.com)
// SPDX-License-Identifier: MIT

//! A glyph atlas: rasterised glyph bitmaps remembered by face, character and size.
//!
//! # What this is
//!
//! The rasteriser is deterministic: the coverage bytes for a character depend only on the face,
//! the character and the device cell. A repaint asks for the same few hundred glyphs over and
//! over, so this module remembers the answer and hands the same buffer back.
//!
//! # What this is **not**
//!
//! It is not a second rasteriser. A miss calls the rasteriser's `rasterize_face_at_weight`, which
//! is the same code path a direct paint takes, with the same face cache and the same per-cell
//! coverage cache. The atlas therefore cannot change a glyph's bytes: it only decides whether to compute
//! them again.
//!
//! # Scope
//!
//! The atlas is owned by its caller and has no process-wide state. A renderer that paints on one
//! thread creates one and passes `&mut` to it; the slots hold [`Rc`] buffers,
//! so an [`AtlasSlot`] can be kept by a caller while the atlas continues to use the same bytes.
//!
//! # Frame epochs
//!
//! [`GlyphAtlas::begin_frame`] starts a new epoch and drops slots that have not been requested
//! for [`ATLAS_GC_FRAMES`] frames. A page of static text therefore keeps its glyphs resident, a
//! one-off label ages out, and a miss is never wrong — only slower.

use crate::compat::{HashMap, Rc};

use super::glyph_source::{paint_active, Cell, InkKind};
use super::raster::rasterize_face_at_weight;
use super::shaping::face_for_family;

/// How many frames a slot may go unrequested before [`GlyphAtlas::begin_frame`] recycles it.
///
/// Two frames covers the ordinary case where a piece of text is drawn every frame but a
/// transient (a tooltip, a hover label) is not. The value is a policy, not a correctness
/// requirement: an evicted slot is rasterised again on its next use.
pub const ATLAS_GC_FRAMES: u64 = 2;

/// What identifies one rasterised glyph.
///
/// # Why each field is part of the key
///
/// * `face` is the family name the caller asked for. It is the name `shaping::face_for_family`
///   resolves, so a host face and a shipped face with the same name are the same choice by
///   definition.
/// * `ch` is the character, not a shaped glyph id: the atlas is consulted from text layout,
///   where the character is what is known.
/// * `size_px` is the nominal device pixel size the square constructors use; the box actually
///   rasterised is `cell_w x cell_h`, which [`Self::with_cell`] can set independently (G-202).
/// * `cell_w`/`cell_h` are the device cell, because coverage is rasterised into one exact box.
/// * `weight` is part of the key because a variable face is one face with a `wght` axis: the same
///   character at 400 and at 700 is different ink. A static face ignores it, so the key still
///   identifies exactly what was rasterised for every face this crate ships.
/// * `italic` is part of the key because the rasteriser has no italic axis: the atlas applies a
///   synthetic oblique, which produces different bytes from the upright glyph (G-18).
///
/// `Ord` is derived as well as `Hash`/`Eq` because this crate's [`HashMap`]
/// alias becomes an ordered map on allocation-frugal builds.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct GlyphKey {
    /// The family name the caller asked for.
    pub face: &'static str,
    /// The character to rasterise.
    pub ch: char,
    /// The nominal device pixel size [`Self::new`] gives both cell dimensions.
    pub size_px: u8,
    /// The device cell width the glyph is rasterised into.
    pub cell_w: u32,
    /// The device cell height the glyph is rasterised into.
    pub cell_h: u32,
    /// The requested weight, in the conventional `100..=900` scale.
    pub weight: u16,
    /// Whether the glyph is drawn with a synthetic oblique slant.
    pub italic: bool,
}

impl GlyphKey {
    /// A key for `ch` at `size_px` pixels and `weight`, drawn from `face`.
    ///
    /// The cell is the em square: `size_px` wide and `size_px` tall. A caller with a different box
    /// chains [`Self::with_cell`].
    pub const fn new(face: &'static str, ch: char, size_px: u8, weight: u16) -> Self {
        Self {
            face,
            ch,
            size_px,
            cell_w: size_px as u32,
            cell_h: size_px as u32,
            weight,
            italic: false,
        }
    }

    /// The same key with an explicit, possibly non-square device cell.
    ///
    /// Ordinary Latin text advances about 0.6 em while its line box is about 1.2 em, so its
    /// drawing cell is never square; without this the atlas could only serve full-em glyphs
    /// (G-202). `size_px` stays the nominal size so the square constructors and their keys are
    /// unchanged, while [`Self::cell`] reports the explicit box. Both dimensions are part of the
    /// key because coverage depends on the whole cell — the height sets the em and the width sets
    /// the box — so a non-square key can never alias a square one with the same height.
    pub const fn with_cell(mut self, width: u32, height: u32) -> Self {
        self.cell_w = width;
        self.cell_h = height;
        self
    }

    /// The same key, drawn upright or with the synthetic oblique slant.
    pub const fn with_italic(mut self, italic: bool) -> Self {
        self.italic = italic;
        self
    }

    /// The one device cell this key rasterises into.
    ///
    /// `new` makes it the em square (`size_px` by `size_px`); [`Self::with_cell`] replaces that
    /// with an explicit box. The cell is part of what was cached, so the atlas and a direct paint
    /// must be given the same one to agree byte for byte.
    pub const fn cell(self) -> Cell {
        Cell::new(self.cell_w, self.cell_h)
    }
}

/// One cached glyph bitmap.
///
/// The bitmap is one coverage or bitmap byte per pixel, row-major, in the cell reported by
/// [`Self::width`] and [`Self::height`]. Colour ink is not represented: a colour face needs four
/// bytes per pixel, and the atlas reserves the one-byte coverage box. A colour-only character
/// therefore falls through the active stack to the next 1-bit face, exactly as a direct paint with
/// a coverage-sized buffer does.
#[derive(Debug, Clone)]
pub struct AtlasSlot {
    /// Cell width in device pixels.
    width: u32,
    /// Cell height in device pixels.
    height: u32,
    /// The glyph's bytes, shared with every other clone of this slot.
    bitmap: Rc<[u8]>,
    /// The source that produced the ink.
    source: &'static str,
    /// What shape of ink [`Self::bitmap`] holds.
    ink: InkKind,
    /// How many times this slot was served after it was rasterised.
    hits: u64,
    /// The epoch of the most recent request.
    last_epoch: u64,
}

impl AtlasSlot {
    /// Cell width in device pixels.
    pub fn width(&self) -> u32 {
        self.width
    }

    /// Cell height in device pixels.
    pub fn height(&self) -> u32 {
        self.height
    }

    /// One byte per pixel, row-major, `width * height` bytes long.
    pub fn bitmap(&self) -> &[u8] {
        &self.bitmap
    }

    /// The source that produced the ink: a face name, or the fallback source's name.
    pub fn source(&self) -> &'static str {
        self.source
    }

    /// What shape of ink the bitmap holds.
    pub fn ink(&self) -> InkKind {
        self.ink
    }

    /// How many times the atlas served this slot from cache.
    pub fn hits(&self) -> u64 {
        self.hits
    }
}

/// A cache of rasterised glyphs, keyed by [`GlyphKey`].
///
/// See the module docs for the caching and eviction policy.
#[derive(Debug)]
pub struct GlyphAtlas {
    slots: HashMap<GlyphKey, AtlasSlot>,
    epoch: u64,
    /// Number of misses that reached the rasteriser. Test-only so the production struct keeps
    /// exactly the state the cache needs.
    #[cfg(test)]
    rasterisations: u64,
}

impl GlyphAtlas {
    /// An empty atlas at epoch zero.
    pub fn new() -> Self {
        Self {
            slots: HashMap::new(),
            epoch: 0,
            #[cfg(test)]
            rasterisations: 0,
        }
    }

    /// The current frame epoch.
    pub fn epoch(&self) -> u64 {
        self.epoch
    }

    /// How many glyphs are resident.
    pub fn slot_count(&self) -> usize {
        self.slots.len()
    }

    /// Whether `key` is resident.
    pub fn contains(&self, key: &GlyphKey) -> bool {
        self.slots.contains_key(key)
    }

    /// The slot for `key`, rasterising it through the text rasteriser on a miss.
    ///
    /// A hit returns the same shared bitmap and increments the slot's hit count. A miss paints
    /// with the key's named face when that face covers the character; otherwise it resolves
    /// through the active glyph stack, so a character the named face does not cover still renders
    /// its normal fallback — including tofu — with the bytes a direct paint would produce.
    pub fn get_or_raster(&mut self, key: GlyphKey) -> AtlasSlot {
        if let Some(slot) = self.slots.get_mut(&key) {
            slot.hits = slot.hits.saturating_add(1);
            slot.last_epoch = self.epoch;
            return slot.clone();
        }

        #[cfg(test)]
        {
            self.rasterisations = self.rasterisations.saturating_add(1);
        }

        let slot = rasterise(key, self.epoch);
        self.slots.insert(key, slot.clone());
        slot
    }

    /// Starts a new frame epoch and recycles slots that have not been requested for
    /// [`ATLAS_GC_FRAMES`] frames.
    ///
    /// A slot requested in the frame that just ended has an age of one and survives; a slot
    /// requested after this call carries the new epoch and cannot be collected until it ages out.
    pub fn begin_frame(&mut self) {
        self.epoch = self.epoch.wrapping_add(1);
        let epoch = self.epoch;
        self.slots.retain(|_, slot| epoch.saturating_sub(slot.last_epoch) <= ATLAS_GC_FRAMES);
    }

    /// Number of glyphs that reached the rasteriser. Test-only.
    #[cfg(test)]
    fn rasterisations(&self) -> u64 {
        self.rasterisations
    }
}

impl Default for GlyphAtlas {
    fn default() -> Self {
        Self::new()
    }
}

/// Rasterises `key` through the text layer and wraps the result as a slot.
fn rasterise(key: GlyphKey, epoch: u64) -> AtlasSlot {
    let cell = key.cell();

    // The key names the face, so try it first. A named face that this build does not carry, or
    // that has no glyph for the character, falls through to the active stack rather than
    // inventing ink.
    if let Some(face) = face_for_family(key.face) {
        if let Some(bitmap) = rasterize_face_at_weight(face, key.ch, cell, key.weight) {
            let bitmap = oblique_if_requested_shared(bitmap, cell, key.italic);
            return AtlasSlot {
                width: cell.width,
                height: cell.height,
                bitmap,
                source: face.name,
                ink: InkKind::Coverage,
                hits: 0,
                last_epoch: epoch,
            };
        }
    }

    // The named face could not answer. Paint through the active stack, which is the same path a
    // caller that did not name a face would take, so a fallback (including tofu) is unchanged.
    let mut out = crate::compat::vec![0u8; cell.area()];
    let painted = paint_active(key.ch, cell, &mut out);
    let (source, ink) = match painted {
        Some(painted) => (painted.source, painted.ink),
        None => (key.face, InkKind::Coverage),
    };
    AtlasSlot {
        width: cell.width,
        height: cell.height,
        bitmap: Rc::from(oblique_if_requested(out, cell, key.italic)),
        source,
        ink,
        hits: 0,
        last_epoch: epoch,
    }
}

/// [`oblique_if_requested`] for an already-shared buffer.
///
/// The upright case returns the same handle, so the common path copies nothing and allocates
/// nothing (G-15); only the synthetic slant needs a new buffer, because it moves every row.
fn oblique_if_requested_shared(bitmap: Rc<[u8]>, cell: Cell, italic: bool) -> Rc<[u8]> {
    if !italic {
        return bitmap;
    }
    Rc::from(oblique_if_requested(bitmap.to_vec(), cell, true))
}

/// Applies the same synthetic oblique the paint path uses: each row slides horizontally around
/// the cell's vertical centre, so the slant stays inside the cell (G-18).
fn oblique_if_requested(bitmap: Vec<u8>, cell: Cell, italic: bool) -> Vec<u8> {
    if !italic || cell.is_empty() || bitmap.len() < cell.area() {
        return bitmap;
    }
    let width = cell.width as i32;
    let height = cell.height as i32;
    let mut slanted = crate::compat::vec![0u8; bitmap.len()];
    for py in 0..height {
        let shift = (((height as f32 * 0.5) - py as f32) * 0.2).round() as i32;
        for px in 0..width {
            let source_px = px - shift;
            if source_px < 0 || source_px >= width {
                continue;
            }
            let source = (py * width + source_px) as usize;
            let destination = (py * width + px) as usize;
            slanted[destination] = bitmap[source];
        }
    }
    slanted
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::compat::{vec, Rc};
    use crate::render::text::{paint_active, Cell};

    /// The key a caller derives from the source the active stack reports for `ch`.
    ///
    /// A source that names an outline face resolves to that face, so the atlas paints the same
    /// outlines the stack does. A source that names a bitmap face does not resolve by name, so the
    /// atlas takes its documented fallback through the active stack — which is the same path and
    /// therefore the same bytes.
    /// The italic key is a distinct cache entry and the oblique changes the bytes (G-18).
    #[test]
    fn italic_is_part_of_the_key_and_slants_the_bitmap() {
        let upright = GlyphKey::new("face", 'A', 8, 400);
        let italic = upright.with_italic(true);
        assert_ne!(upright, italic, "italic must not alias the upright cache entry");

        let cell = Cell::new(8, 8);
        let mut source = vec![0u8; cell.area()];
        // A single top-row pixel: the oblique moves top rows right, so its column changes.
        source[1] = 255;
        let slanted = oblique_if_requested(source.clone(), cell, true);
        assert_eq!(slanted[1], 0, "the top row moved horizontally");
        assert!(slanted.contains(&255), "the ink is still present");
        assert_eq!(
            oblique_if_requested(source.clone(), cell, false),
            source,
            "an upright request must not alter the bytes"
        );
    }

    /// A non-square device cell is its own cache entry and rasterises that exact box (G-202).
    ///
    /// Ordinary Latin text advances about 0.6 em in a 1.2 em line box, so before `with_cell` the
    /// atlas could not serve it at all. The bytes must still match the uncached paint for the same
    /// cell, which is the property the atlas exists to preserve.
    #[test]
    fn a_non_square_cell_is_a_distinct_key_and_matches_the_uncached_path() {
        let square = GlyphKey::new("face", 'A', 16, 400);
        let tall = square.with_cell(8, 16);
        assert_ne!(square, tall, "a non-square cell must not alias the square entry");

        let cell = tall.cell();
        assert_eq!((cell.width, cell.height), (8, 16), "the explicit cell is the rasterised box");

        // The uncached control: the active stack into a coverage-sized buffer. "face" names no
        // shipped family, so the atlas takes the same active-stack fallback.
        let mut expected = vec![0u8; cell.area()];
        let _ = paint_active('A', cell, &mut expected);
        let mut atlas = GlyphAtlas::new();
        let slot = atlas.get_or_raster(tall);

        assert_eq!(slot.bitmap(), &expected[..], "atlas bytes differ for a non-square cell");
    }

    fn key_for(ch: char, size_px: u8) -> GlyphKey {
        let cell = Cell::new(u32::from(size_px), u32::from(size_px));
        let mut scratch = vec![0u8; cell.area()];
        let face =
            paint_active(ch, cell, &mut scratch).map(|painted| painted.source).unwrap_or("font8x8");
        GlyphKey::new(face, ch, size_px, 400)
    }

    #[test]
    fn a_repeated_lookup_returns_the_same_bitmap_and_counts_the_hit() {
        let mut atlas = GlyphAtlas::new();
        let key = key_for('A', 16);

        let first = atlas.get_or_raster(key);
        let second = atlas.get_or_raster(key);

        assert_eq!(first.bitmap(), second.bitmap(), "a hit must return the cached bytes");
        assert!(
            Rc::ptr_eq(&first.bitmap, &second.bitmap),
            "a hit must share the buffer rather than copy it"
        );
        assert_eq!(first.hits(), 0, "the first request rasterises; it is not a hit");
        assert_eq!(second.hits(), 1, "the second request is one hit");
        assert_eq!(atlas.rasterisations(), 1, "only the first request reaches the rasteriser");
    }

    #[test]
    fn a_second_frame_of_the_same_text_rasterises_nothing() {
        let mut atlas = GlyphAtlas::new();
        let keys: crate::compat::Vec<GlyphKey> =
            "Hello 世界".chars().map(|ch| key_for(ch, 16)).collect();

        for key in keys.iter().copied() {
            atlas.get_or_raster(key);
        }
        let first_frame = atlas.rasterisations();
        assert_eq!(
            first_frame as usize,
            atlas.slot_count(),
            "one rasterisation per distinct glyph"
        );

        atlas.begin_frame();
        for key in keys.iter().copied() {
            atlas.get_or_raster(key);
        }

        assert_eq!(
            atlas.rasterisations(),
            first_frame,
            "the second frame must be served entirely from the atlas"
        );
    }

    #[test]
    fn garbage_collection_keeps_a_slot_hit_in_the_current_frame() {
        let mut atlas = GlyphAtlas::new();
        let key = key_for('A', 16);

        atlas.begin_frame();
        atlas.get_or_raster(key);
        assert!(atlas.contains(&key), "the request should have cached the slot");

        // A slot requested in the frame that just ended is one frame old and must survive.
        atlas.begin_frame();
        assert!(atlas.contains(&key), "a slot hit in the current frame must not be collected");

        // Without another request it eventually ages out.
        for _ in 0..=ATLAS_GC_FRAMES {
            atlas.begin_frame();
        }
        assert!(!atlas.contains(&key), "a slot untouched past the window must be recycled");
    }

    #[test]
    fn the_atlas_matches_the_uncached_path_for_mixed_text() {
        let mut atlas = GlyphAtlas::new();

        for ch in "Aé 中あ".chars() {
            let key = key_for(ch, 16);
            let cell = key.cell();

            // The uncached control: the active stack into a coverage-sized buffer.
            let mut expected = vec![0u8; cell.area()];
            let painted = paint_active(ch, cell, &mut expected);
            let slot = atlas.get_or_raster(key);

            assert_eq!(
                slot.bitmap(),
                &expected[..],
                "atlas bytes differ from the uncached path for U+{:04X} ({ch:?})",
                ch as u32
            );
            assert_eq!(
                Some(slot.source()),
                painted.map(|painted| painted.source),
                "atlas and uncached path disagree about the source for U+{:04X}",
                ch as u32
            );
        }
    }
}