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}