rosace-render 0.1.0

GPU/CPU hybrid renderer for ROSACE with dirty-region tracking
Documentation
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
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::Arc;

use swash::scale::{Render, ScaleContext, Source};
use swash::{CacheKey, FontRef, GlyphId};

/// Text weight. Maps onto real font faces: `SemiBold`/`Bold` use the bold
/// face when one was found; `Light`/`Regular`/`Medium` use the regular face.
/// Before this existed the field was silently ignored — headings were never
/// actually bold.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum FontWeight {
    Light,
    #[default]
    Regular,
    Medium,
    SemiBold,
    Bold,
}

impl FontWeight {
    #[inline]
    fn wants_bold(self) -> bool {
        matches!(self, FontWeight::SemiBold | FontWeight::Bold)
    }
}

/// Which face a glyph resolved to: primary regular/bold, the registered
/// icon face, or a Unicode fallback face by index.
type FaceKey = u8;
const FACE_REGULAR: FaceKey = 0;
const FACE_BOLD: FaceKey = 1;
const FACE_ICON: FaceKey = 2;
const FACE_FALLBACK_BASE: FaceKey = 128;

type GlyphCacheKey = (FaceKey, char, u32); // (face, char, px.to_bits())
type ColorGlyphKey = (char, u32); // (char, px.to_bits())
type KernKey = (FaceKey, char, char);
/// `(raw kern value in design units, units_per_em)` — `None` = no kerning
/// for this pair (still a cached, not-yet-parsed-again fact).
type KernEntry = Option<(i16, u16)>;

/// Rasterized glyph metrics — deliberately the SAME shape/field names
/// `fontdue::Metrics` had (D127 rasterizer migration, 2026-08-03): every
/// consumer (`canvas.rs`'s CPU blit and GPU-atlas paths) reads `.width`/
/// `.height`/`.xmin`/`.ymin`/`.advance_width` off this — keeping the shape
/// identical meant the whole blit/atlas pipeline needed ZERO changes for
/// the fontdue -> swash swap, only this file (glyph PRODUCTION) changed.
#[derive(Debug, Clone, Copy, Default)]
pub struct GlyphMetrics {
    pub xmin: i32,
    pub ymin: i32,
    pub width: usize,
    pub height: usize,
    pub advance_width: f32,
}

/// Shared rasterized glyph: metrics + coverage bitmap.
pub type CachedGlyph = Arc<(GlyphMetrics, Vec<u8>)>;

/// An owned font face. `swash::FontRef` only ever BORROWS a byte slice —
/// it's not meant to be stored long-term — so we keep the bytes ourselves
/// and reconstruct a `FontRef` on demand via [`Self::as_ref`], preserving
/// `offset`/`key` exactly as swash's own doc comment on `FontRef`
/// recommends (a fresh `FontRef::from_index` call on every access would
/// mint a new `CacheKey` each time and defeat swash's internal caching).
pub struct OwnedFace {
    data: Arc<Vec<u8>>,
    offset: u32,
    key: CacheKey,
    /// `Some(weight)` when this face is a variable font that needs an
    /// explicit `wght` axis instanced to render at all correctly — see
    /// `system_ui()`'s doc for why (fontdue, the previous rasterizer,
    /// couldn't do this at all; this is the whole reason for this
    /// migration). `None` for an ordinary static face — no variation
    /// settings needed, and passing an empty settings list is harmless
    /// either way.
    variable_weight: Option<f32>,
}

impl OwnedFace {
    /// Load an in-memory face at `wght: 400` if variable — the right
    /// default for icon fonts (`icon.rs`'s own bundled Material Symbols
    /// face is itself variable, "FILL 0, wght 400" being its documented
    /// intended default instance) and any other single-weight custom face.
    pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
        Self::new(bytes.to_vec(), 0, 400.0)
    }

    /// `weight` is the `wght` axis value to request IF this turns out to be
    /// a variable font (e.g. `400.0` for a "regular" candidate, `700.0` for
    /// a "bold" one) — ignored entirely for static faces.
    fn new(bytes: Vec<u8>, index: u32, weight: f32) -> Option<Self> {
        let data = Arc::new(bytes);
        let (offset, key, is_variable) = {
            let font = FontRef::from_index(&data, index as usize)?;
            (font.offset, font.key, font.variations().len() > 0)
        };
        Some(Self {
            data,
            offset,
            key,
            variable_weight: is_variable.then_some(weight),
        })
    }

    /// Reconstructs a cheap `swash::FontRef` borrowing this face's bytes,
    /// preserving `offset`/`key` (see this struct's doc). Named `font_ref`,
    /// not `as_ref`, to avoid silently resolving to `Arc<OwnedFace>`'s OWN
    /// unrelated `as_ref()` (`AsRef<OwnedFace>`) at call sites that hold an
    /// `&Arc<OwnedFace>` — a real footgun caught by the compiler once, not
    /// worth re-risking with a same-named method.
    fn font_ref(&self) -> FontRef<'_> {
        FontRef { data: &self.data, offset: self.offset, key: self.key }
    }
}

enum Fallback {
    Untried(&'static str),
    Missing,
    Loaded(OwnedFace),
}

/// Color-emoji fallback face (Phase 32 Step 4, D115): raw bytes retained —
/// swash only rasterizes vector OUTLINES via the `Source::Outline` path we
/// use, but color emoji glyphs live in a bitmap table (`sbix` on macOS:
/// literally an embedded PNG per glyph per size, "up to the caller to
/// decode" per `ttf-parser`'s own doc comment), so decoding needs
/// `ttf_parser::Face` directly, re-parsed from these bytes on each lookup
/// (parsing itself is cheap — no re-reading the outline tables swash
/// already indexed).
enum EmojiFallback {
    Untried,
    Missing,
    Loaded(Arc<Vec<u8>>),
}

/// One decoded color glyph: real advance width (font units, ttf_parser's
/// own metric — NOT approximated from bitmap size) + a premultiplied RGBA8
/// bitmap (`tiny_skia::Pixmap::decode_png`'s own convention — the SAME one
/// `rosace-render::image`'s `Image` widget already decodes PNGs with, so
/// this reuses an already-consistent pixel-format contract, not a new one)
/// at whatever `sbix` strike size `glyph_raster_image` picked (nearest
/// available, not rescaled to the exact requested px — a named
/// simplification; see `color_glyph_rgba`'s doc).
pub struct ColorGlyph {
    pub advance: f32,
    pub width: u32,
    pub height: u32,
    /// `Arc`, not a plain `Vec` — matches `rosace_render::canvas::ImagePixels`
    /// (the `Image` widget's own blit-source wrapper), so the GPU-shapes path
    /// clones a refcount instead of the pixel bytes every repaint frame.
    pub rgba: Arc<Vec<u8>>,
}

/// Emoji fallback candidates per platform, in priority order — mirrors
/// `FALLBACK_PATHS`'s own per-platform-paths convention. Only macOS is
/// covered today (Apple Color Emoji, `sbix`); Windows (Segoe UI Emoji,
/// COLR/CPAL — a different table `ttf-parser` also supports via
/// `paint_color_glyph`, not wired here) and Linux (Noto Color Emoji, CBDT)
/// are a named, honest gap, not silently assumed to work.
const EMOJI_FALLBACK_PATHS: &[&str] = &[
    "/System/Library/Fonts/Apple Color Emoji.ttc",
];

/// Common emoji Unicode blocks — used to decide whether a character should
/// even ATTEMPT the color-glyph path (most text never does, so this check
/// must be cheap and must not itself trigger loading the emoji font).
/// Deliberately covers the well-known blocks, not a byte-for-byte match of
/// Unicode's own emoji-data.txt (that table also includes plain digits/`#`
/// as "emoji-capable" via keycap sequences — out of scope for this pass).
fn is_emoji_codepoint(c: char) -> bool {
    matches!(c as u32,
        0x1F300..=0x1FAFF // Misc Symbols&Pictographs, Emoticons, Transport, Supplemental Symbols&Pictographs, Symbols&Pictographs Ext-A
        | 0x2600..=0x27BF // Misc Symbols, Dingbats
        | 0x2190..=0x21FF // Arrows (subset render as emoji with presentation)
        | 0x2B00..=0x2BFF // Misc Symbols and Arrows
        | 0x1F1E6..=0x1F1FF // Regional indicators (flags)
    )
}

pub struct FontCache {
    font: OwnedFace,
    /// Real bold face when the platform provides one; None → bold renders
    /// with the regular face (as before).
    bold: Option<OwnedFace>,
    /// In-memory icon face (D115/Phase 32 Step 2) — registered once by the
    /// widget layer, consulted when the primary faces miss a codepoint and
    /// BEFORE the disk fallback chain: icon fonts live in the Private Use
    /// Area, where system fallback faces (Apple Symbols et al.) carry their
    /// own unrelated glyphs.
    icon: RefCell<Option<Arc<OwnedFace>>>,
    /// Unicode fallback faces, loaded lazily on the first glyph miss —
    /// Arial Unicode alone is ~20 MB, so we don't parse it until a CJK or
    /// symbol codepoint actually appears.
    fallbacks: RefCell<Vec<Fallback>>,
    /// (char, wants_bold) → resolved face. Routing is per-character.
    route_cache: RefCell<HashMap<(char, bool), FaceKey>>,
    glyph_cache: RefCell<HashMap<GlyphCacheKey, CachedGlyph>>,
    metrics_cache: RefCell<HashMap<GlyphCacheKey, f32>>,
    /// Color-emoji fallback face (Phase 32 Step 4) — raw bytes, loaded
    /// lazily on the first emoji-range character (same "don't pay for it
    /// until needed" principle as `fallbacks` above).
    emoji: RefCell<EmojiFallback>,
    /// Decoded color glyphs, keyed like `glyph_cache` — PNG decode is real
    /// work (unlike a cached rasterize, which is already cheap), so this
    /// cache matters more, not less.
    color_glyph_cache: RefCell<HashMap<ColorGlyphKey, Option<Arc<ColorGlyph>>>>,
    /// Raw (design-units, NOT px-scaled) kern value per `(face, left, right)`
    /// pair — `None` means "no kerning for this pair" (still cached, so a
    /// miss doesn't re-parse every call). Design-units instead of px-keyed
    /// like `metrics_cache`/`glyph_cache`: kerning is size-independent
    /// until the final `/ units_per_em * px` scale, so one cache entry
    /// serves EVERY font size a pair is ever asked about, not just one.
    /// Exists because `kern_weighted` re-parses the whole font's `kern`
    /// table via `ttf_parser::Face::parse` on every call — found live
    /// (2026-08-03): with no caching at all, that ran on every character
    /// pair of every string on every single paint frame and made the app
    /// "super slow" — a real regression this cache fixes, not a
    /// premature optimization.
    kern_cache: RefCell<HashMap<KernKey, KernEntry>>,
    /// swash's scaling context — owns its own internal LRU caches/scratch
    /// buffers (per swash's own docs: "keep one instance per thread"). One
    /// per `FontCache`, `RefCell`-wrapped to match every other cache field
    /// here (`FontCache` is already `!Sync` via those).
    scale_ctx: RefCell<ScaleContext>,
}

/// Unicode fallback candidates per platform. Order = priority. Coverage:
/// Arial Unicode (huge BMP incl. CJK), Apple Symbols (arrows, misc),
/// Noto (Linux), Segoe Symbol / MS Gothic (Windows).
const FALLBACK_PATHS: &[&str] = &[
    "/System/Library/Fonts/Supplemental/Arial Unicode.ttf",
    "/System/Library/Fonts/Apple Symbols.ttf",
    "/System/Library/Fonts/Supplemental/Zapf Dingbats.ttf",
    "/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf",
    "/usr/share/fonts/truetype/noto/NotoSansSymbols-Regular.ttf",
    "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
    "C:\\Windows\\Fonts\\seguisym.ttf",
    "C:\\Windows\\Fonts\\msgothic.ttc",
];

/// Bold-face candidates paired with nothing in particular — the first that
/// exists wins. (macOS ships most UI families as .ttc collections without a
/// reliable index → member mapping, so we use the standalone bold files.)
const BOLD_PATHS: &[&str] = &[
    "/System/Library/Fonts/Supplemental/Arial Bold.ttf",
    "/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf",
    "/usr/share/fonts/truetype/ubuntu/Ubuntu-B.ttf",
    "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
    "C:\\Windows\\Fonts\\segoeuib.ttf",
    "C:\\Windows\\Fonts\\arialbd.ttf",
    // Android — stable AOSP path since Android 4.x on every stock/AOSP-based
    // device (D127 "environment" track: real system font, read not bundled).
    "/system/fonts/Roboto-Bold.ttf",
];

impl FontCache {
    fn build(font: OwnedFace, bold: Option<OwnedFace>) -> Self {
        Self {
            font,
            bold,
            icon: RefCell::new(None),
            fallbacks: RefCell::new(
                FALLBACK_PATHS.iter().map(|p| Fallback::Untried(p)).collect(),
            ),
            route_cache: RefCell::new(HashMap::new()),
            glyph_cache: RefCell::new(HashMap::new()),
            metrics_cache: RefCell::new(HashMap::new()),
            emoji: RefCell::new(EmojiFallback::Untried),
            color_glyph_cache: RefCell::new(HashMap::new()),
            kern_cache: RefCell::new(HashMap::new()),
            scale_ctx: RefCell::new(ScaleContext::new()),
        }
    }

    pub fn from_bytes(bytes: &[u8]) -> Self {
        let font = OwnedFace::new(bytes.to_vec(), 0, 400.0)
            .expect("invalid font bytes");
        Self::build(font, None)
    }

    /// Load a font from a bundled **asset** by logical name — resolved
    /// per-platform via [`rosace_core::asset`] (dev: `assets/<name>`; mobile:
    /// the app bundle). Returns `None` if the asset is missing or not a valid
    /// font, so callers can fall back to [`default`](Self::default)/[`embedded`].
    ///
    /// ```ignore
    /// let brand = FontCache::from_asset("fonts/Brand.ttf")
    ///     .unwrap_or_else(FontCache::default);
    /// ```
    pub fn from_asset(name: impl rosace_core::asset::AssetRef) -> Option<Self> {
        let bytes = rosace_core::asset::bytes(name)?;
        let font = OwnedFace::new(bytes, 0, 400.0)?;
        Some(Self::build(font, None))
    }

    /// A fallback font compiled into the binary — DejaVu Sans (permissive
    /// Bitstream Vera license). Used when no system font is available, most
    /// importantly on the web/wasm target where `system_ui()` finds nothing.
    /// Guarantees text always renders on every platform.
    pub fn embedded() -> Self {
        const DEJAVU_SANS: &[u8] =
            include_bytes!("../assets/fonts/DejaVuSans.ttf");
        Self::from_bytes(DEJAVU_SANS)
    }

    /// The DEFAULT app font (Phase 32, user-decided): bundled Inter (SIL
    /// OFL — this crate's own `assets/fonts/inter/LICENSE-OFL.txt`), the same pleasant,
    /// screen-tuned face on EVERY platform with clearly differentiable
    /// weights — Regular for body, real Bold (700) for emphasis. Replaces
    /// "whatever the OS ships" as the default (`system_ui()` remains
    /// available as an opt-in); also replaces the short-lived
    /// Medium-by-default experiment, which read slightly bold.
    ///
    /// Italic faces (`Inter-Italic`/`Inter-BoldItalic`) are bundled
    /// alongside but not yet wired — the text pipeline has no italic
    /// axis yet (tracked in `PHASE_32.md`).
    pub fn bundled() -> Self {
        const INTER_REGULAR: &[u8] =
            include_bytes!("../assets/fonts/inter/Inter-Regular.ttf");
        const INTER_BOLD: &[u8] =
            include_bytes!("../assets/fonts/inter/Inter-Bold.ttf");
        let regular = OwnedFace::new(INTER_REGULAR.to_vec(), 0, 400.0)
            .expect("bundled Inter Regular is valid");
        let bold = OwnedFace::new(INTER_BOLD.to_vec(), 0, 700.0)
            .expect("bundled Inter Bold is valid");
        Self::build(regular, Some(bold))
    }

    fn load_first(paths: &[&str], weight: f32) -> Option<OwnedFace> {
        for path in paths {
            if let Ok(bytes) = std::fs::read(path) {
                if let Some(f) = OwnedFace::new(bytes, 0, weight) {
                    return Some(f);
                }
            }
        }
        None
    }

    /// Score how well `name` (a face's full/typographic name) matches the
    /// weight we're looking for. Higher is better; `None` means "not a
    /// candidate at all" for this weight.
    ///
    /// This exists because `.ttc` collections (how macOS ships every UI
    /// family — Avenir Next, Helvetica Neue, ...) do NOT put the Regular
    /// face at index 0. Naively loading a `.ttc` at index 0 silently picks
    /// WHATEVER face happens to be first — on Avenir Next.ttc that's
    /// actually "Avenir Next Bold". Loading that as "regular" and then
    /// falling back to an unrelated Arial Bold for "bold" produces two
    /// different type families where the nominal "bold" face is visually
    /// THINNER than the nominal "regular" one — bold becomes visually
    /// indistinguishable (or reversed) from regular. Real fix: read the
    /// name table and pick the actual matching face for each weight, from
    /// the same family when possible.
    fn weight_score(name: &str, want_bold: bool) -> Option<i32> {
        let n = name.to_ascii_lowercase();
        if n.contains("italic") || n.contains("oblique") {
            return None;
        }
        if want_bold {
            if n.ends_with("bold") && !n.contains("semi") && !n.contains("demi")
                && !n.contains("ultra") && !n.contains("extra")
            {
                return Some(3);
            }
            if n.contains("bold") { return Some(2); }
            if n.contains("heavy") || n.contains("black") { return Some(1); }
            None
        } else {
            if n.ends_with("regular") || n == "regular" { return Some(3); }
            if !n.contains("bold") && !n.contains("black") && !n.contains("heavy")
                && !n.contains("light") && !n.contains("thin") && !n.contains("medium")
                && !n.contains("demi") && !n.contains("semi") && !n.contains("condensed")
                && !n.contains("narrow") && !n.contains("ultra") && !n.contains("extra")
            {
                return Some(2);
            }
            Some(0)
        }
    }

    fn face_name(bytes: &[u8], index: u32) -> Option<String> {
        let face = ttf_parser::Face::parse(bytes, index).ok()?;
        face.names().into_iter()
            .find(|n| n.name_id == 4 && n.is_unicode())
            .and_then(|n| n.to_string())
    }

    /// Pick the best-matching face index in `bytes` for `want_bold`, or
    /// `None` if it isn't a (usable) collection / no good candidate exists.
    fn best_face_index(bytes: &[u8], want_bold: bool) -> Option<u32> {
        let n = ttf_parser::fonts_in_collection(bytes).unwrap_or(1);
        let mut best: Option<(i32, u32)> = None;
        for i in 0..n {
            let Some(name) = Self::face_name(bytes, i) else { continue };
            let Some(score) = Self::weight_score(&name, want_bold) else { continue };
            if best.map(|(s, _)| score > s).unwrap_or(true) {
                best = Some((score, i));
            }
        }
        best.map(|(_, i)| i)
    }

    fn load_face(bytes: &[u8], index: u32, weight: f32) -> Option<OwnedFace> {
        OwnedFace::new(bytes.to_vec(), index, weight)
    }

    /// Load a system proportional / UI font plus (when available) a real
    /// bold face and the Unicode fallback chain. Prefers a same-family
    /// bold face found inside the regular candidate's own file (see
    /// [`Self::weight_score`]); only falls back to the unrelated
    /// `BOLD_PATHS` standalone files when the chosen family has no bold
    /// member of its own (e.g. plain `Arial.ttf`, which IS the regular
    /// face and needs the separate `Arial Bold.ttf`).
    pub fn system_ui() -> Option<Self> {
        let candidates = [
            // macOS — the REAL San Francisco file. Previously excluded here
            // (fontdue, the old rasterizer, had zero variable-font support
            // and rendered this as broken hairlines) — the whole point of
            // the fontdue -> swash migration (D127, 2026-08-03) was to make
            // this candidate usable: `OwnedFace` detects the variable `wght`
            // axis and `glyph_weighted` instances it explicitly (400/700)
            // instead of reading swash's/skrifa's un-instanced default.
            "/System/Library/Fonts/SFNS.ttf",
            "/System/Library/Fonts/Avenir Next.ttc",
            "/System/Library/Fonts/HelveticaNeue.ttc",
            "/System/Library/Fonts/Helvetica.ttc",
            "/System/Library/Fonts/Supplemental/Arial.ttf",
            // Linux
            "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
            "/usr/share/fonts/truetype/ubuntu/Ubuntu-R.ttf",
            "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
            // Windows
            "C:\\Windows\\Fonts\\segoeui.ttf",
            "C:\\Windows\\Fonts\\arial.ttf",
            // Android — Roboto has shipped at this exact path on every
            // stock/AOSP-based device since Android 4.x (D127 "environment"
            // track). A real system-font read, not a bundled/redistributed
            // copy — same reasoning as the desktop paths above.
            "/system/fonts/Roboto-Regular.ttf",
        ];
        for path in candidates {
            let Ok(bytes) = std::fs::read(path) else { continue };
            let reg_idx = Self::best_face_index(&bytes, false).unwrap_or(0);
            let Some(regular) = Self::load_face(&bytes, reg_idx, 400.0) else { continue };
            let bold = Self::best_face_index(&bytes, true)
                .and_then(|i| Self::load_face(&bytes, i, 700.0))
                .or_else(|| Self::load_first(BOLD_PATHS, 700.0));
            return Some(Self::build(regular, bold));
        }
        None
    }

    /// Load a system monospace font (Menlo, Courier, DejaVu Mono, etc.).
    pub fn system_mono() -> Option<Self> {
        let candidates = [
            "/System/Library/Fonts/Menlo.ttc",
            "/System/Library/Fonts/Monaco.ttf",
            "/System/Library/Fonts/Supplemental/Courier New.ttf",
            "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
            "/usr/share/fonts/truetype/ubuntu/UbuntuMono-R.ttf",
            "/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf",
            "C:\\Windows\\Fonts\\consola.ttf",
            "/system/fonts/DroidSansMono.ttf",
        ];
        let regular = Self::load_first(&candidates, 400.0)?;
        Some(Self::build(regular, None))
    }

    // ── Icon face (D115/Phase 32 Step 2) ─────────────────────────────────

    /// Install an in-memory icon face — glyphs the primary faces miss route
    /// to it before the disk fallback chain, so icon-font codepoints (PUA)
    /// flow through the ordinary text path: physical-px rasterization,
    /// glyph cache, and the GPU glyph atlas, with zero new draw commands.
    ///
    /// Idempotent: the first registration wins; later calls are no-ops.
    /// Registration clears the route cache so codepoints resolved earlier
    /// (as tofu) re-route to the new face.
    pub fn set_icon_face(&self, font: Arc<OwnedFace>) {
        {
            let mut slot = self.icon.borrow_mut();
            if slot.is_some() {
                return;
            }
            *slot = Some(font);
        }
        self.route_cache.borrow_mut().clear();
    }

    /// True once an icon face is installed — lets callers skip
    /// re-registration on every paint.
    pub fn has_icon_face(&self) -> bool {
        self.icon.borrow().is_some()
    }

    // ── Face routing (Unicode fallback, D-text) ──────────────────────────

    /// Resolve which face renders `c` at `weight`: bold face when requested
    /// and it has the glyph; else regular; else the first fallback face that
    /// covers the codepoint (loaded lazily); else regular (tofu).
    fn resolve(&self, c: char, weight: FontWeight) -> FaceKey {
        let wants_bold = weight.wants_bold() && self.bold.is_some();
        let key = (c, wants_bold);
        if let Some(&f) = self.route_cache.borrow().get(&key) {
            return f;
        }

        let face = if wants_bold && self.bold.as_ref().unwrap().font_ref().charmap().map(c) != 0 {
            FACE_BOLD
        } else if self.font.font_ref().charmap().map(c) != 0 {
            FACE_REGULAR
        } else if self
            .icon
            .borrow()
            .as_ref()
            .is_some_and(|f| f.font_ref().charmap().map(c) != 0)
        {
            FACE_ICON
        } else {
            let mut found = FACE_REGULAR; // tofu in the primary face
            let mut fallbacks = self.fallbacks.borrow_mut();
            for (i, slot) in fallbacks.iter_mut().enumerate() {
                if let Fallback::Untried(path) = slot {
                    *slot = match std::fs::read(path).ok().and_then(|b| OwnedFace::new(b, 0, 400.0)) {
                        Some(f) => Fallback::Loaded(f),
                        None => Fallback::Missing,
                    };
                }
                if let Fallback::Loaded(f) = slot {
                    if f.font_ref().charmap().map(c) != 0 {
                        found = FACE_FALLBACK_BASE + i as FaceKey;
                        break;
                    }
                }
            }
            found
        };

        self.route_cache.borrow_mut().insert(key, face);
        face
    }

    /// Run `f` with the resolved face's `OwnedFace`.
    fn with_face<R>(&self, face: FaceKey, f: impl FnOnce(&OwnedFace) -> R) -> R {
        if face == FACE_BOLD {
            if let Some(b) = &self.bold {
                return f(b);
            }
        } else if face == FACE_ICON {
            let icon = self.icon.borrow();
            if let Some(i) = icon.as_ref() {
                return f(i);
            }
        } else if face >= FACE_FALLBACK_BASE {
            let fallbacks = self.fallbacks.borrow();
            if let Some(Fallback::Loaded(fb)) = fallbacks.get((face - FACE_FALLBACK_BASE) as usize) {
                return f(fb);
            }
        }
        f(&self.font)
    }

    // ── Glyphs ────────────────────────────────────────────────────────────

    /// Rasterizes `c` from `owned` at `px` — the one place that actually
    /// talks to swash's scaler, producing metrics and the coverage bitmap
    /// from the SAME `Render` call (a second call would re-rasterize the
    /// same glyph twice for no reason). Applies the face's `wght` variation
    /// axis when it's a variable font (see `OwnedFace::variable_weight`'s doc).
    fn rasterize_glyph(&self, owned: &OwnedFace, c: char, px: f32) -> (GlyphMetrics, Vec<u8>) {
        let font_ref = owned.font_ref();
        let glyph_id: GlyphId = font_ref.charmap().map(c);
        let advance = font_ref.glyph_metrics(&[]).scale(px).advance_width(glyph_id);
        let mut ctx = self.scale_ctx.borrow_mut();
        let mut builder = ctx.builder(font_ref).size(px).hint(true);
        if let Some(w) = owned.variable_weight {
            builder = builder.variations(&[("wght", w)]);
        }
        let mut scaler = builder.build();
        let Some(image) = Render::new(&[Source::Outline]).render(&mut scaler, glyph_id) else {
            return (GlyphMetrics { advance_width: advance, ..Default::default() }, Vec::new());
        };
        let metrics = GlyphMetrics {
            xmin: image.placement.left,
            // swash's `Placement.top` is the offset from the glyph origin
            // (baseline) to the bitmap's top edge, positive = ABOVE the
            // baseline (font/outline Y-up convention) — opposite of
            // fontdue's `ymin` (bottom-edge offset). `ymin` here is
            // reconstructed as `top - height` so downstream code
            // (`layout_glyphs`'s `base_y - ymin - height`) keeps working
            // unchanged. Verified against real rendered output, not just
            // read from swash's source — see this migration's live-test step.
            ymin: image.placement.top - image.placement.height as i32,
            width: image.placement.width as usize,
            height: image.placement.height as usize,
            advance_width: advance,
        };
        (metrics, image.data)
    }

    /// Shared handle to the cached glyph for `c` at `px`/`weight` —
    /// routed through the bold face and Unicode fallbacks.
    pub fn glyph_weighted(&self, c: char, px: f32, weight: FontWeight) -> CachedGlyph {
        let face = self.resolve(c, weight);
        let key = (face, c, px.to_bits());
        {
            let cache = self.glyph_cache.borrow();
            if let Some(entry) = cache.get(&key) {
                return Arc::clone(entry);
            }
        }
        let (metrics, bytes) = self.with_face(face, |f| self.rasterize_glyph(f, c, px));
        let entry = Arc::new((metrics, bytes));
        self.glyph_cache.borrow_mut().insert(key, Arc::clone(&entry));
        entry
    }

    /// Regular-weight glyph (hot path for plain text).
    pub fn glyph(&self, c: char, px: f32) -> CachedGlyph {
        self.glyph_weighted(c, px, FontWeight::Regular)
    }

    /// Rasterize a single character (copies the bitmap — prefer
    /// [`FontCache::glyph`] in hot paths).
    pub fn rasterize(&self, c: char, px: f32) -> (GlyphMetrics, Vec<u8>) {
        let glyph = self.glyph(c, px);
        (glyph.0, glyph.1.clone())
    }

    /// Lazily load the color-emoji fallback face's raw bytes (first
    /// emoji-range character only — same principle as `fallbacks`).
    fn emoji_bytes(&self) -> Option<Arc<Vec<u8>>> {
        {
            match &*self.emoji.borrow() {
                EmojiFallback::Loaded(b) => return Some(Arc::clone(b)),
                EmojiFallback::Missing => return None,
                EmojiFallback::Untried => {}
            }
        }
        let found = EMOJI_FALLBACK_PATHS.iter()
            .find_map(|p| std::fs::read(p).ok())
            .map(Arc::new);
        *self.emoji.borrow_mut() = match &found {
            Some(b) => EmojiFallback::Loaded(Arc::clone(b)),
            None => EmojiFallback::Missing,
        };
        found
    }

    /// Real color glyph for `c` at `px`, if `c` is in an emoji range AND the
    /// emoji fallback face actually has a color bitmap for it (`sbix` only
    /// today — see `EMOJI_FALLBACK_PATHS`'s doc for the Windows/Linux gap).
    /// `None` for anything else, including a plain character that happens
    /// to fail this lookup — callers fall through to the normal outline path.
    pub fn color_glyph_rgba(&self, c: char, px: f32) -> Option<Arc<ColorGlyph>> {
        if !is_emoji_codepoint(c) { return None; }

        let cache_key = (c, px.to_bits());
        if let Some(hit) = self.color_glyph_cache.borrow().get(&cache_key) {
            return hit.clone();
        }

        let result = (|| {
            let bytes = self.emoji_bytes()?;
            let face = ttf_parser::Face::parse(&bytes, 0).ok()?;
            let gid = face.glyph_index(c)?;
            // NOT `face.is_color_glyph(gid)` — that method checks ONLY the
            // `COLR`/`CPAL` layered-vector table (confirmed by reading
            // ttf-parser's own source: `self.tables().colr...`), never
            // `sbix`. Apple Color Emoji uses `sbix` exclusively, so that
            // gate was always false here and this function always bailed —
            // a real bug caught only by noticing the LIVE app rendered tofu
            // boxes for real emoji despite an isolated unit test "passing"
            // (its own graceful-skip-if-font-missing branch silently
            // absorbed the same bug as a false "not installed" negative,
            // instead of catching it — a real lesson, not just a fix).
            // `glyph_raster_image` returning `Some` IS already proof this
            // glyph has a real color bitmap; no separate gate is needed.
            let img = face.glyph_raster_image(gid, px.round().clamp(1.0, u16::MAX as f32) as u16)?;
            if img.format != ttf_parser::RasterImageFormat::PNG { return None; }
            let pixmap = tiny_skia::Pixmap::decode_png(img.data).ok()?;
            let units_per_em = face.units_per_em() as f32;
            let advance = face.glyph_hor_advance(gid)
                .map(|a| a as f32 / units_per_em * px)
                .unwrap_or(pixmap.width() as f32);
            Some(Arc::new(ColorGlyph {
                advance,
                width: pixmap.width(),
                height: pixmap.height(),
                rgba: Arc::new(pixmap.data().to_vec()),
            }))
        })();

        self.color_glyph_cache.borrow_mut().insert(cache_key, result.clone());
        result
    }

    /// Kerning between `left` and `right` at `px`/`weight`. Zero when the
    /// pair spans different faces (fallback boundaries have no kern data),
    /// or when the face has no `kern` table. Reads the `kern` table
    /// directly via `ttf_parser` (already a dependency here for name-table/
    /// collection-index introspection) — swash's own shaping module targets
    /// full GPOS-based complex-script shaping, a bigger API than the simple
    /// pairwise advance this UI-text layout model needs.
    pub fn kern_weighted(&self, left: char, right: char, px: f32, weight: FontWeight) -> f32 {
        let fl = self.resolve(left, weight);
        if fl != self.resolve(right, weight) {
            return 0.0;
        }
        let cache_key = (fl, left, right);
        let cached = {
            let cache = self.kern_cache.borrow();
            cache.get(&cache_key).copied()
        };
        let entry = match cached {
            Some(v) => v,
            None => {
                let v = self.with_face(fl, |owned| {
                    let Ok(face) = ttf_parser::Face::parse(&owned.data, 0) else { return None };
                    let (Some(l), Some(r)) = (face.glyph_index(left), face.glyph_index(right)) else { return None };
                    let upem = face.units_per_em();
                    let table = face.tables().kern?;
                    let raw = table.subtables.into_iter().find_map(|st| st.glyphs_kerning(l, r))?;
                    Some((raw, upem))
                });
                self.kern_cache.borrow_mut().insert(cache_key, v);
                v
            }
        };
        let Some((raw, units_per_em)) = entry else { return 0.0 };
        if units_per_em == 0 { return 0.0; }
        raw as f32 / units_per_em as f32 * px
    }

    pub fn kern(&self, left: char, right: char, px: f32) -> f32 {
        self.kern_weighted(left, right, px, FontWeight::Regular)
    }

    /// Pixel advance width at `px`/`weight`. Cached, fallback-routed.
    pub fn advance_width_weighted(&self, c: char, px: f32, weight: FontWeight) -> f32 {
        let face = self.resolve(c, weight);
        let key = (face, c, px.to_bits());
        {
            let cache = self.metrics_cache.borrow();
            if let Some(&w) = cache.get(&key) {
                return w;
            }
        }
        let w = self.with_face(face, |owned| {
            let font_ref = owned.font_ref();
            let glyph_id = font_ref.charmap().map(c);
            font_ref.glyph_metrics(&[]).scale(px).advance_width(glyph_id)
        });
        self.metrics_cache.borrow_mut().insert(key, w);
        w
    }

    pub fn advance_width(&self, c: char, px: f32) -> f32 {
        self.advance_width_weighted(c, px, FontWeight::Regular)
    }

    /// Total pixel width of a string at `px`/`weight` — advances plus
    /// kerning, in lockstep with `SkiaCanvas::draw_text_weighted` so
    /// measured and painted widths agree.
    pub fn measure_text_weighted(&self, text: &str, px: f32, weight: FontWeight) -> f32 {
        let px = px * rosace_core::media_query::use_media_query().text_scale;
        let mut width = 0.0;
        let mut prev: Option<char> = None;
        for c in text.chars() {
            if let Some(p) = prev {
                width += self.kern_weighted(p, c, px, weight);
            }
            width += self.advance_width_weighted(c, px, weight);
            prev = Some(c);
        }
        width
    }

    pub fn measure_text(&self, text: &str, px: f32) -> f32 {
        self.measure_text_weighted(text, px, FontWeight::Regular)
    }

    /// Distance from the top of the line box to the baseline, in pixels.
    /// Always from the primary face — mixed-face runs share one baseline.
    pub fn ascender(&self, px: f32) -> i32 {
        let font_ref = self.font.font_ref();
        let m = font_ref.metrics(&[]).scale(px);
        if m.ascent > 0.0 { m.ascent.round() as i32 } else { (px * 0.78) as i32 }
    }

    /// Full line height (ascender + descender + gap) in pixels.
    pub fn line_height(&self, px: f32) -> f32 {
        let font_ref = self.font.font_ref();
        let m = font_ref.metrics(&[]).scale(px);
        let total = m.ascent + m.descent + m.leading;
        if total > 0.0 { total } else { px * 1.2 }
    }
}

/// One glyph placed by [`layout_glyphs`]: the cached rasterization plus its
/// top-left pixel position and a stable atlas key (D109/Phase 27 Step 4).
pub struct PlacedGlyph {
    pub glyph: CachedGlyph,
    /// Top-left of the glyph bitmap, physical px.
    pub x: i32,
    pub y: i32,
    /// Stable across frames: `px_bits << 32 | char << 1 | wants_bold`.
    /// Face routing is deterministic per `(char, bold)`, so this fully
    /// identifies the rasterization without exposing `FaceKey`.
    pub key: u64,
    /// `Some` for a color-emoji glyph (Phase 32 Step 4) — `glyph` above is
    /// then a cheap zero-size placeholder (never read) and consumers must
    /// blit this RGBA bitmap directly instead of using `glyph`'s coverage
    /// mask. Reuses the same premultiplied-RGBA-quad pipeline
    /// `DrawCommand::BlitRgba` (the `Image` widget) already established in
    /// both the CPU and GPU-shapes paths, rather than adding a second
    /// coverage-atlas page — real color rendering with no new render
    /// primitive.
    pub color_rgba: Option<Arc<ColorGlyph>>,
}

/// The one glyph-placement walk (kerning, baseline, bearing) shared by the
/// CPU blit path (`SkiaCanvas::draw_text_weighted`) and the GPU atlas
/// collect path — they MUST agree glyph-for-glyph, so the math lives once.
///
/// `origin` is the line box's top-left in physical px (the baseline is
/// derived via [`FontCache::ascender`]); zero-size glyphs (spaces) advance
/// the cursor but emit nothing.
pub fn layout_glyphs(
    font: &FontCache,
    text: &str,
    origin_x: f32,
    origin_y: f32,
    px: f32,
    weight: FontWeight,
) -> Vec<PlacedGlyph> {
    let base_y = origin_y.round() as i32 + font.ascender(px);
    let mut cursor_x = origin_x;
    let mut prev: Option<char> = None;
    let mut out = Vec::with_capacity(text.len());
    let bold = weight.wants_bold() as u64;

    for ch in text.chars() {
        // Variation selectors (U+FE00-U+FE0F, e.g. the "emoji presentation"
        // VS-16 that commonly follows a symbol like U+2600 SUN to request
        // its color form — as in "☀️" — real bug found live: this demo's
        // own "☀️ sunny" text rendered a stray tofu box for the selector
        // itself, since it has no visible glyph in ANY face and wasn't
        // being recognized as a zero-width modifier). Invisible by
        // definition — skip entirely, no glyph lookup, no cursor advance.
        if matches!(ch as u32, 0xFE00..=0xFE0F) {
            continue; // invisible modifier — `prev` stays the last REAL glyph for correct kerning after it
        }
        if let Some(p) = prev {
            cursor_x += font.kern_weighted(p, ch, px, weight);
        }
        prev = Some(ch);

        // Color-emoji check first: a real emoji codepoint should never fall
        // through to the outline rasterizer (the primary UI font has no
        // glyph for it at all, or — worse — a plain monochrome fallback
        // shape that isn't the real emoji).
        if let Some(cg) = font.color_glyph_rgba(ch, px) {
            let gx = cursor_x.round() as i32;
            let gy = base_y - cg.height as i32; // bottom-aligned to baseline, left-aligned to cursor
            let key = ((px.to_bits() as u64) << 32) | ((ch as u64) << 1) | bold | (1 << 63);
            let placeholder: CachedGlyph = Arc::new((GlyphMetrics::default(), Vec::new()));
            let advance = cg.advance;
            out.push(PlacedGlyph { glyph: placeholder, x: gx, y: gy, key, color_rgba: Some(cg) });
            cursor_x += advance;
            continue;
        }

        let glyph = font.glyph_weighted(ch, px, weight);
        let advance = glyph.0.advance_width;
        if glyph.0.width != 0 && glyph.0.height != 0 {
            let gx = cursor_x.round() as i32 + glyph.0.xmin;
            let gy = base_y - glyph.0.ymin - glyph.0.height as i32;
            let key = ((px.to_bits() as u64) << 32) | ((ch as u64) << 1) | bold;
            out.push(PlacedGlyph { glyph, x: gx, y: gy, key, color_rgba: None });
        }
        cursor_x += advance;
    }
    out
}

#[cfg(test)]
mod color_glyph_tests {
    use super::*;

    #[test]
    fn is_emoji_codepoint_covers_common_emoji_but_not_plain_text() {
        assert!(is_emoji_codepoint('😀')); // U+1F600, Emoticons block
        assert!(is_emoji_codepoint('🎉')); // U+1F389, Misc Symbols & Pictographs
        assert!(is_emoji_codepoint(''));  // U+2600, Misc Symbols
        assert!(!is_emoji_codepoint('a'));
        assert!(!is_emoji_codepoint('#'));
        assert!(!is_emoji_codepoint(' '));
    }

    /// Real integration test, not a mock: decodes an ACTUAL emoji glyph from
    /// whatever color-emoji font this machine has installed (this repo's
    /// dev machines are macOS, where `/System/Library/Fonts/Apple Color
    /// Emoji.ttc` is always present) — the exit bar this Phase 32 Step 4
    /// task is actually held to ("a real running app renders a string
    /// containing at least one emoji correctly, real color").
    ///
    /// The skip path checks the font FILE's existence directly, separately
    /// from whether decode succeeded — a lesson from a real bug this test
    /// almost hid: an earlier version skipped whenever `color_glyph_rgba`
    /// returned `None`, which is EXACTLY what it did (every time) while a
    /// real bug (`is_color_glyph` checking the wrong table) was silently
    /// swallowing every lookup on this very machine, where the font
    /// genuinely IS installed. A skip must only fire for the environment
    /// gap it claims to be about, or it stops being a skip and becomes a
    /// blindfold.
    #[test]
    fn color_glyph_rgba_decodes_a_real_emoji_on_this_machine() {
        if EMOJI_FALLBACK_PATHS.iter().all(|p| !std::path::Path::new(p).exists()) {
            eprintln!("no color-emoji font file on this machine — skipping (not a failure)");
            return;
        }
        let font = FontCache::embedded();
        let cg = font.color_glyph_rgba('😀', 32.0)
            .expect("font file exists but color_glyph_rgba returned None — a real bug, not an environment gap");
        assert!(cg.width > 0 && cg.height > 0, "decoded bitmap must have real dimensions");
        assert_eq!(cg.rgba.len(), (cg.width * cg.height * 4) as usize, "RGBA8 buffer must match width*height*4");
        assert!(cg.advance > 0.0, "a real emoji must have a positive advance width");
        // At least one non-transparent, non-black pixel — a real decoded
        // photo/icon, not an all-zero buffer silently accepted as "success".
        let has_real_color = cg.rgba.chunks_exact(4).any(|p| p[3] > 0 && (p[0] > 20 || p[1] > 20 || p[2] > 20));
        assert!(has_real_color, "decoded emoji must contain real non-black visible pixels");
    }

    #[test]
    fn color_glyph_rgba_returns_none_for_plain_text() {
        let font = FontCache::embedded();
        assert!(font.color_glyph_rgba('a', 16.0).is_none());
    }

    #[test]
    fn layout_glyphs_places_a_real_emoji_with_color_rgba_set() {
        // px=16.0 deliberately: the exact size that exposed the
        // `is_color_glyph` bug live (the earlier isolated test used 32.0,
        // which happens to be an exact sbix strike — this one must NOT
        // rely on that coincidence, since the real app renders at 16/24px).
        if EMOJI_FALLBACK_PATHS.iter().all(|p| !std::path::Path::new(p).exists()) {
            eprintln!("no color-emoji font file on this machine — skipping (not a failure)");
            return;
        }
        let font = FontCache::embedded();
        let placed = layout_glyphs(&font, "hi 😀 there", 0.0, 0.0, 16.0, FontWeight::Regular);
        let pg = placed.iter().find(|pg| pg.color_rgba.is_some())
            .expect("font file exists but no placed glyph had color_rgba set — a real bug, not an environment gap");
        let cg = pg.color_rgba.as_ref().unwrap();
        assert!(cg.width > 0 && cg.height > 0);
        // Regardless of emoji decode availability, the surrounding plain
        // text must still be placed normally.
        assert!(placed.iter().any(|pg| pg.color_rgba.is_none()), "plain characters must still be placed");
    }

    #[test]
    fn variation_selector_16_produces_no_placed_glyph() {
        // Real bug found live: "☀️" (U+2600 SUN + U+FE0F VARIATION
        // SELECTOR-16, requesting the emoji/color presentation) rendered a
        // stray tofu box for the selector itself — it has no visible glyph
        // in any face and wasn't recognized as a zero-width modifier.
        // A bare selector with nothing else in the string must place NOTHING.
        let font = FontCache::embedded();
        let placed = layout_glyphs(&font, "\u{FE0F}", 0.0, 0.0, 16.0, FontWeight::Regular);
        assert!(placed.is_empty(), "a lone variation selector must never produce a placed glyph");

        // "☀️" must place AT MOST one glyph (the sun itself, color or
        // monochrome depending on font availability) — never two, which
        // would mean the selector also got its own tofu-box glyph.
        let placed = layout_glyphs(&font, "\u{2600}\u{FE0F}", 0.0, 0.0, 16.0, FontWeight::Regular);
        assert!(placed.len() <= 1, "the selector must not add a second placed glyph, got {}", placed.len());
    }
}