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}