Skip to main content

retroglyph/
grid.rs

1//! The grid container.
2
3#[cfg(feature = "egc")]
4use crate::style::Style;
5use crate::tile::Tile;
6#[cfg(feature = "egc")]
7use crate::tile::TileFlags;
8#[cfg(feature = "egc")]
9use crate::tile::cap_grapheme;
10use alloc::vec::Vec;
11use core::fmt;
12use core::ops::{Index, IndexMut};
13use grixy::buf::GridBuf;
14use grixy::ops::layout::RowMajor;
15use grixy::ops::{ExactSizeGrid, GridDiff, GridRead, GridWrite};
16
17/// Size of the grid.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
19pub struct Size {
20    /// Width.
21    pub width: u16,
22    /// Height.
23    pub height: u16,
24}
25
26/// Pos in the grid, in (x = column, y = row) order.
27///
28/// Implements [`Ord`] in row-major order (y primary, then x), which is the
29/// natural ordering for terminal rendering: top-to-bottom, left-to-right within
30/// each row.
31pub type Pos = ixy::Pos<u16>;
32
33/// Rectangle in the grid.
34pub type Rect = ixy::Rect<u16>;
35
36impl From<(u16, u16)> for Size {
37    fn from((width, height): (u16, u16)) -> Self {
38        Self { width, height }
39    }
40}
41
42impl From<Size> for (u16, u16) {
43    fn from(s: Size) -> Self {
44        (s.width, s.height)
45    }
46}
47
48// ---------------------------------------------------------------------------
49// Helpers: coordinate conversion between u16 and usize
50// ---------------------------------------------------------------------------
51
52fn to_grixy_pos(pos: Pos) -> grixy::core::Pos {
53    grixy::core::Pos::new(usize::from(pos.x), usize::from(pos.y))
54}
55
56#[allow(clippy::missing_const_for_fn)]
57fn from_grixy_pos(pos: grixy::core::Pos) -> Pos {
58    #[allow(clippy::cast_possible_truncation)]
59    Pos::new(pos.x as u16, pos.y as u16)
60}
61
62// ---------------------------------------------------------------------------
63// Grid iterators
64// ---------------------------------------------------------------------------
65
66/// Iterator over all cells with their `(x, y)` coordinates.
67pub struct Cells<'a> {
68    iter: core::iter::Enumerate<core::slice::Iter<'a, Tile>>,
69    width: usize,
70}
71
72impl<'a> Iterator for Cells<'a> {
73    type Item = (u16, u16, &'a Tile);
74
75    fn next(&mut self) -> Option<Self::Item> {
76        self.iter.next().map(|(i, tile)| {
77            #[allow(clippy::cast_possible_truncation)]
78            let x = (i % self.width) as u16;
79            #[allow(clippy::cast_possible_truncation)]
80            let y = (i / self.width) as u16;
81            (x, y, tile)
82        })
83    }
84}
85
86/// Mutable iterator over all cells with their `(x, y)` coordinates.
87pub struct CellsMut<'a> {
88    iter: core::iter::Enumerate<core::slice::IterMut<'a, Tile>>,
89    width: usize,
90}
91
92impl<'a> Iterator for CellsMut<'a> {
93    type Item = (u16, u16, &'a mut Tile);
94
95    fn next(&mut self) -> Option<Self::Item> {
96        self.iter.next().map(|(i, tile)| {
97            #[allow(clippy::cast_possible_truncation)]
98            let x = (i % self.width) as u16;
99            #[allow(clippy::cast_possible_truncation)]
100            let y = (i / self.width) as u16;
101            (x, y, tile)
102        })
103    }
104}
105
106// ---------------------------------------------------------------------------
107// LayerBuf — a single layer's flat buffer
108// ---------------------------------------------------------------------------
109
110/// A single layer in the grid: a flat 2D buffer of one tile per cell.
111///
112/// Layer 0 is always allocated. Layers 1–255 are allocated on first write
113/// (see [`Grid::put_tile`]).
114pub(crate) struct LayerBuf {
115    pub(crate) buf: GridBuf<Tile, Vec<Tile>, RowMajor>,
116}
117
118impl LayerBuf {
119    fn new(width: u16, height: u16) -> Self {
120        let n = usize::from(width) * usize::from(height);
121        Self {
122            buf: GridBuf::from_buffer(alloc::vec![Tile::default(); n], usize::from(width)),
123        }
124    }
125}
126
127// ---------------------------------------------------------------------------
128// Grid
129// ---------------------------------------------------------------------------
130
131/// The main grid container for the terminal.
132///
133/// Holds up to 256 layers (0–255). Layer 0 is always allocated; higher layers
134/// are allocated on first write. Single-layer games pay no overhead — layers
135/// 1+ remain `None` until used.
136///
137/// Note: This uses `alloc::vec::Vec`, requiring an allocator in `no_std` environments.
138/// For strictly static, no-alloc environments, a static-sized grid type may be added in the future.
139pub struct Grid {
140    width: u16,
141    height: u16,
142    /// Indexed by layer ID (0–255). Index 0 is always `Some`.
143    /// Unwritten layers are `None` — no allocation until first write.
144    layers: Vec<Option<LayerBuf>>,
145}
146
147// ---------------------------------------------------------------------------
148// Internal helpers
149// ---------------------------------------------------------------------------
150
151impl Grid {
152    /// Borrow a specific layer, or `None` if unallocated.
153    fn layer(&self, id: u8) -> Option<&LayerBuf> {
154        self.layers[usize::from(id)].as_ref()
155    }
156
157    /// Borrow a specific layer mutably, allocating it if necessary.
158    fn layer_or_alloc(&mut self, id: u8) -> &mut LayerBuf {
159        let idx = usize::from(id);
160        if self.layers[idx].is_none() {
161            self.layers[idx] = Some(LayerBuf::new(self.width, self.height));
162        }
163        self.layers[idx].as_mut().unwrap()
164    }
165
166    /// Borrow layer 0 (always allocated).
167    fn layer0(&self) -> &LayerBuf {
168        // SAFETY: layer 0 is always `Some` (set in `new`).
169        self.layers[0].as_ref().unwrap()
170    }
171
172    /// Borrow layer 0 mutably (always allocated).
173    fn layer0_mut(&mut self) -> &mut LayerBuf {
174        self.layers[0].as_mut().unwrap()
175    }
176}
177
178// ---------------------------------------------------------------------------
179// Grid — public API (all forward to layer 0)
180// ---------------------------------------------------------------------------
181
182impl Grid {
183    /// Creates a new grid of the given dimensions.
184    ///
185    /// Layer 0 is allocated immediately. Layers 1–255 are `None` until first
186    /// write via [`put_tile`](Self::put_tile).
187    #[must_use]
188    pub fn new(width: u16, height: u16) -> Self {
189        let mut layers = alloc::vec![];
190        layers.resize_with(256, || None);
191        layers[0] = Some(LayerBuf::new(width, height));
192        Self {
193            width,
194            height,
195            layers,
196        }
197    }
198
199    /// Returns the width of the grid.
200    #[must_use]
201    pub const fn width(&self) -> u16 {
202        self.width
203    }
204
205    /// Returns the height of the grid.
206    #[must_use]
207    pub const fn height(&self) -> u16 {
208        self.height
209    }
210
211    /// Sets the tile at the given coordinates on layer 0.
212    ///
213    /// # Panics
214    ///
215    /// Panics if the coordinates are out of bounds.
216    pub fn put(&mut self, x: u16, y: u16, tile: Tile) {
217        let pos = to_grixy_pos(Pos::new(x, y));
218        let lb = self.layer0_mut();
219        assert!(
220            lb.buf.contains(pos),
221            "coordinates out of bounds: ({x}, {y})"
222        );
223        lb.buf[pos] = tile;
224    }
225
226    /// Gets the tile at the given coordinates on layer 0.
227    ///
228    /// # Panics
229    ///
230    /// Panics if the coordinates are out of bounds.
231    #[must_use]
232    pub fn get(&self, x: u16, y: u16) -> &Tile {
233        &self.layer0().buf[to_grixy_pos(Pos::new(x, y))]
234    }
235
236    /// Tries to set the tile at the given coordinates on layer 0.
237    ///
238    /// Returns `None` if the coordinates are out of bounds.
239    pub fn checked_put(&mut self, x: u16, y: u16, tile: Tile) -> Option<()> {
240        let pos = to_grixy_pos(Pos::new(x, y));
241        let lb = self.layer0_mut();
242        if lb.buf.contains(pos) {
243            lb.buf[pos] = tile;
244            Some(())
245        } else {
246            None
247        }
248    }
249
250    /// Tries to get the tile at the given coordinates on layer 0.
251    ///
252    /// Returns `None` if the coordinates are out of bounds.
253    #[must_use]
254    pub fn checked_get(&self, x: u16, y: u16) -> Option<&Tile> {
255        let pos = to_grixy_pos(Pos::new(x, y));
256        self.layer0().buf.get(pos)
257    }
258
259    /// Tries to get a mutable reference to the tile at the given coordinates
260    /// on layer 0.
261    ///
262    /// Returns `None` if the coordinates are out of bounds.
263    pub fn checked_get_mut(&mut self, x: u16, y: u16) -> Option<&mut Tile> {
264        let pos = to_grixy_pos(Pos::new(x, y));
265        self.layer0_mut().buf.get_mut(pos)
266    }
267
268    /// Iterates all tiles on `layer` with their `(x, y)` coordinates.
269    ///
270    /// Returns `None` if the layer is unallocated.
271    #[must_use]
272    pub fn cells(&self, layer: u8) -> Option<Cells<'_>> {
273        let lb = self.layer(layer)?;
274        Some(Cells {
275            iter: lb.buf.as_ref().iter().enumerate(),
276            width: usize::from(self.width),
277        })
278    }
279
280    /// Iterates all tiles on `layer` mutably with their `(x, y)` coordinates.
281    ///
282    /// If the layer has not been written to yet, it is allocated first.
283    pub fn cells_mut(&mut self, layer: u8) -> CellsMut<'_> {
284        let width = usize::from(self.width);
285        let lb = self.layer_or_alloc(layer);
286        CellsMut {
287            iter: lb.buf.as_mut().iter_mut().enumerate(),
288            width,
289        }
290    }
291
292    /// Clears a specific layer, resetting all tiles to the default.
293    ///
294    /// Does nothing if the layer is unallocated.
295    pub fn clear(&mut self, layer: u8) {
296        if let Some(lb) = self.layers[usize::from(layer)].as_mut() {
297            lb.buf.clear();
298        }
299    }
300
301    /// Resize the grid to `width` × `height` tiles.
302    ///
303    /// Content within the overlapping region is preserved on all allocated
304    /// layers. New cells are initialised to the default tile. Shrinking
305    /// discards tiles outside the new bounds.
306    pub fn resize(&mut self, width: u16, height: u16) {
307        self.width = width;
308        self.height = height;
309        for layer in self.layers.iter_mut().flatten() {
310            layer.buf.resize(usize::from(width), usize::from(height));
311        }
312    }
313
314    // ------------------------------------------------------------------
315    // Write grapheme — layer 0 only
316    // ------------------------------------------------------------------
317
318    /// Write a grapheme cluster at `(x, y)` on layer 0, enforcing wide-
319    /// character invariants.
320    ///
321    /// This is the canonical way to place content into the grid when the `egc`
322    /// feature is enabled. It:
323    ///
324    /// - Clears any wide character whose primary or spacer cell would be
325    ///   overwritten.
326    /// - Sets [`TileFlags::WIDE_CHAR`] on the primary cell and places a
327    ///   [`TileFlags::WIDE_CHAR_SPACER`] in the adjacent cell for 2-column
328    ///   characters.
329    /// - Stores multi-codepoint EGCs (combining marks, ZWJ sequences) in
330    ///   `Tile::extra`, capped at 8 codepoints total.
331    ///
332    /// Does nothing if `(x, y)` is out of bounds, if the grapheme has zero
333    /// display width, or if a 2-column wide character would overflow the grid
334    /// (the last column needs both its own cell and a spacer).
335    ///
336    /// # Panics
337    ///
338    /// Panics if the grapheme's display width exceeds [`u16::MAX`]. In
339    /// practice this cannot happen: the maximum Unicode grapheme width is 2.
340    ///
341    /// Only present when the `egc` feature is enabled.
342    #[cfg(feature = "egc")]
343    pub fn write_grapheme(&mut self, x: u16, y: u16, grapheme: &str, style: Style) {
344        use unicode_width::UnicodeWidthStr;
345
346        let width = u16::try_from(grapheme.width()).expect("grapheme width exceeds u16");
347        if width == 0 {
348            return;
349        }
350
351        // Capture dimensions as plain values to avoid borrow conflicts.
352        let w = usize::from(self.width);
353        let cap = w * usize::from(self.height);
354        let idx = usize::from(y) * w + usize::from(x);
355        if idx >= cap {
356            return;
357        }
358
359        // A 2-column char needs a spacer at x+1. If that's out of bounds,
360        // silently refuse rather than leaving an orphaned primary cell.
361        if width == 2 && x.saturating_add(1) as usize >= w {
362            return;
363        }
364
365        // Clear any wide-char cell that would be partially overwritten.
366        // clear_overlap only needs dimensions as values (captured above).
367        self.clear_overlap(x, y, width);
368
369        // Capture width before borrowing self mutably.
370        let grid_w = usize::from(self.width);
371        let idx = usize::from(y) * grid_w + usize::from(x);
372
373        let lb = self.layer0_mut();
374        // Build cell content.
375        let mut chars = grapheme.chars();
376        let first = chars.next().unwrap_or(' ');
377        let has_extra = chars.next().is_some();
378        let extra = if has_extra {
379            Some(alloc::sync::Arc::new(cap_grapheme(grapheme)))
380        } else {
381            None
382        };
383        let flags = if width == 2 {
384            TileFlags::WIDE_CHAR
385        } else {
386            TileFlags::empty()
387        };
388
389        lb.buf.as_mut()[idx].glyph = first;
390        lb.buf.as_mut()[idx].style = style;
391        lb.buf.as_mut()[idx].extra = extra;
392        lb.buf.as_mut()[idx].flags = flags;
393
394        // Place spacer for wide characters.
395        if width == 2 {
396            let spacer_idx = usize::from(y) * grid_w + usize::from(x + 1);
397            if spacer_idx < cap {
398                let spacer = &mut lb.buf.as_mut()[spacer_idx];
399                spacer.glyph = ' ';
400                spacer.style = style;
401                spacer.extra = None;
402                spacer.flags = TileFlags::WIDE_CHAR_SPACER;
403            }
404        }
405    }
406
407    /// Clears wide-character cells that would be partially overwritten by a
408    /// write starting at `(x, y)` spanning `width` columns.
409    ///
410    /// Operates on layer 0.
411    #[cfg(feature = "egc")]
412    fn clear_overlap(&mut self, x: u16, y: u16, width: u16) {
413        let w = usize::from(self.width);
414        let cap = w * usize::from(self.height);
415        let lb = self.layer0_mut();
416        for cx in x..x.saturating_add(width) {
417            let idx = usize::from(y) * w + usize::from(cx);
418            if idx >= cap {
419                continue;
420            }
421            // flags is Copy, so reading through the shared ref is fine.
422            let flags = lb.buf.as_ref()[idx].flags;
423
424            if flags.contains(TileFlags::WIDE_CHAR_SPACER) && cx > 0 {
425                let pidx = usize::from(y) * w + usize::from(cx - 1);
426                if pidx < cap {
427                    lb.buf.as_mut()[pidx].reset();
428                }
429            }
430
431            if flags.contains(TileFlags::WIDE_CHAR) {
432                let sidx = usize::from(y) * w + usize::from(cx + 1);
433                if sidx < cap {
434                    lb.buf.as_mut()[sidx].reset();
435                }
436            }
437        }
438    }
439}
440
441// ---------------------------------------------------------------------------
442// Grid — multi-layer API
443// ---------------------------------------------------------------------------
444
445impl Grid {
446    /// Write a tile to `layer` at `(x, y)`.
447    ///
448    /// Allocates the layer if it has not been written to yet. Returns `None`
449    /// if `(x, y)` is out of bounds.
450    ///
451    /// To read back, use [`get_tile`](Self::get_tile).
452    pub fn put_tile(&mut self, layer: u8, x: u16, y: u16, tile: Tile) -> Option<()> {
453        let pos = to_grixy_pos(Pos::new(x, y));
454        let lb = self.layer_or_alloc(layer);
455        if !lb.buf.contains(pos) {
456            return None;
457        }
458        lb.buf[pos] = tile;
459        Some(())
460    }
461
462    /// Read a tile on `layer` at `(x, y)`, or `None` if the layer is
463    /// unallocated or the coordinates are out of bounds.
464    #[must_use]
465    pub fn get_tile(&self, layer: u8, x: u16, y: u16) -> Option<&Tile> {
466        let pos = to_grixy_pos(Pos::new(x, y));
467        self.layer(layer)?.buf.get(pos)
468    }
469
470    /// Yield `(layer_id, Pos, &Tile)` for every allocated cell across
471    /// all layers, in layer-major (0 → 255) then row-major order.
472    ///
473    /// Unallocated layers are skipped. This is used by backends that need
474    /// the full frame on every draw (see [`crate::Backend::needs_full_frame`]).
475    pub fn layers(&self) -> impl Iterator<Item = (u8, Pos, &Tile)> + '_ {
476        let mut results = Vec::new();
477        for id in 0u8..=255 {
478            if let Some(lb) = self.layer(id) {
479                #[allow(clippy::cast_possible_truncation)]
480                for (i, tile) in lb.buf.as_ref().iter().enumerate() {
481                    let x = (i % usize::from(self.width)) as u16;
482                    let y = (i / usize::from(self.width)) as u16;
483                    results.push((id, Pos::new(x, y), tile));
484                }
485            }
486        }
487        results.into_iter()
488    }
489
490    /// Clear every allocated layer.
491    pub fn clear_all(&mut self) {
492        for layer in self.layers.iter_mut().flatten() {
493            layer.buf.clear();
494        }
495    }
496
497    /// Yield `(layer_id, Pos, &Tile)` for every changed position across all
498    /// layers, in layer-major (0 → 255) then row-major order.
499    ///
500    /// Three cases per layer:
501    /// - Layer absent in `self`: nothing yielded.
502    /// - Layer in `self`, absent in `other` (newly allocated): all
503    ///   `width × height` tiles yielded.
504    /// - Layer in both: only positions where the `Tile` differs are yielded.
505    pub fn diff<'a>(&'a self, other: &'a Self) -> impl Iterator<Item = (u8, Pos, &'a Tile)> + 'a {
506        let mut results = Vec::new();
507        for id in 0u8..=255 {
508            match (self.layer(id), other.layer(id)) {
509                (None, _) => {}
510                (Some(cur), None) => {
511                    // Newly allocated layer: all cells are "changed".
512                    // TODO(M3): avoid Vec allocation, use Either-style iterator.
513                    #[allow(clippy::cast_possible_truncation)]
514                    for (i, tile) in cur.buf.as_ref().iter().enumerate() {
515                        let x = (i % usize::from(self.width)) as u16;
516                        let y = (i / usize::from(self.width)) as u16;
517                        results.push((id, Pos::new(x, y), tile));
518                    }
519                }
520                (Some(cur), Some(prev)) => {
521                    for (pos, tile) in cur.buf.diff(&prev.buf) {
522                        results.push((id, from_grixy_pos(pos), tile));
523                    }
524                }
525            }
526        }
527        results.into_iter()
528    }
529}
530
531// ---------------------------------------------------------------------------
532// Index / IndexMut — layer 0
533// ---------------------------------------------------------------------------
534
535impl Index<Pos> for Grid {
536    type Output = Tile;
537
538    fn index(&self, pos: Pos) -> &Tile {
539        &self.layer0().buf[to_grixy_pos(pos)]
540    }
541}
542
543impl IndexMut<Pos> for Grid {
544    fn index_mut(&mut self, pos: Pos) -> &mut Tile {
545        let pos = to_grixy_pos(pos);
546        &mut self.layer0_mut().buf[pos]
547    }
548}
549
550// ---------------------------------------------------------------------------
551// Display / Debug — layer 0
552// ---------------------------------------------------------------------------
553
554impl fmt::Display for Grid {
555    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
556        for y in 0..self.height() {
557            for x in 0..self.width() {
558                let tile = self.get(x, y);
559                #[cfg(feature = "egc")]
560                let is_spacer = tile.flags.contains(TileFlags::WIDE_CHAR_SPACER);
561                #[cfg(not(feature = "egc"))]
562                let is_spacer = tile.glyph == '\0';
563                let c = if is_spacer {
564                    ' ' // right half of a wide char — don't print twice
565                } else if tile.glyph == ' ' {
566                    '·' // empty cell marker
567                } else {
568                    tile.glyph
569                };
570                write!(f, "{c}")?;
571            }
572            writeln!(f)?;
573        }
574        Ok(())
575    }
576}
577
578impl fmt::Debug for Grid {
579    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
580        f.debug_struct("Grid")
581            .field("width", &self.width)
582            .field("height", &self.height)
583            .finish_non_exhaustive()
584    }
585}
586
587// ---------------------------------------------------------------------------
588// Tests
589// ---------------------------------------------------------------------------
590
591#[cfg(test)]
592mod tests {
593    use super::*;
594
595    // --- Existing tests (must pass unchanged) ---
596
597    #[test]
598    fn test_grid_new() {
599        let grid = Grid::new(80, 25);
600        assert_eq!(grid.width(), 80);
601        assert_eq!(grid.height(), 25);
602    }
603
604    #[test]
605    fn test_grid_put_get() {
606        let mut grid = Grid::new(10, 10);
607        let tile = Tile::default().with_glyph('X');
608
609        grid.put(5, 5, tile);
610        assert_eq!(grid.get(5, 5).glyph(), 'X');
611    }
612
613    #[test]
614    fn test_grid_checked_put_get() {
615        let mut grid = Grid::new(10, 10);
616        let tile = Tile::default().with_glyph('Y');
617
618        assert!(grid.checked_put(5, 5, tile).is_some());
619        assert_eq!(grid.checked_get(5, 5).unwrap().glyph(), 'Y');
620
621        assert!(grid.checked_get(10, 0).is_none());
622        assert!(grid.checked_put(0, 10, Tile::default()).is_none());
623    }
624
625    #[test]
626    #[should_panic(expected = "coordinates out of bounds")]
627    fn test_grid_panic_put() {
628        let mut grid = Grid::new(10, 10);
629        grid.put(10, 0, Tile::default());
630    }
631
632    #[test]
633    fn test_grid_diff() {
634        let mut g1 = Grid::new(2, 2);
635        let g2 = Grid::new(2, 2);
636
637        g1.put(0, 0, Tile::default().with_glyph('A'));
638
639        let diffs: Vec<_> = g1.diff(&g2).collect();
640        assert_eq!(diffs.len(), 1);
641        assert_eq!(diffs[0], (0, Pos::new(0, 0), g1.get(0, 0)));
642    }
643
644    #[test]
645    fn test_grid_resize_expand() {
646        let mut grid = Grid::new(3, 3);
647        grid.put(1, 1, Tile::default().with_glyph('X'));
648        grid.resize(6, 6);
649        assert_eq!(grid.width(), 6);
650        assert_eq!(grid.height(), 6);
651        assert_eq!(grid.get(1, 1).glyph(), 'X'); // preserved
652        assert_eq!(grid.get(5, 5).glyph(), ' '); // new cells default
653    }
654
655    #[test]
656    fn test_grid_resize_shrink() {
657        let mut grid = Grid::new(10, 10);
658        grid.put(1, 1, Tile::default().with_glyph('A'));
659        grid.resize(5, 5);
660        assert_eq!(grid.width(), 5);
661        assert_eq!(grid.height(), 5);
662        assert_eq!(grid.get(1, 1).glyph(), 'A'); // still in bounds, preserved
663    }
664
665    #[test]
666    fn test_grid_resize_preserves_overlap() {
667        let mut grid = Grid::new(4, 4);
668        grid.put(0, 0, Tile::default().with_glyph('@'));
669        grid.put(3, 3, Tile::default().with_glyph('X'));
670        grid.resize(3, 3); // shrink: (3,3) falls outside
671        assert_eq!(grid.get(0, 0).glyph(), '@');
672        assert_eq!(grid.get(2, 2).glyph(), ' '); // was default, still default
673    }
674
675    #[test]
676    fn test_grid_display() {
677        let mut grid = Grid::new(3, 2);
678        grid.put(0, 0, Tile::default().with_glyph('A'));
679
680        let s = alloc::format!("{grid}");
681        assert_eq!(s, "A··\n···\n");
682    }
683
684    #[test]
685    fn test_grid_cells_count() {
686        let grid = Grid::new(4, 3);
687        assert_eq!(grid.cells(0).unwrap().count(), 12);
688    }
689
690    #[test]
691    fn test_grid_cells_coordinates() {
692        let grid = Grid::new(3, 2);
693        let coords: Vec<(u16, u16)> = grid.cells(0).unwrap().map(|(x, y, _)| (x, y)).collect();
694        assert_eq!(
695            coords,
696            vec![(0, 0), (1, 0), (2, 0), (0, 1), (1, 1), (2, 1),]
697        );
698    }
699
700    #[test]
701    fn test_grid_cells_mut() {
702        use crate::style::Style;
703        let mut grid = Grid::new(2, 2);
704        for (x, y, tile) in grid.cells_mut(0) {
705            #[allow(clippy::cast_possible_truncation)]
706            let idx = (y * 2 + x) as u8;
707            *tile = Tile::new(char::from(b'A' + idx), Style::default());
708        }
709        assert_eq!(grid.get(0, 0).glyph(), 'A');
710        assert_eq!(grid.get(1, 0).glyph(), 'B');
711        assert_eq!(grid.get(0, 1).glyph(), 'C');
712        assert_eq!(grid.get(1, 1).glyph(), 'D');
713    }
714
715    #[test]
716    fn test_rect_contains() {
717        let r = Rect::new(2, 3, 4, 5);
718        assert!(r.contains_pos(Pos::new(2, 3)));
719        assert!(r.contains_pos(Pos::new(5, 7)));
720        assert!(!r.contains_pos(Pos::new(6, 3))); // x == x+width, exclusive
721        assert!(!r.contains_pos(Pos::new(2, 8))); // y == y+height, exclusive
722        assert!(!r.contains_pos(Pos::new(1, 3)));
723    }
724
725    #[test]
726    fn test_rect_area() {
727        assert_eq!(Rect::new(0, 0, 5, 3).area(), 15);
728        assert_eq!(Rect::default().area(), 0);
729    }
730
731    #[test]
732    fn test_rect_top_left_bottom_right() {
733        let r = Rect::new(1, 2, 3, 4);
734        assert_eq!(r.top_left(), Pos::new(1, 2));
735        assert_eq!(r.bottom_right(), Pos::new(4, 6));
736    }
737
738    #[test]
739    fn test_rect_intersects() {
740        let a = Rect::new(0, 0, 4, 4);
741        let b = Rect::new(2, 2, 4, 4);
742        let c = Rect::new(4, 0, 4, 4); // touches edge, no overlap
743        assert!(!a.intersect(b).is_empty());
744        assert!(a.intersect(c).is_empty());
745    }
746
747    #[test]
748    fn test_rect_positions() {
749        let r = Rect::new(1, 2, 2, 2);
750        let pts: Vec<Pos> = r.pos_iter().collect();
751        assert_eq!(
752            pts,
753            vec![
754                Pos::new(1, 2),
755                Pos::new(2, 2),
756                Pos::new(1, 3),
757                Pos::new(2, 3),
758            ]
759        );
760    }
761
762    #[test]
763    fn test_index_position() {
764        let mut grid = Grid::new(5, 5);
765        let pos = Pos::new(2, 3);
766        grid[pos] = Tile::default().with_glyph('Z');
767        assert_eq!(grid[pos].glyph(), 'Z');
768    }
769
770    #[test]
771    fn test_position_from_tuple() {
772        let p: Pos = (3u16, 7u16).into();
773        assert_eq!(p, Pos::new(3, 7));
774        let t: (u16, u16) = p.into();
775        assert_eq!(t, (3, 7));
776    }
777
778    #[test]
779    fn test_size_from_tuple() {
780        let s: Size = (80u16, 25u16).into();
781        assert_eq!(
782            s,
783            Size {
784                width: 80,
785                height: 25
786            }
787        );
788        let t: (u16, u16) = s.into();
789        assert_eq!(t, (80, 25));
790    }
791
792    #[test]
793    fn test_position_ord_row_major() {
794        let mut positions = vec![Pos::new(5, 0), Pos::new(0, 1), Pos::new(3, 0)];
795        positions.sort();
796        assert_eq!(
797            positions,
798            vec![Pos::new(3, 0), Pos::new(5, 0), Pos::new(0, 1),]
799        );
800    }
801
802    #[test]
803    fn test_size_ord() {
804        assert!(
805            Size {
806                width: 1,
807                height: 2
808            } < Size {
809                width: 2,
810                height: 1
811            }
812        );
813    }
814
815    // --- New tests for multi-layer API ---
816
817    #[test]
818    fn test_grid_layer_zero_always_allocated() {
819        let g = Grid::new(5, 5);
820        assert!(g.layer(0).is_some());
821        for id in 1u8..=5 {
822            assert!(g.layer(id).is_none(), "layer {id} should be None");
823        }
824    }
825
826    #[test]
827    fn test_grid_put_tile_allocates_layer() {
828        let mut g = Grid::new(5, 5);
829        g.put_tile(3, 0, 0, Tile::new('@', Style::default()));
830        assert!(g.layer(3).is_some());
831        assert!(g.layer(4).is_none());
832    }
833
834    #[test]
835    fn test_grid_diff_empty_when_identical() {
836        let g = Grid::new(5, 5);
837        let prev = Grid::new(5, 5);
838        assert_eq!(g.diff(&prev).count(), 0);
839    }
840
841    #[test]
842    fn test_grid_diff_reports_changed_cell() {
843        let mut cur = Grid::new(5, 5);
844        let prev = Grid::new(5, 5);
845        cur.put_tile(0, 2, 3, Tile::new('X', Style::default()));
846        let diffs: Vec<_> = cur.diff(&prev).collect();
847        assert_eq!(diffs.len(), 1);
848        assert_eq!(diffs[0].0, 0);
849        assert_eq!(diffs[0].1, Pos::new(2, 3));
850        assert_eq!(diffs[0].2.glyph, 'X');
851    }
852
853    #[test]
854    fn test_grid_diff_new_layer_yields_all_cells() {
855        let mut cur = Grid::new(3, 4);
856        let prev = Grid::new(3, 4);
857        cur.put_tile(1, 0, 0, Tile::new('A', Style::default()));
858        let diffs: Vec<_> = cur.diff(&prev).collect();
859        // All 12 cells of the newly allocated layer 1 are yielded.
860        assert_eq!(diffs.len(), 12);
861        assert!(diffs.iter().all(|(l, _, _)| *l == 1));
862    }
863
864    #[test]
865    fn test_grid_diff_layer_major_order() {
866        let mut cur = Grid::new(3, 3);
867        let prev = Grid::new(3, 3);
868        cur.put_tile(2, 0, 0, Tile::new('B', Style::default()));
869        cur.put_tile(0, 1, 0, Tile::new('A', Style::default()));
870        let layers: Vec<u8> = cur.diff(&prev).map(|(l, _, _)| l).collect();
871        // Layer 0's change appears first, then all of layer 2.
872        assert_eq!(layers[0], 0);
873        assert!(layers[1..].iter().all(|&l| l == 2));
874    }
875
876    #[test]
877    fn test_grid_put_and_get_on_layer_2() {
878        use crate::style::Style;
879        let mut g = Grid::new(5, 5);
880        g.put_tile(2, 1, 1, Tile::new('Z', Style::default()));
881        assert_eq!(g.get_tile(2, 1, 1).unwrap().glyph, 'Z');
882        // Layer 0 at same position should still be default.
883        assert_eq!(g.get(1, 1).glyph, ' ');
884        // Unallocated layer returns None.
885        assert!(g.get_tile(3, 0, 0).is_none());
886    }
887
888    #[test]
889    fn test_grid_clear_layer() {
890        let mut g = Grid::new(5, 5);
891        g.put_tile(1, 0, 0, Tile::new('Z', Style::default()));
892        g.put_tile(0, 0, 0, Tile::new('A', Style::default()));
893        g.clear(1);
894        assert_eq!(g.get_tile(0, 0, 0).unwrap().glyph, 'A');
895        assert!(g.get_tile(1, 0, 0).is_some());
896        assert_eq!(g.get_tile(1, 0, 0).unwrap().glyph, ' '); // cleared
897    }
898
899    #[test]
900    fn test_grid_clear_all() {
901        let mut g = Grid::new(5, 5);
902        g.put_tile(1, 0, 0, Tile::new('Z', Style::default()));
903        g.put_tile(0, 0, 0, Tile::new('A', Style::default()));
904        g.clear_all();
905        // Both layers reset to default (space).
906        assert_eq!(g.get(0, 0).glyph, ' ');
907        assert_eq!(g.get_tile(1, 0, 0).unwrap().glyph, ' ');
908    }
909}