Skip to main content

felis_render_core/
glyphs.rs

1//! Glyph index that bridges `felis_shaping` with a backend's glyph
2//! atlas: [`GlyphIndex`] shapes, rasterizes and packs glyphs, and the
3//! backend drains its [`PendingUpload`]s into wherever its atlas lives
4//! (`felis-render-wgpu`'s `GlyphCache` writes them into a texture).
5
6use std::{
7    collections::{HashMap, HashSet},
8    num::NonZeroU32,
9};
10
11use felis_grid::{AttrFlags, Grapheme, HAlign, ScreenBuffer, Sizing, VAlign};
12use felis_shaping::{
13    CellMetrics, FACE_ID_SATURATED, Font, FontStack, FontStyle, GlyphBitmap, GlyphId, GlyphPixels,
14    ShapeCache, ShapedGlyph, Shaper, SizingKey,
15};
16use foldhash::fast::RandomState;
17
18use crate::{
19    atlas::{Allocation, ShelfAtlas},
20    box_drawing,
21    display_list::AtlasKind,
22};
23
24/// Glyphs are cell-sized, so the 8192 image ceiling would be gross
25/// over-allocation: a 2048² `R8` sheet is 4 MiB against 64 MiB and holds
26/// thousands of slots; the packer's evict-and-rebuild path absorbs
27/// overflow. Revisit if CJK-heavy sessions show glyph atlas churn.
28pub const GLYPH_ATLAS_SIDE: u32 = 2048;
29
30/// Color-glyph atlas side ceiling. At four bytes per pixel an `Rgba8`
31/// sheet half the mono side costs the same bytes as the mono `R8` sheet
32/// and still holds hundreds of emoji before a reset recycles it.
33pub const COLOR_GLYPH_ATLAS_SIDE: u32 = 1024;
34const _: () = assert!(COLOR_GLYPH_ATLAS_SIDE < GLYPH_ATLAS_SIDE);
35
36/// Never larger than the mono sheet. Shared by [`GlyphIndex`] and the
37/// backend's color atlas so their UV spaces agree.
38#[must_use]
39pub fn color_atlas_side(mono_side: NonZeroU32) -> NonZeroU32 {
40    NonZeroU32::new(mono_side.get().min(COLOR_GLYPH_ATLAS_SIDE)).unwrap_or(mono_side)
41}
42
43/// Base scalar of a [`Grapheme::Cluster`] cell. The compositing pass,
44/// `populate`'s priming, and the emission fallback all resolve through
45/// this so they agree on the scalar. `None` while the handle is not yet
46/// in the screen's cluster table (a transient reattach state).
47#[must_use]
48pub fn cluster_base_char(screen: &ScreenBuffer, id: NonZeroU32) -> Option<char> {
49    screen.cluster_str(id).and_then(|s| s.chars().next())
50}
51
52#[derive(Copy, Clone, Debug, PartialEq, Eq)]
53pub struct GlyphSlot {
54    /// Top-left of the bitmap in its atlas sheet, in pixels.
55    pub atlas_px: [u32; 2],
56    /// Pixel size of the glyph bitmap (not the cell).
57    pub size_px: [u32; 2],
58    /// `(left, top)` from the cell's `(0, baseline)` origin, per swash.
59    pub offset_px: [i32; 2],
60    /// True when the bitmap lives in the RGBA color atlas.
61    pub is_color: bool,
62}
63
64pub trait AtlasView {
65    /// `None` on a miss (cache not populated, atlas full, whitespace).
66    /// `sizing` keeps OSC 66 sized variants on separate slots.
67    fn slot(&self, glyph: char, cell_height_px: u32, sizing: SizingKey) -> Option<GlyphSlot>;
68
69    /// Deliberately no default impl: a `None` default would be silently
70    /// inherited by `GlyphCache`, and every shaped lookup would miss, so
71    /// any non-empty `font.features` would render primary-font runs
72    /// blank.
73    fn glyph_id_slot(
74        &self,
75        glyph_id: GlyphId,
76        cell_height_px: u32,
77        sizing: SizingKey,
78    ) -> Option<GlyphSlot>;
79
80    /// The slot of a cluster glyph shrunk to `px` to fit its cells;
81    /// see [`ShapedCell::Cluster`].
82    fn fitted_glyph_id_slot(
83        &self,
84        glyph_id: GlyphId,
85        px: u16,
86        cell_height_px: u32,
87        sizing: SizingKey,
88    ) -> Option<GlyphSlot>;
89
90    /// The shaped glyphs of a multi-scalar overlay cluster, `None` until
91    /// the overlay's text was populated.
92    fn overlay_cluster(&self, text: &str) -> Option<&[ClusterGlyph]>;
93
94    /// The pixel side of the sheet a slot's [`GlyphSlot::atlas_px`] is in.
95    fn sheet_side(&self, sheet: AtlasKind) -> u32;
96}
97
98/// Mirrors the enum-to-u8 mapping `felis-vt`'s parser enforces.
99#[must_use]
100pub const fn sizing_key_from(sizing: Sizing) -> SizingKey {
101    let valign = match sizing.valign() {
102        VAlign::Top => 0,
103        VAlign::Bottom => 1,
104        VAlign::Center => 2,
105    };
106    let halign = match sizing.halign() {
107        HAlign::Left => 0,
108        HAlign::Right => 1,
109        HAlign::Center => 2,
110    };
111    SizingKey::new(
112        sizing.scale(),
113        sizing.cell_width(),
114        sizing.frac_num(),
115        sizing.frac_den(),
116        valign,
117        halign,
118    )
119}
120
121/// Only `BOLD` / `ITALIC` participate; `FAINT` does not change glyph
122/// selection.
123#[must_use]
124pub const fn font_style_of(flags: AttrFlags) -> FontStyle {
125    FontStyle {
126        bold: flags.contains(AttrFlags::BOLD),
127        italic: flags.contains(AttrFlags::ITALIC),
128    }
129}
130
131/// The columns a cluster's block spans before the OSC 66 scale: `w`,
132/// else the grid's span, so a VS16 widen the grid refused gets one.
133#[must_use]
134pub fn cluster_block_cols(screen: &ScreenBuffer, row: u16, col: u16) -> u16 {
135    match screen.cell_sizing(row, col).map_or(0, |s| s.cell_width()) {
136        0 => {
137            let (first, last) = screen.char_span(row, col);
138            last - first + 1
139        }
140        w => u16::from(w),
141    }
142}
143
144#[derive(Debug)]
145pub struct PendingUpload {
146    pub alloc: Allocation,
147    /// The variant picks the destination: `Coverage` goes to the mono
148    /// atlas, `Rgba` to the color atlas.
149    pub pixels: GlyphPixels,
150}
151
152/// The shaper collapses ligatures into glyph ids with no single source
153/// `char`, so the shaped path cannot ride a `char`-keyed map; the
154/// variant keeps the two key spaces apart in one map. A shaped run
155/// claims every cell it covers, so no cell is populated under both.
156#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
157enum SlotKey {
158    Char(char),
159    Glyph(GlyphId),
160    /// A cluster glyph rasterized at the pixel size its cluster fits at:
161    /// a mark such as U+0301 is native in `é` and shrunk in `※́`.
162    FittedGlyph(GlyphId, u16),
163}
164
165pub struct GlyphIndex {
166    stack: FontStack,
167    font_size_physical_px: f32,
168    metrics: CellMetrics,
169    shape: ShapeCache,
170    atlas: ShelfAtlas,
171    /// Separate RGBA shelf so color and mono allocations never
172    /// interleave across textures with different bytes per texel.
173    color_atlas: ShelfAtlas,
174    /// `None` caches known blanks and glyphs too large for an empty sheet.
175    /// The `SizingKey` isolates OSC 66 sized glyphs and faces
176    /// ([`SizingKey::font_id`]). Not `FixedState` like felis-grid's intern
177    /// tables: PTY output picks these keys, so a fixed seed would let a
178    /// producer precompute keys sharing a bucket.
179    slots: HashMap<(SlotKey, u32, SizingKey), Option<GlyphSlot>, RandomState>,
180    /// `None` entries take no atlas space, so no atlas recycle bounds
181    /// them; past [`Self::BLANK_SLOT_MAX`] they are swept instead.
182    blank_slots: usize,
183    /// Tiny glyphs fill a sheet only after millions of slots, so the
184    /// sheets count as full at [`Self::PLACED_SLOT_MAX`] as well.
185    placed_slots: usize,
186    /// Run-level shape memo per [`FontStyle`] for borrow-keyed `&str` lookup.
187    /// Text and style suffice as keys because font, size, and features are fixed.
188    /// Avoids Swash cluster-analysis and GSUB costs on every frame.
189    shaped_runs: [HashMap<String, Vec<ShapedGlyph>>; 4],
190    /// Overlay clusters (pre-edit, bar labels) shaped like a grid cluster
191    /// cell. Lives here so a font, size or feature reload drops it with
192    /// the faces its glyph ids belong to.
193    overlay_clusters: HashMap<String, Vec<ClusterGlyph>>,
194    /// The fitted pixel size of each overrunning cluster, keyed by its
195    /// text, block columns and shaped key: measuring rasterizes every
196    /// glyph, which each repaint of a prompt would otherwise repeat.
197    cluster_fits: HashMap<(String, u16, SizingKey), u16>,
198    pending: Vec<PendingUpload>,
199    primed: HashSet<(char, SizingKey), RandomState>,
200    /// Set when a whole-atlas reset fires mid-walk: the slots the walk
201    /// already handed out are gone, so the renderer walks again before
202    /// building the frame's instances.
203    atlas_reset_during_walk: bool,
204    atlas_resets: u64,
205    /// The last complete walk's key; a recycle drops it along with the
206    /// slots, so the next walk covers every row.
207    walked: Option<WalkKey>,
208}
209
210/// What a grid walk's result depends on besides the cells of the rows
211/// it walked.
212#[derive(Debug, Clone, Copy, PartialEq, Eq)]
213struct WalkKey {
214    rows: u16,
215    cols: u16,
216    shaped: bool,
217}
218
219/// The pixel box an overwide glyph or cluster is shrunk into.
220#[derive(Debug, Clone, Copy)]
221struct FitBlock {
222    w: f32,
223    h: f32,
224    slack: f32,
225}
226
227impl FitBlock {
228    /// The cell width rounds the primary advance, so a primary glyph
229    /// overruns its block by up to half a pixel per unit of `s`.
230    fn holds(self, advance: f32) -> bool {
231        advance <= self.w + self.slack
232    }
233
234    fn ratio(self, ink_w: f32, ink_h: f32) -> f32 {
235        (self.w / ink_w).min(self.h / ink_h).min(1.0)
236    }
237}
238
239impl GlyphIndex {
240    #[must_use]
241    pub fn new(stack: FontStack, font_size_physical_px: f32, atlas_side: NonZeroU32) -> Self {
242        let metrics = stack.primary().cell_metrics(font_size_physical_px);
243        let color_side = color_atlas_side(atlas_side);
244        Self {
245            stack,
246            font_size_physical_px,
247            metrics,
248            shape: ShapeCache::default(),
249            atlas: ShelfAtlas::new(atlas_side, atlas_side),
250            color_atlas: ShelfAtlas::new(color_side, color_side),
251            slots: HashMap::default(),
252            blank_slots: 0,
253            placed_slots: 0,
254            shaped_runs: std::array::from_fn(|_| HashMap::new()),
255            overlay_clusters: HashMap::new(),
256            cluster_fits: HashMap::new(),
257            pending: Vec::new(),
258            primed: HashSet::default(),
259            atlas_reset_during_walk: false,
260            atlas_resets: 0,
261            walked: None,
262        }
263    }
264
265    /// Scrollback streams unbounded unique row texts; at ~2.5 KiB per
266    /// 80-column entry this bounds the memo near 10 MiB, the same order
267    /// as `ShapeCache`'s 16 MiB budget.
268    pub const SHAPED_RUN_CACHE_MAX: usize = 4096;
269
270    /// A screenful rarely holds more than a few dozen distinct blank
271    /// keys (the spaces in four styles and a handful of sizings).
272    pub const BLANK_SLOT_MAX: usize = 4096;
273
274    /// Twice the one-cell slots of an 8×16 cell a 2048² sheet holds, so
275    /// text at a normal size fills the sheet first. The map's table then
276    /// stays under 8 MiB, spare hash capacity included.
277    pub const PLACED_SLOT_MAX: usize = 65_536;
278
279    /// Returns `true` when this call shaped (a miss), `false` on a memo
280    /// hit. `features` must be constant for the life of the index: the
281    /// memo keys on text alone because the renderer routes every feature
282    /// change through a reload that rebuilds the `GlyphIndex`
283    /// (`reload_font_features`).
284    pub fn shape_run_cached(
285        &mut self,
286        shaper: &mut Shaper,
287        features: &[String],
288        style: FontStyle,
289        text: &str,
290        out: &mut Vec<ShapedGlyph>,
291    ) -> bool {
292        out.clear();
293        let map = &self.shaped_runs[style.index()];
294        if let Some(cached) = map.get(text) {
295            out.extend_from_slice(cached);
296            return false;
297        }
298        let primary = self.stack.styled_primary(style).clone();
299        let shaped = shaper.shape_run(&primary, self.font_size_physical_px, features, text);
300        out.extend_from_slice(&shaped);
301        // Insert first, then evict an arbitrary *other* entry while over
302        // cap, so the entry just produced survives its own insertion.
303        let map = &mut self.shaped_runs[style.index()];
304        map.insert(text.to_owned(), shaped);
305        while map.len() > Self::SHAPED_RUN_CACHE_MAX {
306            let Some(evict_key) = map.keys().find(|k| k.as_str() != text).cloned() else {
307                break;
308            };
309            map.remove(&evict_key);
310        }
311        true
312    }
313
314    #[must_use]
315    pub fn shaped_run_cache_len(&self) -> usize {
316        self.shaped_runs.iter().map(HashMap::len).sum()
317    }
318
319    #[must_use]
320    pub const fn cell_metrics(&self) -> CellMetrics {
321        self.metrics
322    }
323
324    #[must_use]
325    pub const fn font_size_physical_px(&self) -> f32 {
326        self.font_size_physical_px
327    }
328
329    #[must_use]
330    pub const fn stack(&self) -> &FontStack {
331        &self.stack
332    }
333
334    #[must_use]
335    pub const fn atlas_side(&self) -> u32 {
336        self.atlas.width()
337    }
338
339    #[must_use]
340    pub fn contains(&self, glyph: char, cell_height: u32, sizing: SizingKey) -> bool {
341        self.contains_key(SlotKey::Char(glyph), cell_height, sizing)
342    }
343
344    fn contains_key(&self, key: SlotKey, cell_height: u32, sizing: SizingKey) -> bool {
345        self.slots.contains_key(&(key, cell_height, sizing))
346    }
347
348    fn lookup(&self, key: SlotKey, cell_height: u32, sizing: SizingKey) -> Option<GlyphSlot> {
349        self.slots
350            .get(&(key, cell_height, sizing))
351            .copied()
352            .flatten()
353    }
354
355    /// A second call with the same `(glyph, cell_height, sizing)` is a
356    /// no-op. The rasterizer receives `font_size_physical_px × scale`,
357    /// so an `s=N` cell produces an N×-bigger bitmap under its own key.
358    /// Returns `true` when this call did work, `false` on a hit.
359    pub fn ensure(&mut self, glyph: char, cell_height: u32, sizing: SizingKey) -> bool {
360        let key = (SlotKey::Char(glyph), cell_height, sizing);
361        if self.slots.contains_key(&key) {
362            return false;
363        }
364        let scale = sizing.effective_scale();
365        let scaled = CellMetrics {
366            width: ((self.metrics.width as f32) * scale).round().max(1.0) as u32,
367            height: ((cell_height as f32) * scale).round().max(1.0) as u32,
368            ascent: ((self.metrics.ascent as f32) * scale).round() as u32,
369        };
370        if let Some(bitmap) = box_drawing::rasterize(glyph, scaled) {
371            let slot = self.allocate(bitmap);
372            self.insert_slot(key, slot);
373            return true;
374        }
375        let font = self.stack.resolve(glyph, sizing.style()).clone();
376        let bitmap = if let Some(bitmap) = self.fitted_bitmap(&font, glyph, cell_height, sizing) {
377            bitmap
378        } else {
379            let px = (self.font_size_physical_px * scale).round() as u32;
380            self.shape
381                .get_or_insert_sized(&font, glyph, px, sizing)
382                .clone()
383        };
384        let slot = self.allocate(bitmap);
385        self.insert_slot(key, slot);
386        true
387    }
388
389    /// `None` keeps the glyph's own size and bearings. The block matches
390    /// `push_char_cell`'s: `w` (or the char's own width) cells at the
391    /// integer scale `s`, whatever the fractional glyph scale.
392    fn fitted_bitmap(
393        &mut self,
394        font: &Font,
395        glyph: char,
396        cell_height: u32,
397        sizing: SizingKey,
398    ) -> Option<GlyphBitmap> {
399        let glyph_scale = sizing.effective_scale();
400        let base_px = self.font_size_physical_px * glyph_scale;
401        let cells = match sizing.cell_width() {
402            0 => felis_grid::char_cell_width(glyph).max(1),
403            w => w,
404        };
405        let block = self.fit_block(cells.into(), cell_height, sizing);
406        if block.holds(font.advance_px(glyph, base_px)?) {
407            return None;
408        }
409        let ink = self
410            .shape
411            .get_or_insert_sized(font, glyph, base_px.round() as u32, sizing);
412        if ink.is_blank() {
413            return None;
414        }
415        let ratio = block.ratio(ink.width() as f32, ink.height() as f32);
416        let px = (base_px * ratio).floor().max(1.0) as u32;
417        let mut bitmap = self
418            .shape
419            .get_or_insert_sized(font, glyph, px, sizing)
420            .clone();
421        bitmap.left = ((block.w - bitmap.width() as f32) / 2.0).round() as i32;
422        let ascent = self.metrics.ascent as f32 * glyph_scale;
423        bitmap.top = (ascent - (block.h - bitmap.height() as f32) / 2.0).round() as i32;
424        Some(bitmap)
425    }
426
427    /// `cells` cells at the integer scale `s`, whatever the fractional
428    /// glyph scale.
429    fn fit_block(&self, cells: u16, cell_height: u32, sizing: SizingKey) -> FitBlock {
430        let layout_scale = f32::from(sizing.scale().max(1));
431        FitBlock {
432            w: (self.metrics.width * u32::from(cells)) as f32 * layout_scale,
433            h: cell_height as f32 * layout_scale,
434            slack: layout_scale,
435        }
436    }
437
438    /// Glyph ids are face-relative: [`SizingKey::font_id`] names the
439    /// face (`0` = the styled primary, non-zero a fallback face) and
440    /// rides the key, so the same raw id from two faces lands two slots.
441    /// Returns `true` on first insertion, `false` on a hit.
442    pub fn ensure_glyph_id(
443        &mut self,
444        glyph_id: GlyphId,
445        cell_height: u32,
446        sizing: SizingKey,
447    ) -> bool {
448        let px = self.font_size_physical_px * sizing.effective_scale();
449        self.ensure_glyph_slot(SlotKey::Glyph(glyph_id), glyph_id, px, cell_height, sizing)
450    }
451
452    /// [`Self::ensure_glyph_id`] at `px`, the size `cluster_fit_px`
453    /// chose, instead of the sizing's own.
454    pub fn ensure_fitted_glyph_id(
455        &mut self,
456        glyph_id: GlyphId,
457        px: u16,
458        cell_height: u32,
459        sizing: SizingKey,
460    ) -> bool {
461        let key = SlotKey::FittedGlyph(glyph_id, px);
462        self.ensure_glyph_slot(key, glyph_id, f32::from(px), cell_height, sizing)
463    }
464
465    fn ensure_glyph_slot(
466        &mut self,
467        slot_key: SlotKey,
468        glyph_id: GlyphId,
469        px: f32,
470        cell_height: u32,
471        sizing: SizingKey,
472    ) -> bool {
473        let key = (slot_key, cell_height, sizing);
474        if self.slots.contains_key(&key) {
475            return false;
476        }
477        let font = self.stack.font_at(sizing.font_id(), sizing.style()).clone();
478        let bitmap = self.shape.rasterize_glyph_id(&font, glyph_id, px);
479        let slot = self.allocate(bitmap);
480        self.insert_slot(key, slot);
481        true
482    }
483
484    /// PTY output picks the keys. Dropping the memo costs only a
485    /// re-measure: the shape frame keeps each cell's fitted size.
486    const CLUSTER_FIT_MAX: usize = 1024;
487
488    /// The pixel size at which the cluster's ink fits a block of `cols`
489    /// cells, or `None` when its advance stays inside the block. The
490    /// block is `fitted_bitmap`'s, so a one-glyph cluster fits exactly
491    /// as its bare char does.
492    fn cluster_fit_px(
493        &mut self,
494        text: &str,
495        glyphs: &[ShapedGlyph],
496        cols: u16,
497        sizing: SizingKey,
498    ) -> Option<u16> {
499        let glyph_scale = sizing.effective_scale();
500        let block = self.fit_block(cols, self.metrics.height, sizing);
501        let advance: f32 = glyphs.iter().map(|g| g.advance_px).sum::<f32>() * glyph_scale;
502        if block.holds(advance) {
503            return None;
504        }
505        let key = (text.to_owned(), cols, sizing);
506        if let Some(&px) = self.cluster_fits.get(&key) {
507            return (px > 0).then_some(px);
508        }
509        let font = self.stack.font_at(sizing.font_id(), sizing.style()).clone();
510        let effective_px = self.font_size_physical_px * glyph_scale;
511        let mut pen = 0.0f32;
512        let mut ink: Option<[f32; 4]> = None;
513        for g in glyphs {
514            let bitmap = self
515                .shape
516                .rasterize_glyph_id(&font, g.glyph_id, effective_px);
517            if !bitmap.is_blank() {
518                let x0 = g.x_offset_px.mul_add(glyph_scale, pen) + bitmap.left as f32;
519                let y0 = g.y_offset_px.mul_add(-glyph_scale, -bitmap.top as f32);
520                let [x1, y1] = [x0 + bitmap.width() as f32, y0 + bitmap.height() as f32];
521                ink = Some(ink.map_or([x0, y0, x1, y1], |[a, b, c, d]| {
522                    [a.min(x0), b.min(y0), c.max(x1), d.max(y1)]
523                }));
524            }
525            pen = g.advance_px.mul_add(glyph_scale, pen);
526        }
527        // A blank cluster has nothing to fit; 0 records that.
528        let px = ink.map_or(0, |[x0, y0, x1, y1]| {
529            (effective_px * block.ratio(x1 - x0, y1 - y0))
530                .floor()
531                .clamp(1.0, f32::from(u16::MAX)) as u16
532        });
533        if self.cluster_fits.len() >= Self::CLUSTER_FIT_MAX {
534            self.cluster_fits.clear();
535        }
536        self.cluster_fits.insert(key, px);
537        (px > 0).then_some(px)
538    }
539
540    /// Overlay text is typed by the user, not streamed by a producer, so
541    /// a frame holds a handful of clusters at most.
542    const OVERLAY_CLUSTER_MAX: usize = 256;
543
544    /// Bounds the overlay shapes between frames, never during one: the
545    /// frame paints every cluster it primed.
546    pub fn trim_overlay_clusters(&mut self) {
547        if self.overlay_clusters.len() > Self::OVERLAY_CLUSTER_MAX {
548            self.overlay_clusters.clear();
549        }
550    }
551
552    /// Shapes `text`, one multi-scalar cluster of overlay text, as
553    /// `composite_cluster` shapes a cluster cell, and primes its glyphs.
554    pub fn ensure_overlay_cluster(&mut self, shaper: &mut Shaper, text: &str) {
555        if !self.overlay_clusters.contains_key(text) {
556            let shaping = shaper.shape_cluster(
557                &self.stack,
558                self.font_size_physical_px,
559                FontStyle::REGULAR,
560                text,
561            );
562            let font_id = saturated_face_id(shaping.font_id);
563            let glyphs = shaping
564                .glyphs
565                .iter()
566                .map(|g| ClusterGlyph {
567                    glyph_id: g.glyph_id,
568                    font_id,
569                    advance_px: g.advance_px,
570                    x_offset_px: g.x_offset_px,
571                    y_offset_px: g.y_offset_px,
572                })
573                .collect();
574            self.overlay_clusters.insert(text.to_owned(), glyphs);
575        }
576        let cell_height = self.metrics.height;
577        let primes: Vec<(GlyphId, u16)> = self.overlay_clusters[text]
578            .iter()
579            .map(|g| (g.glyph_id, g.font_id))
580            .collect();
581        for (glyph_id, font_id) in primes {
582            let key = SizingKey::default().with_font_id(usize::from(font_id));
583            self.ensure_glyph_id(glyph_id, cell_height, key);
584        }
585    }
586
587    /// Dropping a `None` entry cannot change a frame: [`AtlasView`]
588    /// lookups read a missing key and a blank one alike as no slot.
589    fn insert_slot(&mut self, key: (SlotKey, u32, SizingKey), slot: Option<GlyphSlot>) {
590        if slot.is_none() {
591            if self.blank_slots >= Self::BLANK_SLOT_MAX {
592                self.slots.retain(|_, s| s.is_some());
593                self.blank_slots = 0;
594            }
595            self.blank_slots += 1;
596        } else {
597            self.placed_slots += 1;
598        }
599        self.slots.insert(key, slot);
600    }
601
602    #[must_use]
603    pub fn contains_glyph_id(
604        &self,
605        glyph_id: GlyphId,
606        cell_height: u32,
607        sizing: SizingKey,
608    ) -> bool {
609        self.contains_key(SlotKey::Glyph(glyph_id), cell_height, sizing)
610    }
611
612    pub fn drain_pending(&mut self) -> std::vec::Drain<'_, PendingUpload> {
613        self.pending.drain(..)
614    }
615
616    fn begin_populate(&mut self) {
617        self.primed.clear();
618        self.atlas_reset_during_walk = false;
619    }
620
621    /// True means the slots primed before the reset are gone; the
622    /// renderer walks once more (`Renderer::render`).
623    #[must_use]
624    pub const fn atlas_was_reset(&self) -> bool {
625        self.atlas_reset_during_walk
626    }
627
628    /// Counts every recycle, across walks; a new index restarts at zero.
629    #[must_use]
630    pub const fn atlas_resets(&self) -> u64 {
631        self.atlas_resets
632    }
633
634    /// Deduplicated by the cache key, not the char: sized variants
635    /// share a base glyph but land distinct entries.
636    fn prime_char(&mut self, glyph: char, cell_height: u32, sizing: SizingKey) {
637        if self.primed.insert((glyph, sizing)) {
638            self.ensure(glyph, cell_height, sizing);
639        }
640    }
641
642    pub fn populate(&mut self, screen: &ScreenBuffer) {
643        self.begin_populate();
644        for r in 0..screen.rows() {
645            self.populate_row(screen, r);
646        }
647    }
648
649    fn populate_row(&mut self, screen: &ScreenBuffer, r: u16) {
650        let cell_height = self.metrics.height;
651        for c in 0..screen.cols() {
652            let Some(cell) = screen.cell(r, c) else {
653                continue;
654            };
655            // Clusters are skipped: the shaping walk's cluster arm primes them,
656            // so a per-char slot primed here would never be read
657            // (docs/explanation/data-model/grid-and-cells.md "Cluster interning").
658            let glyph = match &cell.grapheme {
659                Grapheme::Empty
660                | Grapheme::Spacer
661                | Grapheme::SizedSpacer
662                | Grapheme::Cluster(_) => continue,
663                Grapheme::Ascii(b) => *b as char,
664                Grapheme::Char(c) => *c,
665            };
666            let sizing = screen
667                .cell_sizing(r, c)
668                .map(|s| sizing_key_from(*s))
669                .unwrap_or_default()
670                .with_style(font_style_of(screen.style(cell.style).flags));
671            self.prime_char(glyph, cell_height, sizing);
672        }
673    }
674
675    /// Resets the atlas and retries once when the shelf packer is full.
676    /// Returns `None` for blank glyphs or glyphs that cannot fit even an empty atlas,
677    /// degrading to a negative slot instead of looping infinitely.
678    fn allocate(&mut self, bitmap: GlyphBitmap) -> Option<GlyphSlot> {
679        match self.try_allocate(bitmap) {
680            AllocOutcome::Placed(slot) => Some(slot),
681            AllocOutcome::Blank => None,
682            AllocOutcome::Full(bitmap) => {
683                self.reset_atlas_full();
684                match self.try_allocate(bitmap) {
685                    AllocOutcome::Placed(slot) => Some(slot),
686                    AllocOutcome::Blank | AllocOutcome::Full(_) => None,
687                }
688            }
689        }
690    }
691
692    /// Both sheets reset together even when only one overflowed, so no
693    /// orphaned allocation leaks space across repeated resets. `shape`
694    /// and `shaped_runs` are position-independent and stay warm. The
695    /// walk's dedup set goes too: it records "already primed this walk",
696    /// which the reset just made false for every entry.
697    fn reset_atlas_full(&mut self) {
698        self.atlas.reset();
699        self.color_atlas.reset();
700        self.slots.clear();
701        self.blank_slots = 0;
702        self.placed_slots = 0;
703        self.pending.clear();
704        self.primed.clear();
705        self.atlas_reset_during_walk = true;
706        self.atlas_resets += 1;
707        self.walked = None;
708    }
709
710    fn try_allocate(&mut self, bitmap: GlyphBitmap) -> AllocOutcome {
711        if bitmap.is_blank() || bitmap.width() == 0 || bitmap.height() == 0 {
712            return AllocOutcome::Blank;
713        }
714        if self.placed_slots >= Self::PLACED_SLOT_MAX {
715            return AllocOutcome::Full(bitmap);
716        }
717        let (width, height) = (bitmap.width(), bitmap.height());
718        let offset_px = [bitmap.left, bitmap.top];
719        let is_color = bitmap.is_color();
720        let sheet = if is_color {
721            &mut self.color_atlas
722        } else {
723            &mut self.atlas
724        };
725        let Some(alloc) = sheet.alloc(width, height) else {
726            return AllocOutcome::Full(bitmap);
727        };
728        let atlas_px = [alloc.x, alloc.y];
729        self.pending.push(PendingUpload {
730            alloc,
731            pixels: bitmap.into_pixels(),
732        });
733        AllocOutcome::Placed(GlyphSlot {
734            atlas_px,
735            size_px: [width, height],
736            offset_px,
737            is_color,
738        })
739    }
740}
741
742enum AllocOutcome {
743    Placed(GlyphSlot),
744    Blank,
745    /// Handed back so the caller can reset the atlas and retry.
746    Full(GlyphBitmap),
747}
748
749/// ASCII bytes are 1:1 with cells, so `cell_count` equals the length
750/// of the text written into the caller's buffer.
751struct ShapeRun {
752    start_col: u16,
753    cell_count: u16,
754    style: FontStyle,
755}
756
757/// Identifies eligible [`Grapheme::Ascii`] runs with default sizing and primary-face coverage.
758///
759/// Color and link boundaries do not end the run, preserving ligatures across syntax highlighting;
760/// `CellPainter` slices quads per cell instead. Bold and italic changes do end the run
761/// because they select a different face.
762fn next_shape_run<F>(
763    screen: &ScreenBuffer,
764    row: u16,
765    start_col: u16,
766    text: &mut String,
767    covers: F,
768) -> Option<ShapeRun>
769where
770    F: Fn(char, FontStyle) -> bool,
771{
772    text.clear();
773    let cols = screen.cols();
774    if start_col >= cols {
775        return None;
776    }
777    let first = screen.cell(row, start_col)?;
778    let (Grapheme::Ascii(b0), None) = (&first.grapheme, screen.cell_sizing(row, start_col)) else {
779        return None;
780    };
781    let style = font_style_of(screen.style(first.style).flags);
782    let c0 = *b0 as char;
783    if !covers(c0, style) {
784        return None;
785    }
786    text.push(c0);
787    let mut last_col = start_col;
788    for c in (start_col + 1)..cols {
789        let Some(cell) = screen.cell(row, c) else {
790            break;
791        };
792        let Grapheme::Ascii(b) = &cell.grapheme else {
793            break;
794        };
795        if screen.cell_sizing(row, c).is_some() {
796            break;
797        }
798        if font_style_of(screen.style(cell.style).flags) != style {
799            break;
800        }
801        let ch = *b as char;
802        if !covers(ch, style) {
803            break;
804        }
805        text.push(ch);
806        last_col = c;
807    }
808    let cell_count = last_col - start_col + 1;
809    Some(ShapeRun {
810        start_col,
811        cell_count,
812        style,
813    })
814}
815
816struct RunPlacement {
817    row: u16,
818    start_col: u16,
819    cell_count: u16,
820    style: FontStyle,
821}
822
823struct ClusterSite {
824    row: u16,
825    col: u16,
826    id: NonZeroU32,
827    style: FontStyle,
828    sizing: SizingKey,
829}
830
831fn apply_shaped_run(
832    frame_cells: &mut [ShapedCell],
833    index: &mut GlyphIndex,
834    cols: u16,
835    run: &RunPlacement,
836    shaped: &[ShapedGlyph],
837) {
838    let cell_height = index.cell_metrics().height;
839    let cell_width = index.cell_metrics().width as f32;
840    // `CellPainter` rebuilds this same key from the cell, so the style
841    // must ride it.
842    let sizing = SizingKey::default().with_style(run.style);
843    let row_base = usize::from(run.row) * usize::from(cols);
844    // Guard so a malformed shape result cannot land a primary past the
845    // run's right edge and corrupt neighbors.
846    let run_start_col = run.start_col;
847    let run_end_col_exclusive = run_start_col + run.cell_count;
848    // Processes clusters as units rather than individual glyphs. All pieces share
849    // `source_byte_start`, so mapping naively by source start would stack them on the
850    // leftmost cell instead of using intra-cluster pen positions.
851    let mut i = 0;
852    while i < shaped.len() {
853        let src_start = shaped[i].source_byte_start;
854        let src_len = shaped[i].source_byte_len;
855        let mut j = i + 1;
856        while j < shaped.len()
857            && shaped[j].source_byte_start == src_start
858            && shaped[j].source_byte_len == src_len
859        {
860            j += 1;
861        }
862        let cluster = &shaped[i..j];
863        i = j;
864        let cluster_col = run_start_col + (src_start as u16);
865        if cluster_col >= run_end_col_exclusive {
866            continue;
867        }
868        let span_cols = u16::try_from(src_len).unwrap_or(1).max(1);
869        // Clamp so a degenerate `source_byte_len = 0` cluster still
870        // terminates.
871        let last_col = (cluster_col + span_cols).min(run_end_col_exclusive);
872        let total_advance: f32 = cluster.iter().map(|g| g.advance_px).sum();
873        // A ligature wider than its source cells stays unshaped: its cells
874        // keep `None`, and the per-char path draws each char in its own
875        // cell. The slack of a pixel per cell absorbs the rounding of the
876        // primary advance into the cell width.
877        if total_advance > (cell_width + 1.0) * f32::from(span_cols) {
878            continue;
879        }
880        if let [g] = cluster {
881            index.ensure_glyph_id(g.glyph_id, cell_height, sizing);
882            if let Some(slot) = frame_cells.get_mut(row_base + usize::from(cluster_col)) {
883                *slot = ShapedCell::Primary {
884                    glyph_id: g.glyph_id,
885                    span_cols,
886                };
887            }
888            for col in (cluster_col + 1)..last_col {
889                if let Some(slot) = frame_cells.get_mut(row_base + usize::from(col)) {
890                    *slot = ShapedCell::Trailing;
891                }
892            }
893            continue;
894        }
895        // Divide the pen by the cluster's own per-cell advance, not the
896        // screen cell width: the two disagree by fractions of a pixel.
897        let per_cell_advance = total_advance / f32::from(span_cols);
898        // Trailing suppresses the per-char fallback, which would repaint
899        // the raw source chars under the substituted pieces.
900        for col in cluster_col..last_col {
901            if let Some(slot) = frame_cells.get_mut(row_base + usize::from(col)) {
902                *slot = ShapedCell::Trailing;
903            }
904        }
905        let mut pen = 0.0f32;
906        for g in cluster {
907            #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
908            let cell_offset = if per_cell_advance > 0.0 {
909                ((pen / per_cell_advance).round().max(0.0) as u16).min(span_cols - 1)
910            } else {
911                0
912            };
913            pen += g.advance_px;
914            let col = cluster_col + cell_offset;
915            if col >= last_col {
916                continue;
917            }
918            index.ensure_glyph_id(g.glyph_id, cell_height, sizing);
919            if let Some(slot) = frame_cells.get_mut(row_base + usize::from(col)) {
920                *slot = ShapedCell::Primary {
921                    glyph_id: g.glyph_id,
922                    span_cols: 1,
923                };
924            }
925        }
926    }
927}
928
929impl AtlasView for GlyphIndex {
930    fn slot(&self, glyph: char, cell_height_px: u32, sizing: SizingKey) -> Option<GlyphSlot> {
931        self.lookup(SlotKey::Char(glyph), cell_height_px, sizing)
932    }
933
934    fn glyph_id_slot(
935        &self,
936        glyph_id: GlyphId,
937        cell_height_px: u32,
938        sizing: SizingKey,
939    ) -> Option<GlyphSlot> {
940        self.lookup(SlotKey::Glyph(glyph_id), cell_height_px, sizing)
941    }
942
943    fn fitted_glyph_id_slot(
944        &self,
945        glyph_id: GlyphId,
946        px: u16,
947        cell_height_px: u32,
948        sizing: SizingKey,
949    ) -> Option<GlyphSlot> {
950        self.lookup(SlotKey::FittedGlyph(glyph_id, px), cell_height_px, sizing)
951    }
952
953    fn overlay_cluster(&self, text: &str) -> Option<&[ClusterGlyph]> {
954        self.overlay_clusters.get(text).map(Vec::as_slice)
955    }
956
957    fn sheet_side(&self, sheet: AtlasKind) -> u32 {
958        match sheet {
959            AtlasKind::Mono => self.atlas.width(),
960            AtlasKind::Color => self.color_atlas.width(),
961        }
962    }
963}
964
965#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
966pub enum ShapedCell {
967    /// No shape data: the renderer falls back to the per-char path.
968    #[default]
969    None,
970    /// Leftmost cell of a shaped cluster; trailing cells carry
971    /// [`ShapedCell::Trailing`].
972    Primary { glyph_id: GlyphId, span_cols: u16 },
973    /// The fg pass skips this cell (the primary's quad covers it); the
974    /// bg quad still draws.
975    Trailing,
976    /// Leftmost cell of a composited cluster; its glyphs live in the
977    /// frame's pool at `[start .. start + len]`. A wide base's second
978    /// cell is a [`Grapheme::Spacer`] the emission loop already skips,
979    /// so it needs no [`Self::Trailing`]. A non-zero `fit_px` is the
980    /// size its glyphs were shrunk to so the cluster fits its cells.
981    Cluster { start: u32, len: u16, fit_px: u16 },
982}
983
984fn saturated_face_id(font_id: usize) -> u16 {
985    u16::try_from(font_id.min(FACE_ID_SATURATED)).unwrap_or(u16::MAX)
986}
987
988/// Produced while the [`ShapeFrame`] is built and read back in
989/// the cell painter, so priming and emission cannot drift.
990#[derive(Debug, Clone, Copy, PartialEq)]
991pub struct ClusterGlyph {
992    pub glyph_id: GlyphId,
993    /// [`SizingKey::with_font_id`] handle, already saturated to
994    /// [`FACE_ID_SATURATED`]; priming and emission both rebuild the key
995    /// with it.
996    pub font_id: u16,
997    /// Horizontal pen advance in pixels (≈0 for a combining mark).
998    pub advance_px: f32,
999    pub x_offset_px: f32,
1000    /// GPOS y offset in pixels (font y-up: positive raises the glyph).
1001    pub y_offset_px: f32,
1002}
1003
1004/// Shape result indexed by cell, kept across frames: a walk rewrites
1005/// only the rows it visits. Empty means the shaper never touched the
1006/// screen: every lookup yields `ShapedCell::None`.
1007#[derive(Debug, Clone, Default)]
1008pub struct ShapeFrame {
1009    cells: Vec<ShapedCell>,
1010    cols: u16,
1011    cluster_glyphs: Vec<ClusterGlyph>,
1012    /// Pool entries some cell still points at; the rest belong to rows
1013    /// walked again since, and [`Self::compact`] drops them.
1014    live_cluster_glyphs: usize,
1015}
1016
1017impl ShapeFrame {
1018    #[must_use]
1019    pub const fn empty() -> Self {
1020        Self {
1021            cells: Vec::new(),
1022            cols: 0,
1023            cluster_glyphs: Vec::new(),
1024            live_cluster_glyphs: 0,
1025        }
1026    }
1027
1028    fn begin(&mut self, rows: u16, cols: u16) {
1029        let cell_count = usize::from(rows) * usize::from(cols);
1030        self.cells.clear();
1031        self.cells.resize(cell_count, ShapedCell::None);
1032        self.cols = cols;
1033        self.cluster_glyphs.clear();
1034        self.live_cluster_glyphs = 0;
1035    }
1036
1037    fn reset_empty(&mut self) {
1038        self.cells.clear();
1039        self.cols = 0;
1040        self.cluster_glyphs.clear();
1041        self.live_cluster_glyphs = 0;
1042    }
1043
1044    fn clear_row(&mut self, row: u16) {
1045        let cols = usize::from(self.cols);
1046        let start = usize::from(row) * cols;
1047        let Some(cells) = self.cells.get_mut(start..start + cols) else {
1048            return;
1049        };
1050        for cell in cells {
1051            if let ShapedCell::Cluster { len, .. } = *cell {
1052                self.live_cluster_glyphs -= usize::from(len);
1053            }
1054            *cell = ShapedCell::None;
1055        }
1056    }
1057
1058    /// Rewrites the pool once the rows walked again left it mostly
1059    /// unreferenced, so it stays within a small multiple of what the
1060    /// screen shows.
1061    fn compact(&mut self) {
1062        if self.cluster_glyphs.len() <= (self.live_cluster_glyphs * 2).max(4096) {
1063            return;
1064        }
1065        let mut pool = Vec::with_capacity(self.live_cluster_glyphs);
1066        for cell in &mut self.cells {
1067            if let ShapedCell::Cluster { start, len, .. } = cell {
1068                let from = *start as usize;
1069                *start = u32::try_from(pool.len()).unwrap_or(u32::MAX);
1070                pool.extend_from_slice(&self.cluster_glyphs[from..from + usize::from(*len)]);
1071            }
1072        }
1073        self.cluster_glyphs = pool;
1074    }
1075
1076    /// The quad-slicing tests in `painter.rs` build frames directly
1077    /// because the full pipeline needs a real ligature font.
1078    #[cfg(any(test, feature = "testing"))]
1079    #[doc(hidden)]
1080    #[must_use]
1081    pub const fn from_cells(cells: Vec<ShapedCell>, cols: u16) -> Self {
1082        Self {
1083            cells,
1084            cols,
1085            cluster_glyphs: Vec::new(),
1086            live_cluster_glyphs: 0,
1087        }
1088    }
1089
1090    #[cfg(any(test, feature = "testing"))]
1091    #[doc(hidden)]
1092    #[must_use]
1093    pub const fn from_cluster_cells(
1094        cells: Vec<ShapedCell>,
1095        cols: u16,
1096        cluster_glyphs: Vec<ClusterGlyph>,
1097    ) -> Self {
1098        let live_cluster_glyphs = cluster_glyphs.len();
1099        Self {
1100            cells,
1101            cols,
1102            cluster_glyphs,
1103            live_cluster_glyphs,
1104        }
1105    }
1106
1107    #[must_use]
1108    pub fn cluster_slice(&self, start: u32, len: u16) -> &[ClusterGlyph] {
1109        let s = start as usize;
1110        let e = s + len as usize;
1111        self.cluster_glyphs.get(s..e).unwrap_or(&[])
1112    }
1113
1114    #[must_use]
1115    pub fn cell_at(&self, row: u16, col: u16) -> ShapedCell {
1116        if self.cells.is_empty() || self.cols == 0 || col >= self.cols {
1117            return ShapedCell::None;
1118        }
1119        let idx = usize::from(row) * usize::from(self.cols) + usize::from(col);
1120        self.cells.get(idx).copied().unwrap_or(ShapedCell::None)
1121    }
1122
1123    #[must_use]
1124    pub fn cells(&self) -> &[ShapedCell] {
1125        &self.cells
1126    }
1127
1128    #[must_use]
1129    pub const fn cols(&self) -> u16 {
1130        self.cols
1131    }
1132}
1133
1134/// Scratch and bookkeeping for the grid walk, kept apart from the GPU
1135/// resources so tests drive it against a bare [`GlyphIndex`].
1136#[derive(Default)]
1137pub struct GridWalker {
1138    run_text: String,
1139    run_glyphs: Vec<ShapedGlyph>,
1140    rows: Vec<u16>,
1141}
1142
1143impl GridWalker {
1144    /// Walks only the rows `screen` marked dirty while `frame` and the
1145    /// index's slots still hold everything the last walk put there, and
1146    /// every row otherwise. With empty `features` and no interned
1147    /// clusters this avoids shaping overhead. `shaper` is borrowed
1148    /// mutably so Swash's feature cache on `ShapeContext` stays resident.
1149    pub fn walk(
1150        &mut self,
1151        index: &mut GlyphIndex,
1152        screen: &ScreenBuffer,
1153        shaper: &mut Shaper,
1154        features: &[String],
1155        frame: &mut ShapeFrame,
1156    ) {
1157        let (rows, cols) = (screen.rows(), screen.cols());
1158        // `cluster_count` is an O(1) table-size probe; it can be non-zero
1159        // for a cluster that has scrolled off, which is harmless.
1160        let shaped = !features.is_empty() || screen.cluster_count() > 0;
1161        let key = WalkKey { rows, cols, shaped };
1162        let full = index.walked != Some(key);
1163        self.rows.clear();
1164        if full {
1165            self.rows.extend(0..rows);
1166        } else {
1167            let dirty = screen.damage().dirty_rows();
1168            self.rows
1169                .extend(dirty.filter_map(|r| u16::try_from(r).ok()));
1170        }
1171        index.walked = None;
1172        index.begin_populate();
1173        let walk = std::mem::take(&mut self.rows);
1174        if shaped {
1175            if full {
1176                frame.begin(rows, cols);
1177            }
1178            let stack = index.stack().clone();
1179            for &r in &walk {
1180                frame.clear_row(r);
1181                self.walk_shaped_row(index, screen, shaper, features, &stack, r, frame);
1182            }
1183            frame.compact();
1184        } else {
1185            frame.reset_empty();
1186            for &r in &walk {
1187                index.populate_row(screen, r);
1188            }
1189        }
1190        self.rows = walk;
1191        if !index.atlas_was_reset() {
1192            index.walked = Some(key);
1193        }
1194    }
1195
1196    #[expect(
1197        clippy::too_many_arguments,
1198        reason = "the walk's borrowed inputs, split so the index and frame stay separately mutable"
1199    )]
1200    fn walk_shaped_row(
1201        &mut self,
1202        index: &mut GlyphIndex,
1203        screen: &ScreenBuffer,
1204        shaper: &mut Shaper,
1205        features: &[String],
1206        stack: &FontStack,
1207        r: u16,
1208        frame: &mut ShapeFrame,
1209    ) {
1210        let cols = screen.cols();
1211        let mut col = 0u16;
1212        while col < cols {
1213            if !features.is_empty()
1214                && let Some(run) = next_shape_run(screen, r, col, &mut self.run_text, |c, style| {
1215                    stack.primary_covers(c, style)
1216                })
1217            {
1218                col = run.start_col + run.cell_count;
1219                self.shape_and_apply_run(
1220                    index,
1221                    shaper,
1222                    features,
1223                    cols,
1224                    &RunPlacement {
1225                        row: r,
1226                        start_col: run.start_col,
1227                        cell_count: run.cell_count,
1228                        style: run.style,
1229                    },
1230                    frame,
1231                );
1232                continue;
1233            }
1234            shape_cell(index, screen, shaper, stack, r, col, frame);
1235            col += 1;
1236        }
1237    }
1238
1239    /// The run's source chars are primed through the per-char path too:
1240    /// shaping can degenerately yield nothing, leaving the cells
1241    /// [`ShapedCell::None`] for the emission fallback.
1242    fn shape_and_apply_run(
1243        &mut self,
1244        index: &mut GlyphIndex,
1245        shaper: &mut Shaper,
1246        features: &[String],
1247        cols: u16,
1248        run: &RunPlacement,
1249        frame: &mut ShapeFrame,
1250    ) {
1251        if self.run_text.is_empty() {
1252            return;
1253        }
1254        let cell_height = index.cell_metrics().height;
1255        let sizing = SizingKey::default().with_style(run.style);
1256        for ch in self.run_text.chars() {
1257            index.prime_char(ch, cell_height, sizing);
1258        }
1259        index.shape_run_cached(
1260            shaper,
1261            features,
1262            run.style,
1263            &self.run_text,
1264            &mut self.run_glyphs,
1265        );
1266        apply_shaped_run(&mut frame.cells, index, cols, run, &self.run_glyphs);
1267    }
1268}
1269
1270fn shape_cell(
1271    index: &mut GlyphIndex,
1272    screen: &ScreenBuffer,
1273    shaper: &mut Shaper,
1274    stack: &FontStack,
1275    row: u16,
1276    col: u16,
1277    frame: &mut ShapeFrame,
1278) {
1279    let Some(cell) = screen.cell(row, col) else {
1280        return;
1281    };
1282    let glyph = match cell.grapheme {
1283        Grapheme::Empty | Grapheme::Spacer | Grapheme::SizedSpacer => return,
1284        Grapheme::Cluster(_) => None,
1285        Grapheme::Ascii(b) => Some(b as char),
1286        Grapheme::Char(c) => Some(c),
1287    };
1288    let style = font_style_of(screen.style(cell.style).flags);
1289    let sizing = screen
1290        .cell_sizing(row, col)
1291        .map(|s| sizing_key_from(*s))
1292        .unwrap_or_default()
1293        .with_style(style);
1294    let cell_height = index.cell_metrics().height;
1295    if let Some(glyph) = glyph {
1296        index.prime_char(glyph, cell_height, sizing);
1297        return;
1298    }
1299    let Grapheme::Cluster(id) = cell.grapheme else {
1300        return;
1301    };
1302    composite_cluster(
1303        index,
1304        screen,
1305        shaper,
1306        stack,
1307        &ClusterSite {
1308            row,
1309            col,
1310            id,
1311            style,
1312            sizing,
1313        },
1314        frame,
1315    );
1316}
1317
1318/// Primes a cluster's glyphs with a sized key for scaled rasterization.
1319///
1320/// If shaping yields nothing, primes the base char under the same key so the fallback
1321/// resolves a correctly scaled bitmap.
1322fn composite_cluster(
1323    index: &mut GlyphIndex,
1324    screen: &ScreenBuffer,
1325    shaper: &mut Shaper,
1326    stack: &FontStack,
1327    site: &ClusterSite,
1328    frame: &mut ShapeFrame,
1329) {
1330    let Some(text) = screen.cluster_str(site.id) else {
1331        return;
1332    };
1333    let base = text.chars().next();
1334    // The image pass paints the placeholder; compositing it would
1335    // draw tofu under the image.
1336    if base == Some(felis_protocol::kitty_graphics::placeholder::PLACEHOLDER) {
1337        return;
1338    }
1339    let cell_height = index.cell_metrics().height;
1340    let px = index.font_size_physical_px();
1341    let shaping = shaper.shape_cluster(stack, px, site.style, text);
1342    if shaping.glyphs.is_empty() {
1343        if let Some(bc) = base {
1344            index.ensure(bc, cell_height, site.sizing);
1345        }
1346        return;
1347    }
1348    let sized_key = site.sizing.with_font_id(shaping.font_id);
1349    let font_id = saturated_face_id(shaping.font_id);
1350    let cols = cluster_block_cols(screen, site.row, site.col);
1351    let fit_px = index.cluster_fit_px(text, &shaping.glyphs, cols, sized_key);
1352    // Emission scales the pen by the sizing's glyph scale; pre-scaling
1353    // the pool by the fit keeps one formula for both.
1354    let pen_ratio = fit_px.map_or(1.0, |px| {
1355        f32::from(px) / (index.font_size_physical_px() * sized_key.effective_scale())
1356    });
1357    let start = u32::try_from(frame.cluster_glyphs.len()).unwrap_or(u32::MAX);
1358    let mut len = 0u16;
1359    for g in &shaping.glyphs {
1360        match fit_px {
1361            Some(px) => index.ensure_fitted_glyph_id(g.glyph_id, px, cell_height, sized_key),
1362            None => index.ensure_glyph_id(g.glyph_id, cell_height, sized_key),
1363        };
1364        frame.cluster_glyphs.push(ClusterGlyph {
1365            glyph_id: g.glyph_id,
1366            font_id,
1367            advance_px: g.advance_px * pen_ratio,
1368            x_offset_px: g.x_offset_px * pen_ratio,
1369            y_offset_px: g.y_offset_px * pen_ratio,
1370        });
1371        len = len.saturating_add(1);
1372    }
1373    let idx = usize::from(site.row) * usize::from(frame.cols) + usize::from(site.col);
1374    if let Some(slot) = frame.cells.get_mut(idx) {
1375        *slot = ShapedCell::Cluster {
1376            start,
1377            len,
1378            fit_px: fit_px.unwrap_or(0),
1379        };
1380        frame.live_cluster_glyphs += usize::from(len);
1381    }
1382}
1383
1384#[cfg(test)]
1385mod tests {
1386    #![allow(clippy::float_cmp)]
1387    #![cfg_attr(
1388        windows,
1389        allow(
1390            dead_code,
1391            reason = "the pinned-font tests are compiled out on Windows"
1392        )
1393    )]
1394    use felis_grid::Grid;
1395
1396    use std::sync::Arc;
1397
1398    use felis_shaping::Font;
1399    use felis_vt::Parser;
1400
1401    use super::*;
1402
1403    fn nz(v: u32) -> NonZeroU32 {
1404        NonZeroU32::new(v).unwrap()
1405    }
1406
1407    fn stack() -> FontStack {
1408        FontStack::new(Arc::new(Font::load_test_font()))
1409    }
1410
1411    #[cfg(not(windows))]
1412    #[test]
1413    fn an_overlay_cluster_is_shaped_and_its_glyphs_primed() {
1414        let mut idx = GlyphIndex::new(stack(), 14.0, nz(256));
1415        let mut shaper = Shaper::new();
1416        assert!(idx.overlay_cluster("e\u{301}").is_none());
1417        idx.ensure_overlay_cluster(&mut shaper, "e\u{301}");
1418        let glyphs = idx.overlay_cluster("e\u{301}").expect("shaped").to_vec();
1419        assert_ne!(glyphs.len(), 0);
1420        let h = idx.cell_metrics().height;
1421        let base = &glyphs[0];
1422        let key = SizingKey::default().with_font_id(usize::from(base.font_id));
1423        assert!(
1424            idx.glyph_id_slot(base.glyph_id, h, key).is_some(),
1425            "base primed"
1426        );
1427    }
1428
1429    #[cfg(not(windows))]
1430    #[test]
1431    fn overlay_shapes_survive_the_frame_that_primed_them() {
1432        let mut idx = GlyphIndex::new(stack(), 14.0, nz(256));
1433        let mut shaper = Shaper::new();
1434        let clusters: Vec<String> = ('a'..='z')
1435            .flat_map(|base| ('\u{300}'..='\u{30F}').map(move |mark| format!("{base}{mark}")))
1436            .take(GlyphIndex::OVERLAY_CLUSTER_MAX + 50)
1437            .collect();
1438        idx.trim_overlay_clusters();
1439        for cluster in &clusters {
1440            idx.ensure_overlay_cluster(&mut shaper, cluster);
1441        }
1442        assert!(
1443            clusters.iter().all(|c| idx.overlay_cluster(c).is_some()),
1444            "a label past the bound keeps every cluster for the frame it paints"
1445        );
1446        idx.trim_overlay_clusters();
1447        assert!(
1448            idx.overlay_cluster(&clusters[0]).is_none(),
1449            "bounded between frames"
1450        );
1451    }
1452
1453    #[cfg(not(windows))]
1454    #[test]
1455    fn ensure_records_a_slot_and_pending_upload_for_a_printable_glyph() {
1456        let mut idx = GlyphIndex::new(stack(), 14.0, nz(256));
1457        let h = idx.cell_metrics().height;
1458        let sk = SizingKey::default();
1459        let did_work = idx.ensure('A', h, sk);
1460        assert!(did_work, "first ensure must rasterize + allocate");
1461        assert!(idx.contains('A', h, sk));
1462        let slot = idx.slot('A', h, sk).expect("A produced a slot");
1463        assert!(slot.atlas_px[0] + slot.size_px[0] <= idx.sheet_side(AtlasKind::Mono));
1464        assert!(slot.atlas_px[1] + slot.size_px[1] <= idx.sheet_side(AtlasKind::Mono));
1465        assert!(slot.size_px[0] > 0 && slot.size_px[1] > 0);
1466        let mut iter = idx.drain_pending();
1467        let first = iter.next().expect("one upload queued");
1468        assert!(iter.next().is_none(), "exactly one upload queued");
1469        assert_eq!(
1470            first.pixels.as_bytes().len(),
1471            (first.alloc.width * first.alloc.height) as usize,
1472            "pixel buffer length matches the allocation"
1473        );
1474    }
1475
1476    #[cfg(not(windows))]
1477    #[test]
1478    fn ensure_is_idempotent_for_the_same_key() {
1479        let mut idx = GlyphIndex::new(stack(), 14.0, nz(256));
1480        let h = idx.cell_metrics().height;
1481        let sk = SizingKey::default();
1482        assert!(idx.ensure('B', h, sk));
1483        assert!(!idx.ensure('B', h, sk), "second ensure is a hit");
1484        assert_eq!(
1485            idx.drain_pending().count(),
1486            1,
1487            "second ensure must not enqueue another upload"
1488        );
1489    }
1490
1491    #[cfg(not(windows))]
1492    #[test]
1493    fn whitespace_caches_a_negative_entry_with_no_upload() {
1494        let mut idx = GlyphIndex::new(stack(), 14.0, nz(256));
1495        let h = idx.cell_metrics().height;
1496        let sk = SizingKey::default();
1497        idx.ensure(' ', h, sk);
1498        assert!(idx.contains(' ', h, sk));
1499        assert_eq!(idx.slot(' ', h, sk), None, "space resolves to no slot");
1500        assert!(
1501            idx.drain_pending().next().is_none(),
1502            "blank glyphs do not queue uploads"
1503        );
1504    }
1505
1506    #[cfg(not(windows))]
1507    #[test]
1508    fn populate_walks_the_grid_and_caches_unique_glyphs_once() {
1509        let mut screen = Grid::new(2, 5);
1510        let mut parser = Parser::new();
1511        parser.advance(&mut screen, b"hello\r\nhello");
1512        let mut idx = GlyphIndex::new(stack(), 14.0, nz(256));
1513        idx.populate(screen.screen());
1514        let h = idx.cell_metrics().height;
1515        let sk = SizingKey::default();
1516        for c in ['h', 'e', 'l', 'o'] {
1517            assert!(idx.contains(c, h, sk), "missing cache entry for {c:?}");
1518            assert!(idx.slot(c, h, sk).is_some(), "missing slot for {c:?}");
1519        }
1520        let pending_count = idx.drain_pending().count();
1521        assert_eq!(pending_count, 4);
1522    }
1523
1524    #[cfg(not(windows))]
1525    #[test]
1526    fn sized_variant_takes_a_distinct_atlas_slot_from_default() {
1527        // An OSC 66 `s=2` 'A' must not share a slot with the default 'A'.
1528        let mut idx = GlyphIndex::new(stack(), 14.0, nz(512));
1529        let h = idx.cell_metrics().height;
1530        let default_sk = SizingKey::default();
1531        let scale_2_sk = SizingKey::new(2, 0, 0, 0, 0, 0);
1532        assert!(idx.ensure('A', h, default_sk), "first ensure must work");
1533        assert!(
1534            idx.ensure('A', h, scale_2_sk),
1535            "sized variant must rasterize separately"
1536        );
1537        let small = idx.slot('A', h, default_sk).expect("default slot");
1538        let large = idx.slot('A', h, scale_2_sk).expect("scaled slot");
1539        assert_ne!(
1540            (small.atlas_px, small.size_px),
1541            (large.atlas_px, large.size_px),
1542            "sized variant must occupy a different atlas rect"
1543        );
1544        // Generous slack: swash's hinted metrics are not exactly 2×.
1545        assert!(
1546            large.size_px[1] > small.size_px[1],
1547            "scaled bitmap height ({}) must exceed default ({})",
1548            large.size_px[1],
1549            small.size_px[1]
1550        );
1551    }
1552
1553    #[cfg(not(windows))]
1554    #[test]
1555    fn a_full_atlas_resets_and_keeps_placing_glyphs() {
1556        // A small atlas so one page of distinct glyphs overflows; the
1557        // index must reset and keep handing out real slots.
1558        let mut idx = GlyphIndex::new(stack(), 14.0, nz(48));
1559        let h = idx.cell_metrics().height;
1560        let sk = SizingKey::default();
1561        let mut reset_seen = false;
1562        let mut placed_after_reset = false;
1563        let mut prev_len = idx.slots.len();
1564        for c in ('!'..='~').chain('\u{a1}'..='\u{17f}') {
1565            idx.ensure(c, h, sk);
1566            let len = idx.slots.len();
1567            if len < prev_len {
1568                reset_seen = true;
1569                assert!(
1570                    idx.slot(c, h, sk).is_some(),
1571                    "post-reset glyph {c:?} must get a real slot",
1572                );
1573                placed_after_reset = true;
1574            }
1575            prev_len = len;
1576        }
1577        assert!(
1578            reset_seen,
1579            "a page of glyphs must overflow the tiny atlas and reset it",
1580        );
1581        assert!(
1582            placed_after_reset,
1583            "allocations must resume after a reset instead of turning into permanent misses",
1584        );
1585    }
1586
1587    /// The OSC 66 sizings the VT parser admits, each a distinct key.
1588    fn osc66_sizings() -> impl Iterator<Item = SizingKey> {
1589        (1..=7u8).flat_map(|s| {
1590            (0..=7u8).flat_map(move |w| {
1591                (0..=15u8).flat_map(move |d| {
1592                    (0..d.max(1)).flat_map(move |n| {
1593                        (0..=2u8).flat_map(move |v| {
1594                            (0..=2u8).map(move |h| SizingKey::new(s, w, n, d, v, h))
1595                        })
1596                    })
1597                })
1598            })
1599        })
1600    }
1601
1602    #[cfg(not(windows))]
1603    #[test]
1604    fn distinct_blank_keys_keep_the_slot_map_under_the_blank_cap() {
1605        let mut idx = GlyphIndex::new(stack(), 14.0, nz(256));
1606        let h = idx.cell_metrics().height;
1607        for sk in osc66_sizings().take(3 * GlyphIndex::BLANK_SLOT_MAX) {
1608            idx.ensure(' ', h, sk);
1609            assert_eq!(idx.slot(' ', h, sk), None, "a sized space stays blank");
1610            assert!(
1611                idx.slots.len() <= GlyphIndex::BLANK_SLOT_MAX,
1612                "{} slot entries after blank key {sk:?}",
1613                idx.slots.len(),
1614            );
1615        }
1616    }
1617
1618    #[cfg(not(windows))]
1619    #[test]
1620    fn dropping_blank_entries_keeps_placed_slots_and_the_atlas() {
1621        let mut idx = GlyphIndex::new(stack(), 14.0, nz(256));
1622        let h = idx.cell_metrics().height;
1623        let sk = SizingKey::default();
1624        idx.ensure('A', h, sk);
1625        let placed = idx.slot('A', h, sk).expect("A is placed");
1626        let _ = idx.drain_pending().count();
1627        for blank in osc66_sizings().take(2 * GlyphIndex::BLANK_SLOT_MAX) {
1628            idx.ensure(' ', h, blank);
1629        }
1630        assert_eq!(
1631            idx.atlas_resets(),
1632            0,
1633            "blank entries never recycle the atlas"
1634        );
1635        assert!(!idx.ensure('A', h, sk), "A is still resident");
1636        assert_eq!(idx.slot('A', h, sk), Some(placed));
1637        assert!(idx.drain_pending().next().is_none(), "nothing re-uploads");
1638    }
1639
1640    fn placed_slot_count(idx: &GlyphIndex) -> usize {
1641        idx.slots.values().filter(|s| s.is_some()).count()
1642    }
1643
1644    #[cfg(not(windows))]
1645    #[test]
1646    fn distinct_tiny_placed_glyphs_recycle_the_atlas_at_the_placed_cap() {
1647        let mut idx = GlyphIndex::new(stack(), 14.0, nz(GLYPH_ATLAS_SIDE));
1648        let h = idx.cell_metrics().height;
1649        let fractional = osc66_sizings()
1650            .filter(|sk| sk.scale() == 1 && sk.frac_num() > 0 && sk.frac_num() < sk.frac_den());
1651        let keys = fractional.flat_map(|sk| ('A'..='Z').map(move |c| (c, sk)));
1652        let mut placed = 0;
1653        let mut last = None;
1654        for (c, sk) in keys {
1655            let before = idx.placed_slots;
1656            let resets = idx.atlas_resets();
1657            idx.ensure(c, h, sk);
1658            if idx.atlas_resets() != resets {
1659                assert_eq!(
1660                    before,
1661                    GlyphIndex::PLACED_SLOT_MAX,
1662                    "the cap, not the sheet, recycles tiny glyphs",
1663                );
1664            }
1665            assert!(idx.placed_slots <= GlyphIndex::PLACED_SLOT_MAX);
1666            if let Some(slot) = idx.slot(c, h, sk) {
1667                placed += 1;
1668                last = Some((c, sk, slot));
1669                if placed == GlyphIndex::PLACED_SLOT_MAX + 1 {
1670                    break;
1671                }
1672            }
1673        }
1674        assert_eq!(placed, GlyphIndex::PLACED_SLOT_MAX + 1);
1675        assert_eq!(idx.atlas_resets(), 1);
1676        assert_eq!(placed_slot_count(&idx), idx.placed_slots);
1677        let (c, sk, slot) = last.expect("a glyph was placed");
1678        assert!(
1679            !idx.ensure(c, h, sk),
1680            "the newest glyph survives the recycle"
1681        );
1682        assert_eq!(idx.slot(c, h, sk), Some(slot));
1683    }
1684
1685    /// Overlay text (a chrome bar's label, the pre-edit) is primed
1686    /// after the grid walk, so its own overflow drops the grid's fresh
1687    /// slots; the renderer reads the same reported reset to walk again.
1688    #[cfg(not(windows))]
1689    #[test]
1690    fn an_overlay_char_that_overflows_after_the_walk_reports_the_reset() {
1691        let mut idx = GlyphIndex::new(stack(), 14.0, nz(32));
1692        let h = idx.cell_metrics().height;
1693        let sk = SizingKey::default();
1694
1695        let text = "uvwxyz";
1696        let mut screen = Grid::new(1, 8);
1697        Parser::new().advance(&mut screen, text.as_bytes());
1698        idx.populate(screen.screen());
1699        assert!(
1700            !idx.atlas_was_reset(),
1701            "the walk of one short row must fit, isolating the overlay overflow",
1702        );
1703
1704        // Overlay text never goes through `populate`, so it primes on
1705        // top of a finished walk exactly as `populate_chars` does.
1706        for c in '\u{a1}'..='\u{2ff}' {
1707            if idx.atlas_was_reset() {
1708                break;
1709            }
1710            idx.ensure(c, h, sk);
1711        }
1712        assert!(
1713            idx.atlas_was_reset(),
1714            "priming overlay text after the walk must report the reset",
1715        );
1716        assert!(
1717            text.chars().any(|c| idx.slot(c, h, sk).is_none()),
1718            "the grid slots handed out before the overlay's reset are gone",
1719        );
1720    }
1721
1722    /// A reset partway through a walk drops slots already handed out,
1723    /// and rendering is damage-driven, so nothing would repaint those
1724    /// cells; the reported reset drives a second walk.
1725    #[cfg(not(windows))]
1726    #[test]
1727    fn a_mid_walk_reset_is_reported_and_a_second_walk_reslots_the_grid() {
1728        let mut idx = GlyphIndex::new(stack(), 14.0, nz(48));
1729        let h = idx.cell_metrics().height;
1730        let sk = SizingKey::default();
1731
1732        // Measured, not assumed: it depends on the system face's bitmap
1733        // sizes.
1734        let capacity = {
1735            let mut probe = GlyphIndex::new(stack(), 14.0, nz(48));
1736            ('!'..='~')
1737                .take_while(|c| {
1738                    let before = probe.slots.len();
1739                    probe.ensure(*c, h, sk);
1740                    probe.slots.len() > before
1741                })
1742                .count()
1743        };
1744        let text = "uvwxyz";
1745        assert!(
1746            capacity > text.len() + 2,
1747            "the sheet must hold the screen's own set with room to pre-fill",
1748        );
1749
1750        // Fill the sheet with glyphs the screen does not use, stopping
1751        // just short of a reset, so the screen's walk resets partway.
1752        for c in ('!'..='~').take(capacity - 2) {
1753            idx.ensure(c, h, sk);
1754        }
1755
1756        let mut screen = Grid::new(1, 8);
1757        Parser::new().advance(&mut screen, text.as_bytes());
1758
1759        idx.populate(screen.screen());
1760        assert!(
1761            idx.atlas_was_reset(),
1762            "the pre-filled sheet must overflow during the walk",
1763        );
1764        assert!(
1765            text.chars().any(|c| idx.slot(c, h, sk).is_none()),
1766            "the glyphs primed before the reset lost their slots",
1767        );
1768
1769        idx.populate(screen.screen());
1770        assert!(
1771            !idx.atlas_was_reset(),
1772            "the reclaimed sheet holds the screen's own set in one walk",
1773        );
1774        for c in text.chars() {
1775            assert!(
1776                idx.slot(c, h, sk).is_some(),
1777                "second walk must re-slot {c:?} so its cell paints this frame",
1778            );
1779        }
1780    }
1781
1782    #[cfg(not(windows))]
1783    #[test]
1784    fn a_glyph_too_large_for_the_atlas_degrades_without_looping() {
1785        // An atlas so small nothing fits even when empty: a reset frees
1786        // no usable space, so without the retry-once guard this hangs.
1787        let mut idx = GlyphIndex::new(stack(), 14.0, nz(2));
1788        let h = idx.cell_metrics().height;
1789        let sk = SizingKey::default();
1790        idx.ensure('W', h, sk);
1791        assert_eq!(
1792            idx.slot('W', h, sk),
1793            None,
1794            "a glyph larger than the whole atlas degrades to no slot",
1795        );
1796    }
1797
1798    #[cfg(not(windows))]
1799    #[test]
1800    fn fractional_scale_rasterizes_below_default_for_sub_cell_shrink() {
1801        // `s=1, n=1, d=2` is effective scale 0.5, so the bitmap must be
1802        // smaller than the default.
1803        let mut idx = GlyphIndex::new(stack(), 14.0, nz(512));
1804        let h = idx.cell_metrics().height;
1805        let default_sk = SizingKey::default();
1806        let frac_half_sk = SizingKey::new(1, 0, 1, 2, 0, 0);
1807        let scale_2_sk = SizingKey::new(2, 0, 0, 0, 0, 0);
1808        assert!(idx.ensure('A', h, default_sk));
1809        assert!(idx.ensure('A', h, frac_half_sk));
1810        assert!(idx.ensure('A', h, scale_2_sk));
1811        let small = idx.slot('A', h, frac_half_sk).expect("fractional slot");
1812        let mid = idx.slot('A', h, default_sk).expect("default slot");
1813        let large = idx.slot('A', h, scale_2_sk).expect("integer scale=2 slot");
1814        // swash's hinted metrics are not strictly proportional; compare
1815        // the ordering, not exact ratios.
1816        assert!(
1817            small.size_px[1] < mid.size_px[1],
1818            "0.5× bitmap height ({}) must be below default ({})",
1819            small.size_px[1],
1820            mid.size_px[1],
1821        );
1822        assert!(
1823            large.size_px[1] > mid.size_px[1],
1824            "2× bitmap height ({}) must exceed default ({})",
1825            large.size_px[1],
1826            mid.size_px[1],
1827        );
1828    }
1829
1830    #[cfg(not(windows))]
1831    #[test]
1832    fn primary_face_ascii_is_never_fitted_at_any_osc66_scale() {
1833        let font = Arc::new(Font::load_test_font());
1834        for px in [13.0, 18.67, 24.0, 29.33] {
1835            let mut idx = GlyphIndex::new(FontStack::new(font.clone()), px, nz(512));
1836            let h = idx.cell_metrics().height;
1837            for s in 1..=7 {
1838                let sk = SizingKey::new(s, 0, 0, 0, 0, 0);
1839                for c in '!'..='~' {
1840                    assert!(
1841                        idx.fitted_bitmap(&font, c, h, sk).is_none(),
1842                        "{c:?} at {px} px, s={s}"
1843                    );
1844                }
1845            }
1846        }
1847    }
1848
1849    /// Width-1 chars the pinned emoji and symbol faces draw wider than a
1850    /// Monaspace cell.
1851    const OVERWIDE: [char; 6] = [
1852        '\u{263A}',
1853        '\u{2764}',
1854        '\u{2654}',
1855        '\u{26F5}',
1856        '\u{2B50}',
1857        '\u{1F30D}',
1858    ];
1859
1860    /// The ink fills the block along its binding axis and is centred on
1861    /// both, so a fitted glyph reads as large as the cell allows.
1862    #[cfg(not(windows))]
1863    #[test]
1864    fn a_fallback_glyph_wider_than_its_cell_fills_and_centres_in_it() {
1865        let stack = FontStack::pinned_test_stack();
1866        let mut idx = GlyphIndex::new(stack, 18.0, nz(1024));
1867        let m = idx.cell_metrics();
1868        let (cw, ch, ascent) = (i64::from(m.width), i64::from(m.height), i64::from(m.ascent));
1869        let mut fitted = 0;
1870        for c in OVERWIDE
1871            .into_iter()
1872            .filter(|&c| felis_grid::char_cell_width(c) == 1)
1873        {
1874            let font = idx.stack().resolve(c, FontStyle::REGULAR).clone();
1875            let Some(bitmap) = idx.fitted_bitmap(&font, c, m.height, SizingKey::default()) else {
1876                continue;
1877            };
1878            fitted += 1;
1879            let (w, h) = (i64::from(bitmap.width()), i64::from(bitmap.height()));
1880            let (left, top) = (i64::from(bitmap.left), i64::from(bitmap.top));
1881            let right = cw - left - w;
1882            let (above, below) = (ascent - top, ch - (ascent - top) - h);
1883            assert!(
1884                left >= 0 && right >= -1,
1885                "{c:?} inside the cell: left {left}, right {right}"
1886            );
1887            assert!(
1888                above >= -1 && below >= -1,
1889                "{c:?} inside the cell: above {above}, below {below}"
1890            );
1891            assert!(
1892                (left - right).abs() <= 1,
1893                "{c:?} centred: left {left}, right {right}"
1894            );
1895            assert!(
1896                (above - below).abs() <= 1,
1897                "{c:?} centred: above {above}, below {below}"
1898            );
1899            assert!(
1900                w >= cw - 2 || h >= ch - 2,
1901                "{c:?} fills an axis: {w}x{h} in {cw}x{ch}"
1902            );
1903        }
1904        assert!(
1905            fitted > 0,
1906            "no candidate overruns its cell in the pinned set"
1907        );
1908    }
1909
1910    /// An OSC 66 block is `w` (or the char's width) cells at the integer
1911    /// `s`, so a glyph between one and two cells wide fits `w=2` and a
1912    /// halved glyph in an `s=2` block, and is fitted only for `w=1`.
1913    #[cfg(not(windows))]
1914    #[test]
1915    fn osc66_width_and_fractional_scale_set_the_fitted_block() {
1916        let stack = FontStack::pinned_test_stack();
1917        let px = 18.0;
1918        let mut idx = GlyphIndex::new(stack, px, nz(1024));
1919        let m = idx.cell_metrics();
1920        let cw = m.width as f32;
1921        let (c, font) = OVERWIDE
1922            .into_iter()
1923            .filter(|&c| felis_grid::char_cell_width(c) == 1)
1924            .map(|c| (c, idx.stack().resolve(c, FontStyle::REGULAR).clone()))
1925            .find(|(c, font)| {
1926                font.advance_px(*c, px)
1927                    .is_some_and(|a| a > cw + 1.0 && a < 2.0 * cw)
1928            })
1929            .expect("the pinned set has a width-1 glyph between one and two cells wide");
1930        let w1 = SizingKey::new(1, 1, 0, 0, 0, 0);
1931        let w2 = SizingKey::new(1, 2, 0, 0, 0, 0);
1932        let half_in_s2 = SizingKey::new(2, 0, 1, 2, 0, 0);
1933        assert!(
1934            idx.fitted_bitmap(&font, c, m.height, w1).is_some(),
1935            "w=1 fits {c:?}"
1936        );
1937        assert!(
1938            idx.fitted_bitmap(&font, c, m.height, w2).is_none(),
1939            "w=2 holds {c:?}"
1940        );
1941        assert!(
1942            idx.fitted_bitmap(&font, c, m.height, half_in_s2).is_none(),
1943            "s=2 n=1 d=2 holds {c:?}"
1944        );
1945    }
1946
1947    /// A star the symbol faces cover is still drawn in color, as an
1948    /// emoji-presentation scalar.
1949    #[cfg(not(windows))]
1950    #[test]
1951    fn an_emoji_presentation_char_rasterizes_in_color() {
1952        let stack = FontStack::pinned_test_stack();
1953        let mut idx = GlyphIndex::new(stack, 18.0, nz(1024));
1954        let h = idx.cell_metrics().height;
1955        idx.ensure('\u{2B50}', h, SizingKey::default());
1956        let slot = idx
1957            .slot('\u{2B50}', h, SizingKey::default())
1958            .expect("a star slot");
1959        assert!(slot.is_color);
1960    }
1961
1962    /// A one-row grid fed `bytes` and walked against the pinned set.
1963    struct Walked {
1964        index: GlyphIndex,
1965        frame: ShapeFrame,
1966        grid: Grid,
1967    }
1968
1969    fn walk_pinned(bytes: &[u8], cols: u16) -> Walked {
1970        let stack = FontStack::pinned_test_stack();
1971        let mut grid = Grid::new(1, cols);
1972        Parser::new().advance(&mut grid, bytes);
1973        let mut index = GlyphIndex::new(stack, 18.0, nz(1024));
1974        let mut frame = ShapeFrame::empty();
1975        GridWalker::default().walk(
1976            &mut index,
1977            grid.screen(),
1978            &mut Shaper::new(),
1979            &[],
1980            &mut frame,
1981        );
1982        Walked { index, frame, grid }
1983    }
1984
1985    fn cluster_fit_px_at(p: &Walked, col: u16) -> u16 {
1986        match p.frame.cell_at(0, col) {
1987            ShapedCell::Cluster { fit_px, .. } => fit_px,
1988            other => panic!("col {col} is not a cluster: {other:?}"),
1989        }
1990    }
1991
1992    /// Clusters whose advance stays within their cells keep the native
1993    /// path: a ligated flag, a widened VS16 emoji, a keycap, a combining mark.
1994    #[cfg(not(windows))]
1995    #[test]
1996    fn a_cluster_within_its_cells_is_not_fitted() {
1997        for text in [
1998            "\u{1F1EF}\u{1F1F5}",
1999            "\u{2764}\u{FE0F}",
2000            "#\u{FE0F}\u{20E3}",
2001            "e\u{301}",
2002        ] {
2003            let p = walk_pinned(text.as_bytes(), 4);
2004            assert_eq!(cluster_fit_px_at(&p, 0), 0, "{text:?}");
2005        }
2006    }
2007
2008    /// The fit is measured once per cluster: a later walk reads the memo,
2009    /// so a repainted prompt does not rasterize its clusters again.
2010    #[cfg(not(windows))]
2011    #[test]
2012    fn a_fitted_cluster_is_measured_once() {
2013        let mut p = walk_pinned("\u{1F408}\u{200D}\u{1F408}".as_bytes(), 4);
2014        assert_eq!(p.index.cluster_fits.len(), 1);
2015        let first = cluster_fit_px_at(&p, 0);
2016        p.index.cluster_fits.values_mut().for_each(|px| *px += 1);
2017        let mut frame = ShapeFrame::empty();
2018        p.index.walked = None;
2019        GridWalker::default().walk(
2020            &mut p.index,
2021            p.grid.screen(),
2022            &mut Shaper::new(),
2023            &[],
2024            &mut frame,
2025        );
2026        p.frame = frame;
2027        assert_eq!(
2028            cluster_fit_px_at(&p, 0),
2029            first + 1,
2030            "the walk read the memo"
2031        );
2032    }
2033
2034    /// A font-supplied `│` lands a glyph-sized bitmap shorter than the
2035    /// cell, so `size_px[1] == cell_height` only holds on the synthetic
2036    /// path.
2037    #[cfg(not(windows))]
2038    #[test]
2039    fn vertical_box_drawing_bitmap_exactly_fills_the_cell() {
2040        let mut idx = GlyphIndex::new(stack(), 14.0, nz(256));
2041        let metrics = idx.cell_metrics();
2042        let sk = SizingKey::default();
2043        assert!(idx.ensure('│', metrics.height, sk));
2044        let slot = idx.slot('│', metrics.height, sk).expect("│ slot");
2045        assert_eq!(
2046            slot.size_px,
2047            [metrics.width, metrics.height],
2048            "synthetic │ must fill the cell so adjacent rows connect",
2049        );
2050        let ascent_i32 = i32::try_from(metrics.ascent).expect("ascent fits in i32");
2051        assert_eq!(
2052            slot.offset_px,
2053            [0, ascent_i32],
2054            "left=0 + top=ascent pins the bitmap at the cell's top-left",
2055        );
2056    }
2057
2058    #[cfg(not(windows))]
2059    #[test]
2060    fn allocations_within_the_atlas_never_overlap() {
2061        let mut idx = GlyphIndex::new(stack(), 14.0, nz(256));
2062        let h = idx.cell_metrics().height;
2063        let sk = SizingKey::default();
2064        let mut rects: Vec<(u32, u32, u32, u32)> = Vec::new();
2065        for c in 0x21u8..=0x7Eu8 {
2066            idx.ensure(c as char, h, sk);
2067            if let Some(s) = idx.slot(c as char, h, sk) {
2068                let [x, y] = s.atlas_px;
2069                rects.push((x, y, x + s.size_px[0], y + s.size_px[1]));
2070            }
2071        }
2072        for (i, a) in rects.iter().enumerate() {
2073            for b in &rects[i + 1..] {
2074                let separated = a.2 <= b.0 || b.2 <= a.0 || a.3 <= b.1 || b.3 <= a.1;
2075                assert!(separated, "overlap between {a:?} and {b:?}");
2076            }
2077        }
2078    }
2079
2080    /// Same idempotence and negative-entry handling as the per-char
2081    /// path.
2082    #[cfg(not(windows))]
2083    #[test]
2084    fn ensure_glyph_id_is_idempotent_and_caches_a_slot() {
2085        use felis_shaping::Shaper;
2086        let mut idx = GlyphIndex::new(stack(), 14.0, nz(256));
2087        let h = idx.cell_metrics().height;
2088        let sk = SizingKey::default();
2089        let primary = idx.stack().primary().clone();
2090        let mut shaper = Shaper::new();
2091        let shaped = shaper.shape_run(&primary, 14.0, &[], "A");
2092        let glyph_id = shaped[0].glyph_id;
2093        assert_ne!(glyph_id, 0);
2094        let first = idx.ensure_glyph_id(glyph_id, h, sk);
2095        assert!(first, "first ensure must rasterize + allocate");
2096        assert!(idx.contains_glyph_id(glyph_id, h, sk));
2097        assert!(idx.glyph_id_slot(glyph_id, h, sk).is_some());
2098        let second = idx.ensure_glyph_id(glyph_id, h, sk);
2099        assert!(!second, "second ensure is a cache hit");
2100    }
2101
2102    /// `.notdef` stores a negative entry, like a charmap miss.
2103    #[cfg(not(windows))]
2104    #[test]
2105    fn ensure_glyph_id_zero_lands_a_negative_slot() {
2106        let mut idx = GlyphIndex::new(stack(), 14.0, nz(256));
2107        let h = idx.cell_metrics().height;
2108        let sk = SizingKey::default();
2109        idx.ensure_glyph_id(0, h, sk);
2110        assert!(idx.contains_glyph_id(0, h, sk));
2111        assert_eq!(idx.glyph_id_slot(0, h, sk), None);
2112        assert_eq!(idx.drain_pending().count(), 0);
2113    }
2114
2115    #[test]
2116    fn empty_shape_frame_reports_none_for_every_cell() {
2117        let frame = ShapeFrame::empty();
2118        assert_eq!(frame.cell_at(0, 0), ShapedCell::None);
2119        assert_eq!(frame.cell_at(100, 100), ShapedCell::None);
2120        assert_eq!(frame.cols(), 0);
2121        assert_eq!(frame.cells(), []);
2122    }
2123
2124    /// Non-ASCII graphemes split the run; the wide cell falls back to
2125    /// the per-char path.
2126    #[cfg(not(windows))]
2127    #[test]
2128    fn shape_run_stops_at_non_ascii_grapheme() {
2129        let mut screen = Grid::new(1, 5);
2130        let mut parser = Parser::new();
2131        parser.advance(&mut screen, "aあb".as_bytes());
2132        let stack = stack();
2133        let primary = stack.primary().clone();
2134        let mut text = String::new();
2135        let run = next_shape_run(screen.screen(), 0, 0, &mut text, |c, _style| {
2136            primary.has_glyph(c)
2137        })
2138        .expect("first cell must start a run");
2139        assert_eq!(text, "a");
2140        assert_eq!(run.cell_count, 1, "non-ASCII grapheme stops the sweep");
2141    }
2142
2143    /// The same raw `glyph_id` primed against two faces keys two slots;
2144    /// the second `ensure_glyph_id` reports work only because the key
2145    /// differs.
2146    #[cfg(not(windows))]
2147    #[test]
2148    fn glyph_id_slots_key_on_font_id_so_faces_do_not_collide() {
2149        let mut idx = GlyphIndex::new(stack(), 14.0, nz(512));
2150        let h = idx.cell_metrics().height;
2151        let face0 = SizingKey::default();
2152        let face2 = SizingKey::default().with_font_id(2);
2153        assert_ne!(face0, face2, "font-id must change the key");
2154        assert!(
2155            idx.ensure_glyph_id(7, h, face0),
2156            "first face-0 insert works"
2157        );
2158        assert!(
2159            idx.ensure_glyph_id(7, h, face2),
2160            "same id under a different font-id is a distinct slot, not a hit",
2161        );
2162        assert!(
2163            !idx.ensure_glyph_id(7, h, face0),
2164            "re-inserting the face-0 key is a cache hit",
2165        );
2166        assert!(idx.contains_glyph_id(7, h, face0));
2167        assert!(idx.contains_glyph_id(7, h, face2));
2168    }
2169
2170    /// Single-glyph ligature cluster (the FiraCode-style collapse).
2171    /// Hand-rolled `ShapedGlyph` values so the shape under test cannot
2172    /// drift with the pinned font's GSUB.
2173    #[cfg(not(windows))]
2174    #[test]
2175    fn single_glyph_ligature_cluster_marks_primary_then_trailing() {
2176        let stack = stack();
2177        let mut idx = GlyphIndex::new(stack, 14.0, nz(512));
2178        let mut frame_cells = vec![ShapedCell::None; 4];
2179        let shaped = [ShapedGlyph {
2180            glyph_id: 42,
2181            source_byte_start: 0,
2182            source_byte_len: 2,
2183            advance_px: 16.0,
2184            x_offset_px: 0.0,
2185            y_offset_px: 0.0,
2186            from_ligature: true,
2187        }];
2188        apply_shaped_run(
2189            &mut frame_cells,
2190            &mut idx,
2191            4,
2192            &RunPlacement {
2193                row: 0,
2194                start_col: 0,
2195                cell_count: 2,
2196                style: FontStyle::REGULAR,
2197            },
2198            &shaped,
2199        );
2200        assert_eq!(
2201            frame_cells[0],
2202            ShapedCell::Primary {
2203                glyph_id: 42,
2204                span_cols: 2,
2205            },
2206        );
2207        assert_eq!(frame_cells[1], ShapedCell::Trailing);
2208        assert_eq!(frame_cells[2], ShapedCell::None);
2209    }
2210
2211    /// Multi-glyph ligature cluster (the Monaspace-style piece form: a
2212    /// blank lead glyph plus the body, both claiming the full span). The
2213    /// body's ink hangs off the second cell's pen with a negative left
2214    /// bearing, so it must land on cell 1.
2215    #[cfg(not(windows))]
2216    #[test]
2217    fn multi_glyph_ligature_cluster_places_pieces_by_pen_position() {
2218        let stack = stack();
2219        let mut idx = GlyphIndex::new(stack, 14.0, nz(512));
2220        let mut frame_cells = vec![ShapedCell::None; 4];
2221        let piece = |glyph_id| ShapedGlyph {
2222            glyph_id,
2223            source_byte_start: 0,
2224            source_byte_len: 2,
2225            advance_px: 8.0,
2226            x_offset_px: 0.0,
2227            y_offset_px: 0.0,
2228            from_ligature: true,
2229        };
2230        let shaped = [piece(100), piece(200)];
2231        apply_shaped_run(
2232            &mut frame_cells,
2233            &mut idx,
2234            4,
2235            &RunPlacement {
2236                row: 0,
2237                start_col: 0,
2238                cell_count: 2,
2239                style: FontStyle::REGULAR,
2240            },
2241            &shaped,
2242        );
2243        assert_eq!(
2244            frame_cells[0],
2245            ShapedCell::Primary {
2246                glyph_id: 100,
2247                span_cols: 1,
2248            },
2249            "lead piece (pen 0) anchors at the cluster's first cell",
2250        );
2251        assert_eq!(
2252            frame_cells[1],
2253            ShapedCell::Primary {
2254                glyph_id: 200,
2255                span_cols: 1,
2256            },
2257            "body piece (pen = one advance) anchors at the second cell",
2258        );
2259        assert_eq!(frame_cells[2], ShapedCell::None);
2260    }
2261
2262    /// A ligature wider than the cells it replaces is dropped, so the
2263    /// per-char path draws its source chars; one within them is kept.
2264    #[cfg(not(windows))]
2265    #[test]
2266    fn a_ligature_wider_than_its_cells_leaves_them_unshaped() {
2267        let mut idx = GlyphIndex::new(stack(), 14.0, nz(512));
2268        let cw = idx.cell_metrics().width as f32;
2269        let run = RunPlacement {
2270            row: 0,
2271            start_col: 0,
2272            cell_count: 2,
2273            style: FontStyle::REGULAR,
2274        };
2275        let ligature = |advance_px| ShapedGlyph {
2276            glyph_id: 42,
2277            source_byte_start: 0,
2278            source_byte_len: 2,
2279            advance_px,
2280            x_offset_px: 0.0,
2281            y_offset_px: 0.0,
2282            from_ligature: true,
2283        };
2284        let mut wide = vec![ShapedCell::None; 2];
2285        apply_shaped_run(
2286            &mut wide,
2287            &mut idx,
2288            2,
2289            &run,
2290            &[ligature(cw.mul_add(2.0, 3.0))],
2291        );
2292        assert_eq!(wide, [ShapedCell::None, ShapedCell::None]);
2293        let mut fitting = vec![ShapedCell::None; 2];
2294        apply_shaped_run(
2295            &mut fitting,
2296            &mut idx,
2297            2,
2298            &run,
2299            &[ligature(cw.mul_add(2.0, 1.0))],
2300        );
2301        assert_eq!(
2302            fitting,
2303            [
2304                ShapedCell::Primary {
2305                    glyph_id: 42,
2306                    span_cols: 2,
2307                },
2308                ShapedCell::Trailing,
2309            ],
2310        );
2311    }
2312
2313    /// Shaping `"->"` with `ss03` through the real run-scan + shape +
2314    /// apply pipeline against Monaspace Neon: both cells consumed, the
2315    /// visible arrow on cell 1.
2316    #[cfg(not(windows))]
2317    #[test]
2318    fn ligature_run_places_monaspace_arrow_pieces() {
2319        use felis_shaping::{Font, FontStack, Shaper};
2320        let font = Font::load_test_font();
2321        let features = vec!["ss03".to_owned()];
2322        let mut screen = Grid::new(1, 6);
2323        let mut parser = Parser::new();
2324        parser.advance(&mut screen, b"-> ab");
2325        let primary = Arc::new(font);
2326        let stack = FontStack::with_primary_features(primary, features.clone());
2327        let mut idx = GlyphIndex::new(stack, 14.0, nz(512));
2328        let cell_height = idx.cell_metrics().height;
2329        let cols = screen.cols();
2330        let cell_count = usize::from(screen.rows()) * usize::from(cols);
2331        let mut frame_cells = vec![ShapedCell::None; cell_count];
2332        let mut shaper = Shaper::new();
2333        let primary_face = idx.stack().primary().clone();
2334        let mut col = 0u16;
2335        let mut run_text = String::new();
2336        while col < cols {
2337            let Some(run) = next_shape_run(screen.screen(), 0, col, &mut run_text, |c, _style| {
2338                primary_face.has_glyph(c)
2339            }) else {
2340                col += 1;
2341                continue;
2342            };
2343            let run_start = run.start_col;
2344            let run_cell_count = run.cell_count;
2345            col = run_start + run_cell_count;
2346            if run_text.is_empty() {
2347                continue;
2348            }
2349            let shaped = shaper.shape_run(&primary_face, 14.0, &features, &run_text);
2350            apply_shaped_run(
2351                &mut frame_cells,
2352                &mut idx,
2353                cols,
2354                &RunPlacement {
2355                    row: 0,
2356                    start_col: run_start,
2357                    cell_count: run_cell_count,
2358                    style: FontStyle::REGULAR,
2359                },
2360                &shaped,
2361            );
2362        }
2363        // Unsubstituted gids via a features-off shape of each char alone:
2364        // the raw charmap is not public outside felis-shaping.
2365        let plain_gid = |shaper: &mut Shaper, c: &str| {
2366            let out = shaper.shape_run(&primary_face, 14.0, &[], c);
2367            assert_eq!(out.len(), 1);
2368            out[0].glyph_id
2369        };
2370        let plain_hyphen = plain_gid(&mut shaper, "-");
2371        let plain_greater = plain_gid(&mut shaper, ">");
2372        match frame_cells[1] {
2373            ShapedCell::Primary {
2374                glyph_id,
2375                span_cols,
2376            } => {
2377                assert_ne!(glyph_id, 0, "arrow body glyph id must be non-zero");
2378                assert_ne!(
2379                    glyph_id, plain_greater,
2380                    "cell 1 must carry the substituted arrow piece, not `>`",
2381                );
2382                assert_eq!(span_cols, 1, "piece glyphs claim one cell each");
2383                assert!(idx.contains_glyph_id(glyph_id, cell_height, SizingKey::default()));
2384            }
2385            other => panic!("expected the arrow body Primary at (0,1), got {other:?}"),
2386        }
2387        match frame_cells[0] {
2388            ShapedCell::Primary { glyph_id, .. } => {
2389                assert_ne!(
2390                    glyph_id, plain_hyphen,
2391                    "cell 0 must carry the blank lead piece, not `-`",
2392                );
2393            }
2394            ShapedCell::Trailing => {}
2395            ShapedCell::None | ShapedCell::Cluster { .. } => {
2396                panic!("cell 0 must be consumed by the shaped ligature cluster")
2397            }
2398        }
2399    }
2400
2401    /// The memo returns byte-identical output to a direct
2402    /// `Shaper::shape_run` and reports hit/miss like `ensure`.
2403    #[cfg(not(windows))]
2404    #[test]
2405    fn shape_run_cached_matches_direct_output_and_hits_on_repeat() {
2406        let stack = stack();
2407        let primary = stack.primary().clone();
2408        let features = vec!["calt".to_owned()];
2409        let mut idx = GlyphIndex::new(stack, 14.0, nz(256));
2410        let mut shaper = Shaper::new();
2411        let direct = shaper.shape_run(&primary, 14.0, &features, "Hello");
2412        let mut out = Vec::new();
2413        let first = idx.shape_run_cached(
2414            &mut shaper,
2415            &features,
2416            FontStyle::REGULAR,
2417            "Hello",
2418            &mut out,
2419        );
2420        assert!(first, "first call must shape (miss)");
2421        assert_eq!(out, direct, "memoized output must match a direct shape");
2422        let second = idx.shape_run_cached(
2423            &mut shaper,
2424            &features,
2425            FontStyle::REGULAR,
2426            "Hello",
2427            &mut out,
2428        );
2429        assert!(!second, "second call must be a cache hit");
2430        assert_eq!(out, direct, "hit must replay the identical output");
2431    }
2432
2433    /// Scrollback produces unbounded unique row texts, so the memo must
2434    /// stay bounded.
2435    #[cfg(not(windows))]
2436    #[test]
2437    fn shape_run_cache_stays_bounded_under_unique_text_pressure() {
2438        let stack = stack();
2439        let mut idx = GlyphIndex::new(stack, 14.0, nz(256));
2440        let mut shaper = Shaper::new();
2441        let mut out = Vec::new();
2442        let cap = GlyphIndex::SHAPED_RUN_CACHE_MAX;
2443        for i in 0..(cap + 16) {
2444            let text = format!("line {i}");
2445            idx.shape_run_cached(&mut shaper, &[], FontStyle::REGULAR, &text, &mut out);
2446        }
2447        assert!(
2448            idx.shaped_run_cache_len() <= cap,
2449            "cache len {} must not exceed cap {cap}",
2450            idx.shaped_run_cache_len(),
2451        );
2452        let kept = format!("line {}", cap + 15);
2453        assert!(
2454            !idx.shape_run_cached(&mut shaper, &[], FontStyle::REGULAR, &kept, &mut out),
2455            "most-recent insert must survive the eviction sweep",
2456        );
2457    }
2458
2459    /// A color change mid-row must not end a run (kitty ligates across
2460    /// syntax-highlight boundaries); per-cell color is applied at
2461    /// emission by `push_sliced_by_cell`.
2462    #[cfg(not(windows))]
2463    #[test]
2464    fn shape_run_continues_across_attribute_boundary() {
2465        use felis_grid::{Attributes, Cell, Color};
2466        let mut screen = Grid::new(1, 4);
2467        let default_style = screen.style_table_mut().intern(Attributes::default());
2468        screen.set_cell(
2469            0,
2470            0,
2471            Cell {
2472                grapheme: Grapheme::Ascii(b'='),
2473                style: default_style,
2474                link: None,
2475                sizing: None,
2476            },
2477        );
2478        let differ = screen.style_table_mut().intern(Attributes {
2479            fg: Color::Indexed(1),
2480            ..Attributes::default()
2481        });
2482        screen.set_cell(
2483            0,
2484            1,
2485            Cell {
2486                grapheme: Grapheme::Ascii(b'>'),
2487                style: differ,
2488                link: None,
2489                sizing: None,
2490            },
2491        );
2492        let stack = stack();
2493        let primary = stack.primary().clone();
2494        let mut text = String::new();
2495        let run0 = next_shape_run(screen.screen(), 0, 0, &mut text, |c, _style| {
2496            primary.has_glyph(c)
2497        })
2498        .expect("first cell must start a run");
2499        assert_eq!(run0.cell_count, 2, "color boundary must not end the run");
2500        assert_eq!(text, "=>");
2501    }
2502
2503    /// Unlike color, a bold/italic change ends the run.
2504    #[cfg(not(windows))]
2505    #[test]
2506    fn shape_run_breaks_at_bold_italic_boundary() {
2507        use felis_grid::{AttrFlags, Attributes, Cell};
2508        let mut screen = Grid::new(1, 4);
2509        let default_style = screen.style_table_mut().intern(Attributes::default());
2510        screen.set_cell(
2511            0,
2512            0,
2513            Cell {
2514                grapheme: Grapheme::Ascii(b'='),
2515                style: default_style,
2516                link: None,
2517                sizing: None,
2518            },
2519        );
2520        let bold = screen.style_table_mut().intern(Attributes {
2521            flags: AttrFlags::BOLD,
2522            ..Attributes::default()
2523        });
2524        screen.set_cell(
2525            0,
2526            1,
2527            Cell {
2528                grapheme: Grapheme::Ascii(b'>'),
2529                style: bold,
2530                link: None,
2531                sizing: None,
2532            },
2533        );
2534        let stack = stack();
2535        let primary = stack.primary().clone();
2536        let mut text = String::new();
2537        let run0 = next_shape_run(screen.screen(), 0, 0, &mut text, |c, _style| {
2538            primary.has_glyph(c)
2539        })
2540        .expect("first cell must start a run");
2541        assert_eq!(run0.cell_count, 1, "bold boundary must end the run");
2542        assert_eq!(text, "=");
2543        assert_eq!(run0.style, FontStyle::REGULAR);
2544        let run1 = next_shape_run(screen.screen(), 0, 1, &mut text, |c, _style| {
2545            primary.has_glyph(c)
2546        })
2547        .expect("second cell starts a fresh run");
2548        assert_eq!(text, ">");
2549        assert_eq!(
2550            run1.style,
2551            FontStyle {
2552                bold: true,
2553                italic: false
2554            },
2555        );
2556    }
2557
2558    /// The style rides the `SizingKey`, so bold and regular `'A'` key
2559    /// separate slots.
2560    #[cfg(not(windows))]
2561    #[test]
2562    fn styled_cells_get_distinct_atlas_slots() {
2563        let stack = stack();
2564        let mut idx = GlyphIndex::new(stack, 14.0, nz(512));
2565        let h = idx.cell_metrics().height;
2566        let regular = SizingKey::default();
2567        let bold = SizingKey::default().with_style(FontStyle {
2568            bold: true,
2569            italic: false,
2570        });
2571        assert!(idx.ensure('A', h, regular), "regular 'A' is a miss");
2572        assert!(
2573            idx.ensure('A', h, bold),
2574            "bold 'A' must be a separate miss, not a hit on the regular slot",
2575        );
2576        assert!(idx.contains('A', h, regular));
2577        assert!(idx.contains('A', h, bold));
2578    }
2579}
2580
2581#[cfg(test)]
2582#[cfg(not(windows))]
2583mod walk_tests;