Skip to main content

retroglyph_core/grid/
spans.rs

1//! `Grid`'s multi-cell span API: [`Grid::write_span`](crate::grid::Grid::write_span), [`Grid::write_span_uniform`](crate::grid::Grid::write_span_uniform),
2//! [`Grid::span_owner`](crate::grid::Grid::span_owner), and [`Grid::clear_span`](crate::grid::Grid::clear_span), plus the anchor/covered-cell bookkeeping they
3//! share.
4
5use super::{Grid, Pos, Size, to_grixy_pos};
6use crate::color::Style;
7use crate::tile::{Tile, TileFlags};
8use alloc::collections::BTreeSet;
9use alloc::vec::Vec;
10use grixy::ops::GridRead;
11use ixy::HasSize;
12
13/// A span's largest representable extent on either axis (see `Tile::span_w`/`Tile::span_h`),
14/// and so the widest band [`Grid::repair_spans_after_resize`] ever needs to scan near a shrunk
15/// edge: no anchor further than this from the edge can have a stale footprint reaching it.
16const MAX_SPAN_EXTENT: u16 = u8::MAX as u16;
17
18impl Grid {
19    /// Writes a multi-cell span at `(x, y)` on `layer`: one piece of artwork occupying a block of
20    /// cells rather than one.
21    ///
22    /// `rows` holds one string per row of the footprint, so the span is `rows.len()` cells tall
23    /// and `rows[0]`'s character count wide, and every row must be that same width. Any
24    /// `AsRef<str>` row works, so a literal footprint (`&["[==]", "|__|"]`) and a computed one
25    /// (`&Vec<String>`) both pass without a borrowing pass over the rows. The first
26    /// character goes to the **anchor** cell at `(x, y)` with [`TileFlags::SPAN_ANCHOR`]; each
27    /// remaining character goes to its own cell with [`TileFlags::SPAN_COVERED`]. `style` applies
28    /// to every cell.
29    ///
30    /// # Text fallback
31    ///
32    /// The covered cells keep real glyphs, which is what lets one call render correctly on every
33    /// backend with no capability check:
34    ///
35    /// - A **cell backend** (`Headless`, `retroglyph-crossterm`, `retroglyph-terminal`) ignores
36    ///   [`TileFlags::SPAN_COVERED`] and prints all of them, so `["[==]", "|__|"]` reads as a
37    ///   small piece of ASCII art.
38    /// - A **pixel backend** (`retroglyph-software`, `retroglyph-gl`) looks the anchor glyph up in
39    ///   its sprite cache, draws that one sprite across the whole footprint, and skips every
40    ///   covered cell's glyph.
41    ///
42    /// This is the deliberate difference from [`TileFlags::WIDE_CHAR_SPACER`], which every
43    /// backend skips.
44    ///
45    /// Any existing span or wide character the footprint would partially overwrite is cleared
46    /// first, in full, as [`write_grapheme`](Self::write_grapheme) does for its own 1- or 2-cell
47    /// write.
48    ///
49    /// For the common sprite case (one runtime-chosen anchor glyph, blanks in every covered
50    /// cell), [`write_span_uniform`](Self::write_span_uniform) says the same thing without
51    /// building the rows.
52    ///
53    /// # Returns
54    ///
55    /// `Some(())` once the whole span is written, or `None` having written nothing at all when
56    /// `rows` is empty, its first row is empty, its rows differ in width, either axis exceeds 255
57    /// cells, or the footprint would not fit in the grid at `(x, y)`.
58    ///
59    /// # Examples
60    ///
61    /// ```
62    /// # fn main() {
63    /// # fn run() -> Option<()> {
64    /// use retroglyph_core::color::Style;
65    /// use retroglyph_core::grid::{Grid, Pos};
66    ///
67    /// let mut grid = Grid::new(8, 4);
68    /// grid.write_span(0, 1, 1, &["[==]", "|__|"], Style::default())?;
69    ///
70    /// assert_eq!(grid.tile(0, Pos::new(1, 1))?.span(), (4, 2));
71    /// // Covered cells keep their fallback glyphs, and name their anchor.
72    /// assert_eq!(grid.tile(0, Pos::new(4, 2))?.glyph(), '|');
73    /// assert_eq!(grid.span_owner(0, 4, 2), Some(Pos::new(1, 1)));
74    /// # Some(())
75    /// # }
76    /// # run().unwrap();
77    /// # }
78    /// ```
79    pub fn write_span<S: AsRef<str>>(
80        &mut self,
81        layer: u8,
82        x: u16,
83        y: u16,
84        rows: &[S],
85        style: Style,
86    ) -> Option<()> {
87        let cols = rows.first()?.as_ref().chars().count();
88        if cols == 0 || rows.iter().any(|r| r.as_ref().chars().count() != cols) {
89            return None;
90        }
91        // `Tile` stores a span's dimensions in one byte each (see `Tile::span_w`), so a span
92        // wider or taller than 255 cells is not representable.
93        let footprint = (u8::try_from(cols).ok()?, u8::try_from(rows.len()).ok()?);
94
95        self.write_span_cells(
96            layer,
97            Pos::new(x, y),
98            footprint,
99            style,
100            rows.iter().map(|row| row.as_ref().chars()),
101        )
102    }
103
104    /// Writes a `size` multi-cell span at `pos` on `layer`: `anchor` in the anchor cell, `fill`
105    /// in every other cell of the footprint.
106    ///
107    /// The uniform case of [`write_span`](Self::write_span), and the shape a sheet-driven
108    /// renderer usually wants: one sprite, chosen at runtime, with the cells it covers blanked so
109    /// nothing shows through its transparent pixels. Spelling that as an array of blank rows
110    /// carries no information and, for a computed anchor, has to be allocated per draw.
111    ///
112    /// `fill` is what a *cell* backend prints for the covered cells (a pixel backend skips them
113    /// and draws the sprite instead), so it is the span's text fallback: `' '` blanks them, and a
114    /// visible character keeps the footprint legible in a terminal. See
115    /// [`write_span`](Self::write_span) for the full write semantics.
116    ///
117    /// # Returns
118    ///
119    /// `Some(())` once the whole span is written, or `None` having written nothing at all when
120    /// either axis of `size` is `0` or exceeds 255 cells, or the footprint would not fit in the
121    /// grid at `pos`.
122    ///
123    /// # Examples
124    ///
125    /// ```
126    /// # fn main() {
127    /// # fn run() -> Option<()> {
128    /// use retroglyph_core::color::Style;
129    /// use retroglyph_core::grid::{Grid, Pos};
130    ///
131    /// let mut grid = Grid::new(8, 4);
132    /// let anchor = '\u{E000}'; // chosen at runtime from a tilesheet
133    /// grid.write_span_uniform(0, (1, 1), (2, 2), anchor, ' ', Style::default())?;
134    ///
135    /// assert_eq!(grid.tile(0, Pos::new(1, 1))?.span(), (2, 2));
136    /// assert_eq!(grid.span_owner(0, 2, 2), Some(Pos::new(1, 1)));
137    /// # Some(())
138    /// # }
139    /// # run().unwrap();
140    /// # }
141    /// ```
142    pub fn write_span_uniform(
143        &mut self,
144        layer: u8,
145        pos: impl Into<Pos>,
146        size: impl Into<Size>,
147        anchor: char,
148        fill: char,
149        style: Style,
150    ) -> Option<()> {
151        let size = size.into();
152        // `Tile` stores a span's dimensions in one byte each (see `Tile::span_w`), so a span
153        // wider or taller than 255 cells is not representable.
154        let footprint = (
155            u8::try_from(size.width()).ok()?,
156            u8::try_from(size.height()).ok()?,
157        );
158        if footprint.0 == 0 || footprint.1 == 0 {
159            return None;
160        }
161
162        let rows = (0..footprint.1).map(move |row| {
163            (0..footprint.0).map(move |col| if (row, col) == (0, 0) { anchor } else { fill })
164        });
165        self.write_span_cells(layer, pos.into(), footprint, style, rows)
166    }
167
168    /// Writes a `footprint` (`w`, `h`) span at `pos` on `layer`, taking its glyphs row by row.
169    ///
170    /// The shared body of [`write_span`](Self::write_span) and
171    /// [`write_span_uniform`](Self::write_span_uniform): both have already narrowed the footprint
172    /// to a `u8` per axis, so all that is left is the grid-fit check and the write itself.
173    /// `rows` must yield exactly `footprint.1` rows of exactly `footprint.0` glyphs.
174    fn write_span_cells<R: Iterator<Item = char>>(
175        &mut self,
176        layer: u8,
177        pos: Pos,
178        footprint: (u8, u8),
179        style: Style,
180        rows: impl Iterator<Item = R>,
181    ) -> Option<()> {
182        let (footprint_w, footprint_h) = footprint;
183        let (x, y) = (pos.x, pos.y);
184
185        let grid_w = usize::from(self.width);
186        if usize::from(x) + usize::from(footprint_w) > grid_w
187            || usize::from(y) + usize::from(footprint_h) > usize::from(self.height)
188        {
189            return None;
190        }
191
192        // Clear anything the footprint would partially overwrite. Every rejection above happens
193        // first, so a refused write can never have already destroyed the caller's content.
194        // Neither call is gated on `egc`: `put_tile` writes a `WIDE_CHAR`/`WIDE_CHAR_SPACER` pair
195        // on every feature combination, so a span that can land inside one has to clean it up
196        // regardless of `egc` (same reasoning as `fill_region`'s matching pair of calls).
197        for row in 0..footprint_h {
198            let cy = y + u16::from(row);
199            self.clear_span_overlap(layer, x, cy, u16::from(footprint_w));
200            self.clear_overlap(layer, x, cy, u16::from(footprint_w));
201        }
202
203        self.has_spans = true;
204        let lb = self.layer_or_alloc(layer);
205        for (row, line) in rows.enumerate() {
206            for (col, ch) in line.enumerate() {
207                let idx = (usize::from(y) + row) * grid_w + usize::from(x) + col;
208                let mut tile = Tile::new(ch, style);
209                if row == 0 && col == 0 {
210                    tile.flags = TileFlags::SPAN_ANCHOR;
211                    tile.span_w = footprint_w;
212                    tile.span_h = footprint_h;
213                } else {
214                    // Both fit in a `u8`: they are strictly less than the footprint, which was
215                    // already narrowed to one above.
216                    #[allow(clippy::cast_possible_truncation)]
217                    {
218                        tile.flags = TileFlags::SPAN_COVERED;
219                        tile.span_w = col as u8;
220                        tile.span_h = row as u8;
221                    }
222                }
223                lb.buf.as_mut()[idx] = tile;
224                lb.extras.remove(&idx);
225            }
226        }
227        Some(())
228    }
229
230    /// The anchor of the multi-cell span occupying `(x, y)` on `layer`, or `None` when the cell
231    /// belongs to no span or is out of bounds.
232    ///
233    /// An anchor cell reports itself, so every cell of one span answers with the same position
234    /// and hit-testing multi-cell artwork is a single comparison:
235    ///
236    /// ```
237    /// # fn main() {
238    /// # fn run() -> Option<()> {
239    /// # use retroglyph_core::color::Style;
240    /// # use retroglyph_core::grid::{Grid, Pos};
241    /// # let mut grid = Grid::new(8, 4);
242    /// grid.write_span(0, 2, 1, &["[==]", "|__|"], Style::default())?;
243    /// let chest = Pos::new(2, 1);
244    /// // Any of the eight cells counts as standing on the chest.
245    /// assert_eq!(grid.span_owner(0, 2, 1), Some(chest));
246    /// assert_eq!(grid.span_owner(0, 5, 2), Some(chest));
247    /// assert_eq!(grid.span_owner(0, 6, 2), None);
248    /// # Some(())
249    /// # }
250    /// # run().unwrap();
251    /// # }
252    /// ```
253    ///
254    /// O(1): a covered tile stores its offset back to the anchor (see [`Tile::span_offset`](crate::tile::Tile::span_offset)), so
255    /// this is a lookup and a subtraction, not a scan.
256    #[must_use]
257    pub fn span_owner(&self, layer: u8, x: u16, y: u16) -> Option<Pos> {
258        self.span_anchor_at(layer, x, y)
259    }
260
261    /// Clears the whole multi-cell span that `(x, y)` on `layer` belongs to, anchor included,
262    /// resetting every one of its cells to the default (empty) tile.
263    ///
264    /// Works from any cell of the span, so it pairs with [`span_owner`](Self::span_owner): hit-test
265    /// a cell, then clear the artwork it belongs to. Does nothing if the cell is not part of a
266    /// span, is out of bounds, or the layer is unallocated.
267    pub fn clear_span(&mut self, layer: u8, x: u16, y: u16) {
268        if let Some(anchor) = self.span_anchor_at(layer, x, y) {
269            self.reset_span_at(layer, anchor);
270        }
271    }
272
273    /// The anchor of the span `(x, y)` belongs to, treating an anchor cell as its own anchor.
274    fn span_anchor_at(&self, layer: u8, x: u16, y: u16) -> Option<Pos> {
275        let tile = self.layer(layer)?.buf.get(to_grixy_pos(Pos::new(x, y)))?;
276        if tile.flags.contains(TileFlags::SPAN_ANCHOR) {
277            return Some(Pos::new(x, y));
278        }
279        let (dx, dy) = tile.span_offset()?;
280        Some(Pos::new(x.checked_sub(dx)?, y.checked_sub(dy)?))
281    }
282
283    /// Resets every cell of the span anchored at `anchor` on `layer`. No-op if that cell is not
284    /// a [`TileFlags::SPAN_ANCHOR`], or the layer is unallocated.
285    fn reset_span_at(&mut self, layer: u8, anchor: Pos) {
286        let w = usize::from(self.width);
287        let h = usize::from(self.height);
288        let Some(lb) = self
289            .layers
290            .get_mut(usize::from(layer))
291            .and_then(Option::as_mut)
292        else {
293            return;
294        };
295        let anchor_idx = usize::from(anchor.y) * w + usize::from(anchor.x);
296        let Some(anchor_tile) = lb.buf.as_ref().get(anchor_idx).copied() else {
297            return;
298        };
299        if !anchor_tile.flags.contains(TileFlags::SPAN_ANCHOR) {
300            return;
301        }
302        for row in 0..usize::from(anchor_tile.span_h) {
303            let cy = usize::from(anchor.y) + row;
304            if cy >= h {
305                break;
306            }
307            for col in 0..usize::from(anchor_tile.span_w) {
308                let cx = usize::from(anchor.x) + col;
309                if cx >= w {
310                    break;
311                }
312                let idx = cy * w + cx;
313                lb.buf.as_mut()[idx].reset();
314                lb.extras.remove(&idx);
315            }
316        }
317    }
318
319    /// Repairs [`TileFlags::SPAN_ANCHOR`] footprints made stale by a shrinking
320    /// [`resize`](Self::resize).
321    ///
322    /// `resize` keeps a grid's top-left corner, and an anchor is always the top-left of its own
323    /// footprint, so shrinking can never orphan a *covered* cell from its anchor. It can still
324    /// leave the anchor's declared `(span_w, span_h)` running past the new edge. Half a span is
325    /// not representable (the same reasoning [`blit`](Self::blit) documents for clipping one), so
326    /// any anchor whose footprint no longer fits has its whole span cleared via
327    /// [`reset_span_at`](Self::reset_span_at) instead of being left to claim cells that do not
328    /// exist.
329    ///
330    /// `width_shrank`/`height_shrank` say which axis actually got smaller; `resize` only calls
331    /// this when at least one is true, so a growing resize never reaches here. Each shrunk axis
332    /// only scans a band up to [`u8::MAX`] cells deep from its new edge -- a span's largest
333    /// representable extent (see `Tile::span_w`) -- rather than the whole grid, so an anchor far
334    /// from the shrunk edge is never visited and the cost tracks the resize, not the grid's total
335    /// size.
336    pub(super) fn repair_spans_after_resize(&mut self, width_shrank: bool, height_shrank: bool) {
337        if !self.has_spans {
338            return;
339        }
340        let w = self.width;
341        let h = self.height;
342        if width_shrank {
343            let x_start = w.saturating_sub(MAX_SPAN_EXTENT);
344            self.repair_span_region(x_start, w, 0, h);
345        }
346        if height_shrank {
347            let y_start = h.saturating_sub(MAX_SPAN_EXTENT);
348            self.repair_span_region(0, w, y_start, h);
349        }
350    }
351
352    /// Resets every [`TileFlags::SPAN_ANCHOR`] in `x_start..x_end` × `y_start..y_end`, on every
353    /// allocated layer, whose stored footprint no longer fits within the grid's current bounds.
354    ///
355    /// The two calls in [`repair_spans_after_resize`](Self::repair_spans_after_resize) can overlap
356    /// in their shared corner when both axes shrink; revisiting that corner just re-checks a few
357    /// already-repaired anchors; `reset_span_at` is a no-op on a cell that is no longer an anchor.
358    fn repair_span_region(&mut self, x_start: u16, x_end: u16, y_start: u16, y_end: u16) {
359        let w = self.width;
360        let h = self.height;
361        for layer_id in 0..self.layers.len() {
362            let mut anchors: Vec<Pos> = Vec::new();
363            if let Some(lb) = self.layers[layer_id].as_ref() {
364                for y in y_start..y_end {
365                    for x in x_start..x_end {
366                        // `x_end`/`y_end` are always `w`/`h` (see the two call sites in
367                        // `repair_spans_after_resize`), so `idx` is always in bounds: no `.get`
368                        // needed, and no untestable out-of-bounds branch to carry.
369                        let idx = usize::from(y) * usize::from(w) + usize::from(x);
370                        let tile = &lb.buf.as_ref()[idx];
371                        if !tile.flags.contains(TileFlags::SPAN_ANCHOR) {
372                            continue;
373                        }
374                        let (span_w, span_h) = tile.span();
375                        if usize::from(x) + usize::from(span_w) > usize::from(w)
376                            || usize::from(y) + usize::from(span_h) > usize::from(h)
377                        {
378                            anchors.push(Pos::new(x, y));
379                        }
380                    }
381                }
382            } else {
383                continue;
384            }
385            #[allow(clippy::cast_possible_truncation)]
386            let layer = layer_id as u8;
387            for anchor in anchors {
388                self.reset_span_at(layer, anchor);
389            }
390        }
391    }
392
393    /// Clears every multi-cell span that a `width`-cell write starting at `(x, y)` on `layer`
394    /// would partially overwrite.
395    ///
396    /// The span analogue of [`clear_overlap`](Self::clear_overlap), and the reason every ordinary
397    /// write path calls it: overwriting one cell of a span would otherwise leave an anchor
398    /// claiming cells it no longer owns, or a covered cell pointing at an anchor that is gone.
399    ///
400    /// Returns immediately on a grid that has never had a span written to it, which is what keeps
401    /// this off the cost of an ordinary [`put_tile`](Self::put_tile) (see
402    /// [`has_spans`](Self::has_spans)).
403    pub(super) fn clear_span_overlap(&mut self, layer: u8, x: u16, y: u16, width: u16) {
404        if !self.has_spans {
405            return;
406        }
407        // Collect first: resetting a span mutates cells this scan is still reading. Overlapping
408        // writes touch at most a handful of spans, so the linear `contains` beats a set.
409        let mut anchors: Vec<Pos> = Vec::new();
410        let Some(lb) = self.layer(layer) else {
411            return;
412        };
413        for cx in x..x.saturating_add(width) {
414            let Some(tile) = lb.buf.get(to_grixy_pos(Pos::new(cx, y))) else {
415                continue;
416            };
417            let anchor = if tile.flags.contains(TileFlags::SPAN_ANCHOR) {
418                Pos::new(cx, y)
419            } else if let Some((dx, dy)) = tile.span_offset() {
420                match (cx.checked_sub(dx), y.checked_sub(dy)) {
421                    (Some(ax), Some(ay)) => Pos::new(ax, ay),
422                    _ => continue,
423                }
424            } else {
425                continue;
426            };
427            if !anchors.contains(&anchor) {
428                anchors.push(anchor);
429            }
430        }
431        for anchor in anchors {
432            self.reset_span_at(layer, anchor);
433        }
434    }
435
436    /// Clears every multi-cell span that a `width` x `height` write starting at `(x, y)` on
437    /// `layer` would partially overwrite.
438    ///
439    /// The region analogue of [`clear_span_overlap`](Self::clear_span_overlap), for callers like
440    /// [`fill_region`](super::Grid::fill_region) that would otherwise call it once per row: a span
441    /// spanning several of those rows would then be collected, and fully reset, once per row it
442    /// occupies. This scans the whole region once instead, deduplicating anchors in a
443    /// `BTreeSet<(u16, u16)>` (`Pos` has no `Ord`) so each span is reset exactly once regardless
444    /// of how many rows or columns of the region it overlaps.
445    ///
446    /// Returns immediately on a grid that has never had a span written to it, same as
447    /// [`clear_span_overlap`](Self::clear_span_overlap).
448    pub(super) fn clear_span_overlap_rect(
449        &mut self,
450        layer: u8,
451        x: u16,
452        y: u16,
453        width: u16,
454        height: u16,
455    ) {
456        if !self.has_spans {
457            return;
458        }
459        // Collect first, same reasoning as `clear_span_overlap`: resetting a span mutates cells
460        // this scan is still reading.
461        let mut anchors: BTreeSet<(u16, u16)> = BTreeSet::new();
462        let Some(lb) = self.layer(layer) else {
463            return;
464        };
465        for cy in y..y.saturating_add(height) {
466            for cx in x..x.saturating_add(width) {
467                let Some(tile) = lb.buf.get(to_grixy_pos(Pos::new(cx, cy))) else {
468                    continue;
469                };
470                let anchor = if tile.flags.contains(TileFlags::SPAN_ANCHOR) {
471                    (cx, cy)
472                } else if let Some((dx, dy)) = tile.span_offset() {
473                    match (cx.checked_sub(dx), cy.checked_sub(dy)) {
474                        (Some(ax), Some(ay)) => (ax, ay),
475                        _ => continue,
476                    }
477                } else {
478                    continue;
479                };
480                anchors.insert(anchor);
481            }
482        }
483        for (ax, ay) in anchors {
484            self.reset_span_at(layer, Pos::new(ax, ay));
485        }
486    }
487}
488
489#[cfg(test)]
490mod tests {
491    use super::*;
492
493    // ── Multi-cell spans (retroglyph#412) ────────────────────────────────
494    /// The anchor owns the footprint; every other cell names the anchor and keeps its own glyph.
495    #[test]
496    fn write_span_marks_anchor_and_covered_cells() {
497        let mut grid = Grid::new(4, 4);
498        grid.write_span(0, 1, 1, &["C=", "[]"], Style::default())
499            .expect("2x2 span fits in a 4x4 grid");
500
501        let anchor = grid.tile(0, (1, 1)).unwrap();
502        assert!(anchor.flags().contains(TileFlags::SPAN_ANCHOR));
503        assert_eq!(anchor.span(), (2, 2));
504        assert_eq!(anchor.span_offset(), None);
505        assert_eq!(anchor.glyph(), 'C');
506
507        for (x, y, glyph, offset) in [
508            (2, 1, '=', (1, 0)),
509            (1, 2, '[', (0, 1)),
510            (2, 2, ']', (1, 1)),
511        ] {
512            let tile = grid.tile(0, (x, y)).unwrap();
513            assert!(
514                tile.flags().contains(TileFlags::SPAN_COVERED),
515                "({x}, {y}) should be covered"
516            );
517            assert_eq!(tile.glyph(), glyph, "({x}, {y}) keeps its fallback glyph");
518            assert_eq!(tile.span_offset(), Some(offset));
519            // A covered cell is inside a footprint, it does not own one.
520            assert_eq!(tile.span(), (1, 1));
521        }
522    }
523
524    /// `clear_overlap` runs regardless of `egc` (see its own doc comment): `write_span_cells`
525    /// gated it behind the feature until retroglyph#1014, so a wide pair written by `put_tile`
526    /// (which is not itself `egc`-gated) kept a stale `WIDE_CHAR` flag after a span write
527    /// partially overwrote it with `egc` off.
528    #[test]
529    fn write_span_clears_a_wide_char_it_partially_overwrites() {
530        let mut grid = Grid::new(4, 1);
531        grid.put_tile(0, (0, 0), Tile::new('\u{4e2d}', Style::default()));
532        assert!(
533            grid.tile(0, (0, 0))
534                .unwrap()
535                .flags()
536                .contains(TileFlags::WIDE_CHAR)
537        );
538
539        grid.write_span(0, 1, 0, &["ab"], Style::default()).unwrap();
540
541        assert!(
542            !grid
543                .tile(0, (0, 0))
544                .unwrap()
545                .flags()
546                .contains(TileFlags::WIDE_CHAR)
547        );
548    }
549
550    /// The whole point of `SPAN_COVERED` differing from `WIDE_CHAR_SPACER`: cell backends read
551    /// these glyphs, so they must survive the write intact.
552    #[test]
553    fn write_span_keeps_the_fallback_glyphs_readable() {
554        let mut grid = Grid::new(4, 4);
555        grid.write_span(0, 0, 0, &["C=", "[]"], Style::default())
556            .unwrap();
557        let read = |x, y| grid.tile(0, (x, y)).unwrap().glyph();
558        assert_eq!(
559            [read(0, 0), read(1, 0), read(0, 1), read(1, 1)],
560            ['C', '=', '[', ']']
561        );
562    }
563
564    #[test]
565    fn span_owner_reports_the_anchor_from_every_cell_of_the_span() {
566        let mut grid = Grid::new(6, 6);
567        grid.write_span(0, 2, 3, &["AB", "CD", "EF"], Style::default())
568            .unwrap();
569
570        // Every cell of the footprint, the anchor included, answers with the same position, so
571        // hit-testing is one comparison.
572        for (x, y) in [(2, 3), (3, 3), (2, 4), (3, 4), (2, 5), (3, 5)] {
573            assert_eq!(grid.span_owner(0, x, y), Some(Pos::new(2, 3)), "({x}, {y})");
574        }
575        // A free cell, an out-of-bounds one, and one on an unallocated layer belong to no span.
576        assert_eq!(grid.span_owner(0, 0, 0), None);
577        assert_eq!(grid.span_owner(0, 99, 99), None);
578        assert_eq!(grid.span_owner(3, 3, 3), None);
579    }
580
581    #[test]
582    fn write_span_rejects_malformed_input_without_writing() {
583        let mut grid = Grid::new(4, 4);
584        assert_eq!(
585            grid.write_span(0, 0, 0, &[] as &[&str], Style::default()),
586            None
587        );
588        assert_eq!(grid.write_span(0, 0, 0, &[""], Style::default()), None);
589        // Ragged rows.
590        assert_eq!(
591            grid.write_span(0, 0, 0, &["ab", "c"], Style::default()),
592            None
593        );
594        // Too wide / too tall for the grid at this origin.
595        assert_eq!(grid.write_span(0, 3, 0, &["ab"], Style::default()), None);
596        assert_eq!(
597            grid.write_span(0, 0, 3, &["a", "b"], Style::default()),
598            None
599        );
600        // Nothing was written by any of the above.
601        for y in 0..4 {
602            for x in 0..4 {
603                assert!(
604                    grid[Pos::new(x, y)].is_empty(),
605                    "({x}, {y}) should be untouched"
606                );
607            }
608        }
609    }
610
611    #[test]
612    fn write_span_takes_any_as_ref_str_row() {
613        use alloc::string::String;
614        use alloc::vec::Vec;
615
616        let mut grid = Grid::new(4, 4);
617        // A footprint computed at runtime: owned rows, no borrowing pass over them.
618        let rows: Vec<String> = (0..2)
619            .map(|row| {
620                (0..2)
621                    .map(|col| if (row, col) == (0, 0) { 'C' } else { ' ' })
622                    .collect()
623            })
624            .collect();
625
626        assert_eq!(grid.write_span(0, 0, 0, &rows, Style::default()), Some(()));
627        assert_eq!(grid[Pos::new(0, 0)].glyph(), 'C');
628        assert_eq!(grid[Pos::new(0, 0)].span(), (2, 2));
629    }
630
631    #[test]
632    fn write_span_uniform_writes_the_anchor_once_and_fills_the_rest() {
633        let mut grid = Grid::new(4, 4);
634        assert_eq!(
635            grid.write_span_uniform(0, (1, 1), (2, 2), 'C', '.', Style::default()),
636            Some(())
637        );
638
639        assert_eq!(grid[Pos::new(1, 1)].glyph(), 'C');
640        assert_eq!(grid[Pos::new(1, 1)].span(), (2, 2));
641        for (x, y) in [(2, 1), (1, 2), (2, 2)] {
642            assert_eq!(grid[Pos::new(x, y)].glyph(), '.', "({x}, {y})");
643            assert_eq!(grid.span_owner(0, x, y), Some(Pos::new(1, 1)));
644        }
645    }
646
647    #[test]
648    fn write_span_uniform_matches_the_equivalent_write_span() {
649        let mut uniform = Grid::new(4, 4);
650        uniform
651            .write_span_uniform(0, (0, 0), (3, 2), 'C', ' ', Style::default())
652            .unwrap();
653
654        let mut rows = Grid::new(4, 4);
655        rows.write_span(0, 0, 0, &["C  ", "   "], Style::default())
656            .unwrap();
657
658        for y in 0..4 {
659            for x in 0..4 {
660                assert_eq!(
661                    uniform[Pos::new(x, y)],
662                    rows[Pos::new(x, y)],
663                    "({x}, {y}) differs"
664                );
665            }
666        }
667    }
668
669    #[test]
670    fn write_span_uniform_rejects_a_degenerate_or_oversized_footprint() {
671        let mut grid = Grid::new(4, 4);
672        let style = Style::default();
673
674        assert_eq!(
675            grid.write_span_uniform(0, (0, 0), (0, 2), 'C', ' ', style),
676            None
677        );
678        assert_eq!(
679            grid.write_span_uniform(0, (0, 0), (2, 0), 'C', ' ', style),
680            None
681        );
682        // A span's dimensions are one byte each.
683        assert_eq!(
684            grid.write_span_uniform(0, (0, 0), (256, 1), 'C', ' ', style),
685            None
686        );
687        // Does not fit the grid at this origin.
688        assert_eq!(
689            grid.write_span_uniform(0, (3, 0), (2, 1), 'C', ' ', style),
690            None
691        );
692
693        for y in 0..4 {
694            for x in 0..4 {
695                assert!(
696                    grid[Pos::new(x, y)].is_empty(),
697                    "({x}, {y}) should be untouched"
698                );
699            }
700        }
701    }
702
703    #[test]
704    fn writing_into_a_covered_cell_clears_the_whole_span() {
705        let mut grid = Grid::new(4, 4);
706        grid.write_span(0, 0, 0, &["C=", "[]"], Style::default())
707            .unwrap();
708        grid.put_tile(0, (1, 1), Tile::new('x', Style::default()));
709
710        assert_eq!(grid[Pos::new(1, 1)].glyph(), 'x');
711        for (x, y) in [(0, 0), (1, 0), (0, 1)] {
712            let tile = grid[Pos::new(x, y)];
713            assert!(tile.is_empty(), "({x}, {y}) should have been cleared");
714            assert_eq!(tile.flags(), TileFlags::EMPTY);
715        }
716    }
717
718    #[test]
719    fn writing_over_the_anchor_clears_the_whole_span() {
720        let mut grid = Grid::new(4, 4);
721        grid.write_span(0, 0, 0, &["C=", "[]"], Style::default())
722            .unwrap();
723        grid.put_tile(0, (0, 0), Tile::new('x', Style::default()));
724
725        assert_eq!(grid[Pos::new(0, 0)].glyph(), 'x');
726        assert_eq!(grid[Pos::new(0, 0)].span(), (1, 1));
727        for (x, y) in [(1, 0), (0, 1), (1, 1)] {
728            assert!(
729                grid[Pos::new(x, y)].is_empty(),
730                "({x}, {y}) should be cleared"
731            );
732        }
733    }
734
735    #[test]
736    fn overlapping_spans_erase_the_old_one_entirely() {
737        let mut grid = Grid::new(4, 4);
738        grid.write_span(0, 0, 0, &["AB", "CD"], Style::default())
739            .unwrap();
740        // Overlaps the first span's bottom-right cell only; all four of its cells must go.
741        grid.write_span(0, 1, 1, &["EF", "GH"], Style::default())
742            .unwrap();
743
744        assert!(grid[Pos::new(0, 0)].is_empty());
745        assert!(grid[Pos::new(1, 0)].is_empty());
746        assert!(grid[Pos::new(0, 1)].is_empty());
747        assert_eq!(grid[Pos::new(1, 1)].glyph(), 'E');
748        assert_eq!(grid.span_owner(0, 2, 2), Some(Pos::new(1, 1)));
749    }
750
751    #[test]
752    fn clear_span_works_from_any_cell_of_the_span() {
753        let mut grid = Grid::new(4, 4);
754        for from in [(0, 0), (1, 0), (0, 1), (1, 1)] {
755            grid.write_span(0, 0, 0, &["C=", "[]"], Style::default())
756                .unwrap();
757            grid.clear_span(0, from.0, from.1);
758            for y in 0..2 {
759                for x in 0..2 {
760                    assert!(
761                        grid[Pos::new(x, y)].is_empty(),
762                        "clearing from {from:?}: ({x}, {y})"
763                    );
764                }
765            }
766        }
767        // A cell that is not part of a span is left alone.
768        grid.put_tile(0, (3, 3), Tile::new('z', Style::default()));
769        grid.clear_span(0, 3, 3);
770        assert_eq!(grid[Pos::new(3, 3)].glyph(), 'z');
771    }
772
773    /// `clear_span_overlap_rect` scans the whole region once (retroglyph#1020), rather than
774    /// calling `clear_span_overlap` once per row: a span several rows tall must still come out
775    /// fully reset, not just its slice under the first row scanned.
776    #[test]
777    fn clear_span_overlap_rect_clears_a_span_spanning_every_row_it_touches() {
778        let mut grid = Grid::new(6, 6);
779        grid.write_span(0, 1, 1, &["AB", "CD", "EF", "GH"], Style::default())
780            .expect("2x4 span fits in a 6x6 grid");
781
782        // A single call covering all four rows the span occupies, same as `fill_region` now
783        // makes once per call instead of once per row.
784        grid.clear_span_overlap_rect(0, 0, 1, 6, 4);
785
786        for y in 1..5 {
787            for x in 1..3 {
788                let tile = grid[Pos::new(x, y)];
789                assert!(tile.is_empty(), "({x}, {y}) should have been reset");
790            }
791        }
792    }
793
794    /// `has_spans` is grid-wide, not per layer (see its own doc comment), so a span written to
795    /// layer 0 is enough to take `clear_span_overlap_rect` past its fast path even when called
796    /// against a layer that has never been allocated. That layer must return with no allocation
797    /// and no panic, not implicitly create one just to find it empty.
798    #[test]
799    fn clear_span_overlap_rect_on_an_unallocated_layer_is_a_no_op() {
800        let mut grid = Grid::new(4, 4);
801        grid.write_span(0, 0, 0, &["C=", "[]"], Style::default())
802            .unwrap();
803
804        grid.clear_span_overlap_rect(1, 0, 0, 4, 4);
805
806        assert!(grid.tile(1, (0, 0)).is_none());
807    }
808
809    /// A direct `clear_span_overlap_rect` call, unlike `fill_region`, is not clipped to the grid
810    /// before it scans: `width`/`height` reaching past the grid's own edges must skip the
811    /// out-of-bounds cells rather than panicking, while still resetting the in-bounds portion of
812    /// a span the in-bounds part of the scan touches.
813    #[test]
814    fn clear_span_overlap_rect_skips_out_of_bounds_cells_without_panicking() {
815        let mut grid = Grid::new(4, 4);
816        grid.write_span(0, 2, 2, &["C=", "[]"], Style::default())
817            .unwrap();
818
819        grid.clear_span_overlap_rect(0, 2, 2, 10, 10);
820
821        for y in 2..4 {
822            for x in 2..4 {
823                assert!(grid[Pos::new(x, y)].is_empty(), "({x}, {y})");
824            }
825        }
826    }
827
828    /// A `SPAN_COVERED` cell whose stored offset is larger than its own position never comes out
829    /// of a real write (an anchor is always in-bounds and at or before every cell it covers), but
830    /// a corrupted or adversarial layer should not panic subtracting past zero. Hand-crafts that
831    /// cell directly (bypassing `write_span`) to exercise the `checked_sub` guard.
832    #[test]
833    fn clear_span_overlap_rect_skips_a_covered_cell_whose_offset_underflows() {
834        let mut grid = Grid::new(4, 4);
835        // A real span elsewhere sets `has_spans`, so the scan below actually runs instead of
836        // short-circuiting.
837        grid.write_span(0, 3, 3, &["Z"], Style::default()).unwrap();
838
839        let mut bogus = Tile::new('x', Style::default());
840        bogus.flags = TileFlags::SPAN_COVERED;
841        bogus.span_w = 1;
842        bogus.span_h = 0;
843        grid[Pos::new(0, 0)] = bogus;
844
845        grid.clear_span_overlap_rect(0, 0, 0, 1, 1);
846
847        // No panic, and the bogus cell is left alone: there is no real anchor at (-1, 0) to
848        // reset it against.
849        assert_eq!(grid[Pos::new(0, 0)].glyph(), 'x');
850    }
851
852    #[test]
853    fn spans_are_layer_scoped() {
854        let mut grid = Grid::new(4, 4);
855        grid.write_span(1, 0, 0, &["C=", "[]"], Style::default())
856            .unwrap();
857        assert_eq!(grid.span_owner(1, 1, 1), Some(Pos::new(0, 0)));
858        // Layer 0 knows nothing about layer 1's span, and writing there leaves it intact.
859        assert_eq!(grid.span_owner(0, 1, 1), None);
860        assert_eq!(grid.span_owner(0, 0, 0), None);
861        grid.put_tile(0, (1, 1), Tile::new('x', Style::default()));
862        assert_eq!(grid.span_owner(1, 1, 1), Some(Pos::new(0, 0)));
863    }
864
865    #[test]
866    fn clear_region_clears_a_span_it_only_partly_covers() {
867        let mut grid = Grid::new(4, 4);
868        grid.write_span(0, 0, 0, &["C=", "[]"], Style::default())
869            .unwrap();
870        // Only the anchor cell is inside the region, but the whole span must go.
871        grid.put_tile(0, (0, 0), Tile::default());
872        for y in 0..2 {
873            for x in 0..2 {
874                assert!(grid[Pos::new(x, y)].is_empty(), "({x}, {y})");
875            }
876        }
877    }
878
879    #[test]
880    fn resize_narrower_clears_a_span_anchor_whose_footprint_no_longer_fits() {
881        let mut grid = Grid::new(4, 2);
882        grid.write_span(0, 0, 0, &["ab", "cd"], Style::default())
883            .unwrap();
884
885        // Drops the span's right-hand column: a 2-wide footprint cannot survive on a 1-wide grid,
886        // so the whole span must go rather than leave the anchor claiming a footprint that no
887        // longer fits.
888        grid.resize(1, 2);
889        assert!(grid.tile(0, (0, 0)).unwrap().is_empty());
890        assert_eq!(grid.tile(0, (0, 0)).unwrap().span(), (1, 1));
891    }
892
893    #[test]
894    fn resize_shorter_clears_a_span_anchor_whose_footprint_no_longer_fits() {
895        let mut grid = Grid::new(2, 4);
896        grid.write_span(0, 0, 0, &["a", "c"], Style::default())
897            .unwrap();
898
899        // Same shape of bug on the other axis: drops the span's bottom row.
900        grid.resize(2, 1);
901        assert!(grid.tile(0, (0, 0)).unwrap().is_empty());
902        assert_eq!(grid.tile(0, (0, 0)).unwrap().span(), (1, 1));
903    }
904
905    #[test]
906    fn resize_narrower_skips_unallocated_layers_between_allocated_ones() {
907        // Layer 1 stays `None`: writing to layer 2 grows the layer table past it without
908        // allocating it. The repair scan must walk straight past that gap layer instead of
909        // panicking or mistaking it for one with a stale anchor.
910        let mut grid = Grid::new(4, 2);
911        grid.write_span(0, 0, 0, &["ab", "cd"], Style::default())
912            .unwrap();
913        grid.put_tile(2, (0, 0), Tile::new('z', Style::default()));
914        assert!(grid.tile(1, (0, 0)).is_none());
915
916        grid.resize(1, 2);
917        assert!(grid.tile(0, (0, 0)).unwrap().is_empty());
918        assert_eq!(grid.tile(0, (0, 0)).unwrap().span(), (1, 1));
919        // Untouched by the repair scan on an unrelated layer.
920        assert_eq!(grid.tile(2, (0, 0)).unwrap().glyph(), 'z');
921    }
922
923    #[test]
924    fn resize_wider_leaves_a_span_anchor_untouched() {
925        // A growing resize never removes any of a footprint's cells, so the anchor must survive
926        // exactly as written.
927        let mut grid = Grid::new(4, 4);
928        grid.write_span(0, 0, 0, &["C=", "[]"], Style::default())
929            .unwrap();
930        grid.resize(8, 8);
931        assert_eq!(grid.tile(0, (0, 0)).unwrap().span(), (2, 2));
932        assert_eq!(grid.span_owner(0, 1, 1), Some(Pos::new(0, 0)));
933    }
934}