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