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