Skip to main content

retroglyph_core/grid/layers/
flatten.rs

1//! Whole-grid iteration and clearing: [`Grid::layers`], [`Grid::clear_all`], and the
2//! single-layer compositing [`Grid::flatten_into`] uses for cell backends.
3
4use super::super::{Grid, Pos, flat_index_to_xy};
5use crate::backend::DrawCell;
6use crate::color::Color;
7#[cfg(test)]
8use crate::color::Style;
9#[cfg(test)]
10use crate::tile::Tile;
11use crate::tile::TileFlags;
12use grixy::ops::GridWrite;
13
14impl Grid {
15    /// Yield a [`DrawCell`] for every allocated cell across all layers, in
16    /// layer-major (0 → `max_layer`) then row-major order. `grapheme` is
17    /// `Some` only when [`TileFlags::HAS_EXTRA`] is set.
18    ///
19    /// Unallocated layers are skipped. This is used by backends that need
20    /// the full frame on every draw (see [`crate::backend::Compositing::PixelLayered`]'s
21    /// `needs_full_frame` field).
22    ///
23    /// This iterator is zero-allocation: it walks the layer buffers inline.
24    pub fn layers(&self) -> impl Iterator<Item = DrawCell<'_>> + '_ {
25        let width = usize::from(self.width);
26        (0..=self.max_layer)
27            .filter_map(move |id| self.layer(id).map(|lb| (id, lb)))
28            .flat_map(move |(id, lb)| {
29                lb.buf.as_ref().iter().enumerate().map(move |(i, tile)| {
30                    let (x, y) = flat_index_to_xy(i, width);
31                    DrawCell {
32                        layer: id,
33                        pos: Pos::new(x, y),
34                        tile,
35                        grapheme: lb.extra_for(i, tile),
36                        tint: lb.tint_for(i, tile),
37                    }
38                })
39            })
40    }
41
42    /// Clears every allocated layer.
43    pub fn clear_all(&mut self) {
44        for layer in self.layers.iter_mut().flatten() {
45            layer.buf.clear();
46            layer.extras.clear();
47        }
48    }
49
50    /// Composites every allocated layer into `dst`'s layer 0, one tile per cell.
51    ///
52    /// Used by [`crate::terminal::Terminal::present`] for backends that do not composite
53    /// layers themselves (see [`crate::backend::Compositing::CellFlattened`]). The rule
54    /// matches the software renderer's pixel semantics and the [`blit`](Self::blit)
55    /// transparency convention:
56    ///
57    /// - Start from layer 0's tile (its `bg` fills the cell).
58    /// - For each higher allocated layer, in ascending order: if the tile is
59    ///   not empty (see [`Tile::is_empty`](crate::tile::Tile::is_empty)) replace the glyph, foreground,
60    ///   offsets, flags, span, and extra; if its background is not
61    ///   [`Color::Default`], replace the background.
62    ///
63    /// The span fields travel with the flags they are keyed by (see [`Tile::span`](crate::tile::Tile::span)): a
64    /// multi-cell span on a higher layer must arrive at a cell backend intact, or its covered
65    /// cells lose the anchor they name.
66    ///
67    /// Because an explicit space is not empty, drawing one on a higher layer
68    /// overwrites (erases) the glyph beneath it.
69    ///
70    /// `dst` must have the same dimensions as `self`.
71    ///
72    /// Walks layer buffers directly by flat index instead of calling
73    /// [`tile`](Self::tile) per cell (see retroglyph#262): that recomputes a coordinate
74    /// conversion and a bounds check per cell, which a flat scan over each layer's backing
75    /// buffer (the same style [`layers`](Self::layers) and [`diff`](Self::diff) already use)
76    /// avoids entirely.
77    pub(crate) fn flatten_into(&self, dst: &mut Self) {
78        debug_assert_eq!(
79            (self.width, self.height),
80            (dst.width, dst.height),
81            "flatten_into requires dst to have the same dimensions as self"
82        );
83        dst.has_spans |= self.has_spans;
84        let layer0 = self.layer0();
85        let cell_count = layer0.buf.as_ref().len();
86
87        // Seed every destination cell from layer 0: its tile verbatim, and its extra text
88        // filtered through `HAS_EXTRA` (the flag is authoritative, see `LayerBuf::extras`'
89        // doc comment, so a stale, unflagged entry in `layer0.extras` is not carried over).
90        let dst_layer0 = dst.layer0_mut();
91        dst_layer0.buf.as_mut().copy_from_slice(layer0.buf.as_ref());
92        dst_layer0.extras.clear();
93        for (&idx, extra) in &layer0.extras {
94            if layer0.buf.as_ref()[idx]
95                .flags
96                .contains(TileFlags::HAS_EXTRA)
97            {
98                dst_layer0.extras.insert(idx, extra.clone());
99            }
100        }
101
102        // Overlay every higher allocated layer, in ascending order, index-for-index.
103        for id in 1..=self.max_layer {
104            let Some(lb) = self.layer(id) else {
105                continue;
106            };
107            let src_buf = lb.buf.as_ref();
108            debug_assert_eq!(src_buf.len(), cell_count);
109            let dst_layer0 = dst.layer0_mut();
110            for (idx, tile) in src_buf.iter().enumerate() {
111                if !tile.flags.contains(TileFlags::EMPTY) {
112                    {
113                        let out = &mut dst_layer0.buf.as_mut()[idx];
114                        out.glyph = tile.glyph;
115                        out.width = tile.width;
116                        out.style.fg = tile.style.fg;
117                        out.dx = tile.dx;
118                        out.dy = tile.dy;
119                        out.flags = tile.flags;
120                        out.span_w = tile.span_w;
121                        out.span_h = tile.span_h;
122                    }
123                    if tile.flags.contains(TileFlags::HAS_EXTRA) {
124                        if let Some(extra) = lb.extra_entry_for(idx, tile) {
125                            dst_layer0.extras.insert(idx, extra);
126                        }
127                    } else {
128                        dst_layer0.extras.remove(&idx);
129                    }
130                }
131                if tile.style.bg != Color::Default {
132                    dst_layer0.buf.as_mut()[idx].style.bg = tile.style.bg;
133                }
134            }
135        }
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[cfg(feature = "egc")]
144    #[test]
145    fn clone_preserves_extra() {
146        let mut g = Grid::new(2, 2);
147        g.write_grapheme(0, 0, 0, "e\u{0301}", Style::default());
148        let cloned = g.clone();
149        assert_eq!(
150            crate::grid::grapheme_at(&cloned, 0, 0, 0),
151            Some("e\u{0301}")
152        );
153    }
154
155    #[cfg(feature = "egc")]
156    #[test]
157    fn flatten_into_carries_extra_from_higher_layer() {
158        let mut g = Grid::new(2, 2);
159        g.write_grapheme(1, 0, 0, "e\u{0301}", Style::default());
160        let mut flattened = Grid::new(2, 2);
161        g.flatten_into(&mut flattened);
162        assert_eq!(flattened[Pos::new(0, 0)].glyph, 'e');
163        assert_eq!(
164            crate::grid::grapheme_at(&flattened, 0, 0, 0),
165            Some("e\u{0301}")
166        );
167    }
168
169    #[test]
170    #[should_panic(expected = "same dimensions")]
171    fn flatten_into_mismatched_dimensions_panics() {
172        let g = Grid::new(2, 2);
173        let mut flattened = Grid::new(2, 3);
174        g.flatten_into(&mut flattened);
175    }
176
177    #[test]
178    fn flatten_into_single_layer_is_a_plain_copy() {
179        let mut g = Grid::new(2, 2);
180        g.put_tile(0, (0, 0), Tile::new('a', Style::default()));
181        g.put_tile(0, (1, 1), Tile::new('b', Style::default()));
182        let mut flattened = Grid::new(2, 2);
183        g.flatten_into(&mut flattened);
184        assert_eq!(flattened[Pos::new(0, 0)].glyph(), 'a');
185        assert_eq!(flattened[Pos::new(1, 1)].glyph(), 'b');
186        assert_eq!(flattened[Pos::new(1, 0)].glyph(), ' ');
187    }
188
189    #[test]
190    fn flatten_into_higher_layer_overwrites_glyph_and_fg_but_not_default_bg() {
191        let mut g = Grid::new(1, 1);
192        g.put_tile(
193            0,
194            (0, 0),
195            Tile::new('a', Style::new().fg(Color::BLACK).bg(Color::WHITE)),
196        );
197        g.put_tile(1, (0, 0), Tile::new('b', Style::new().fg(Color::WHITE)));
198
199        let mut flattened = Grid::new(1, 1);
200        g.flatten_into(&mut flattened);
201        let out = flattened[Pos::new(0, 0)];
202        assert_eq!(out.glyph(), 'b');
203        assert_eq!(out.style().fg, Color::WHITE);
204        // Layer 1's tile has a `Default` background, so layer 0's background shows through.
205        assert_eq!(out.style().bg, Color::WHITE);
206    }
207
208    #[test]
209    fn flatten_into_empty_higher_layer_cell_is_transparent() {
210        let mut g = Grid::new(2, 1);
211        g.put_tile(0, (0, 0), Tile::new('a', Style::default()));
212        g.put_tile(0, (1, 0), Tile::new('b', Style::default()));
213        // Only touch (0, 0) on layer 1; (1, 0) on layer 1 stays at its default (EMPTY) tile.
214        g.put_tile(1, (0, 0), Tile::new('c', Style::default()));
215
216        let mut flattened = Grid::new(2, 1);
217        g.flatten_into(&mut flattened);
218        assert_eq!(flattened[Pos::new(0, 0)].glyph(), 'c');
219        // Untouched by the transparent layer-1 cell: layer 0's glyph shows through.
220        assert_eq!(flattened[Pos::new(1, 0)].glyph(), 'b');
221    }
222
223    #[test]
224    fn flatten_into_multi_layer_stale_dst_extra_is_cleared() {
225        // `dst` may be a reused scratch buffer with stale content from a previous frame (see
226        // `Terminal::present`): `flatten_into` must fully overwrite it, not merge with it.
227        let mut flattened = Grid::new(1, 1);
228        flattened.put_tile(0, (0, 0), Tile::new('z', Style::default()));
229
230        let g = Grid::new(1, 1);
231        g.flatten_into(&mut flattened);
232        assert_eq!(flattened[Pos::new(0, 0)].glyph(), ' ');
233    }
234
235    #[test]
236    fn flatten_into_carries_span() {
237        // Cell backends receive the flattened grid, so a span on a higher layer has to survive
238        // flattening with both its flags *and* its span fields, or every covered cell ends up
239        // naming an anchor that isn't there.
240        let mut grid = Grid::new(4, 4);
241        grid.write_span(2, 1, 1, &["C=", "[]"], Style::default())
242            .unwrap();
243
244        let mut flat = Grid::new(4, 4);
245        grid.flatten_into(&mut flat);
246
247        assert_eq!(flat[Pos::new(1, 1)].span(), (2, 2));
248        assert!(
249            flat[Pos::new(1, 1)]
250                .flags()
251                .contains(TileFlags::SPAN_ANCHOR)
252        );
253        assert_eq!(flat.span_owner(0, 2, 2), Some(Pos::new(1, 1)));
254        assert_eq!(flat[Pos::new(2, 2)].glyph(), ']');
255    }
256
257    #[test]
258    fn copy_layer_from_overwrites_a_cell_the_destination_wrote_this_frame() {
259        // retroglyph#956: unlike `blit`, an empty source tile is not transparent, so a cell the
260        // destination wrote but the source never touched is erased, not left standing.
261        let mut src = Grid::new(4, 1);
262        src.put_tile(0, (0, 0), Tile::new('W', Style::default()));
263
264        let mut dst = Grid::new(4, 1);
265        dst.put_tile(0, (0, 0), Tile::new('W', Style::default()));
266        dst.put_tile(0, (2, 0), Tile::new('X', Style::default()));
267        dst.copy_layer_from(0, &src);
268
269        assert_eq!(dst[Pos::new(0, 0)].glyph(), 'W');
270        assert_eq!(dst[Pos::new(2, 0)].glyph(), ' ');
271    }
272
273    #[test]
274    fn copy_layer_from_preserves_span_flags_verbatim() {
275        // Unlike `blit`, `copy_layer_from` never clips or offsets, so it has no reason to
276        // degrade a span to its fallback glyphs the way
277        // `blit_degrades_a_span_to_its_fallback_glyphs` documents for `blit`.
278        let mut src = Grid::new(4, 4);
279        src.write_span(0, 0, 0, &["C=", "[]"], Style::default())
280            .unwrap();
281
282        let mut dst = Grid::new(4, 4);
283        dst.copy_layer_from(0, &src);
284
285        assert_eq!(dst[Pos::new(0, 0)].span(), (2, 2));
286        assert!(dst[Pos::new(0, 0)].flags().contains(TileFlags::SPAN_ANCHOR));
287        assert_eq!(dst.span_owner(0, 1, 1), Some(Pos::new(0, 0)));
288        assert_eq!(dst[Pos::new(1, 1)].glyph(), ']');
289    }
290
291    #[test]
292    fn copy_layer_from_clears_the_destination_when_the_source_layer_is_unallocated() {
293        // `src` never wrote layer 1, so `copy_layer_from` treats that as "nothing", clearing
294        // whatever `dst` had on layer 1 rather than leaving it untouched.
295        let src = Grid::new(4, 1);
296
297        let mut dst = Grid::new(4, 1);
298        dst.put_tile(1, (0, 0), Tile::new('X', Style::default()));
299        dst.copy_layer_from(1, &src);
300
301        assert_eq!(dst.tile(1, (0, 0)), None);
302    }
303
304    #[test]
305    fn copy_layer_from_is_a_noop_when_neither_side_has_the_layer_allocated() {
306        let src = Grid::new(4, 1);
307        let mut dst = Grid::new(4, 1);
308        dst.copy_layer_from(1, &src);
309
310        assert_eq!(dst.tile(1, (0, 0)), None);
311    }
312
313    #[test]
314    fn copy_layer_from_lowers_max_layer_when_the_source_layer_is_unallocated() {
315        // retroglyph#1378: deallocating `dst`'s top layer through `copy_layer_from` must lower
316        // `max_layer` exactly like `deallocate_layer` does, or the single-layer `present` fast
317        // path stays stuck off for the buffer's life.
318        let src = Grid::new(4, 1);
319
320        let mut dst = Grid::new(4, 1);
321        dst.put_tile(5, (0, 0), Tile::new('X', Style::default()));
322        assert_eq!(dst.max_layer(), 5);
323
324        dst.copy_layer_from(5, &src);
325
326        assert_eq!(dst.max_layer(), 0);
327        assert_eq!(dst.tile(5, (0, 0)), None);
328    }
329}