Skip to main content

retroglyph_core/grid/
mod.rs

1//! The layered tile grid: [`Grid`](crate::grid::Grid), plus the [`Size`](crate::grid::Size), [`Pos`](crate::grid::Pos), and [`Rect`](crate::grid::Rect)
2//! coordinate types used throughout the crate.
3//!
4//! # Layers, draw order, and compositing
5//!
6//! A [`Grid`](crate::grid::Grid) holds up to 256 independent layers (`u8` ids `0..=255`), one
7//! [`Tile`](crate::tile::Tile) per cell on each. Layer 0 is always allocated; layers 1-255 are
8//! allocated lazily, on first write to that layer (see
9//! [`put_tile`](crate::grid::Grid::put_tile)): a
10//! single-layer game pays zero overhead for layers it never writes to. This is the
11//! crate's most distinctive feature and the one most worth understanding
12//! before reaching for a second layer.
13//!
14//! Each cell carries a glyph, foreground/background [`Color`](crate::color::Color), and
15//! sub-cell pixel offsets. [`Color`](crate::color::Color) covers the full spectrum: the
16//! terminal's default foreground/background, the 16 standard ANSI colors, the 256-color
17//! palette, and 24-bit RGB.
18//! [`Style`](crate::color::Style) has no text modifiers (bold, italic, underline, ...);
19//! see its own doc comment for the full rationale.
20//!
21//! ## Draw order
22//!
23//! ```text
24//!   painted last  ->  layer 3   UI / effects     (topmost)
25//!                     layer 2   actors
26//!                     layer 1   items
27//!   painted first ->  layer 0   terrain          (always allocated)
28//!
29//!                     ascending id == ascending z; no separate depth field.
30//!                     each layer holds one Tile per cell of the whole grid.
31//! ```
32//!
33//! Layers composite bottom-to-top, in ascending id order: 0 first, then every
34//! allocated layer up to [`max_layer`](crate::grid::Grid::max_layer), each painted over
35//! whatever the layers below it produced. Layer id *is* z-order: there is
36//! no separate depth or z-index to set. A common convention is layer 0 for
37//! terrain, 1 for items, 2 for actors, 3+ for UI/effects, but the crate
38//! enforces nothing; any id can hold any content.
39//!
40//! For overlapping *UI* specifically (chrome, popups, debug overlays, as opposed to a tile map's
41//! own terrain/items/actors split), see [`crate::surface::Layer`] and
42//! [`Surface::on_tier`](crate::surface::Surface::on_tier) for a small named convention and why
43//! it beats ordering draw calls.
44//!
45//! Compositing itself happens in one of two places, chosen by the backend
46//! (see [`crate::backend::Output::composites_layers`]):
47//!
48//! - **Cell backends** (`Headless`, `retroglyph-crossterm`) do not composite
49//!   layers themselves. [`crate::terminal::Terminal::present`] calls
50//!   `flatten_into` (crate-private) to collapse every allocated layer
51//!   into a single-layer frame *before* handing it to the backend, so
52//!   layers 1+ behave identically on every cell backend.
53//! - **Pixel backends** (`retroglyph-software`) composite per pixel: they
54//!   receive the raw layered stream from
55//!   [`crate::backend::Output::draw_layers`] (layer-major, ascending id) and paint
56//!   each layer's cells directly onto the pixel buffer in that order.
57//!
58//! ## The `EMPTY` flag: transparency vs. opaque occlusion
59//!
60//! Every [`Tile`](crate::tile::Tile) carries [`TileFlags::EMPTY`], set on [`Tile::default`](crate::tile::Tile::default) and
61//! cleared by every write (`put_tile`, `write_grapheme`, indexing, ...).
62//! Compositing treats it as the transparency bit:
63//!
64//! - An **untouched cell** (`EMPTY` set) is fully transparent:
65//!   [`blit`](crate::grid::Grid::blit) skips it, and `flatten_into` (crate-private)
66//!   leaves whatever the layers below already drew.
67//! - An **explicit space** (`Tile::new(' ', style)`, `EMPTY` clear) is
68//!   opaque: it overwrites the glyph and foreground below it, same as any
69//!   other character. This is the one sharp edge in the model: `' '`
70//!   painted on a higher layer *erases* content underneath, it does not
71//!   reveal it.
72//!
73//! Background color follows its own rule, independent of `EMPTY`: a tile's
74//! background only overwrites the composited background when it is not
75//! [`Color::Default`](crate::color::Color::Default). A non-empty tile with a `Default`
76//! background still lets a lower layer's background show through even though its glyph is
77//! opaque. See `flatten_into` (crate-private) for the exact rule.
78//!
79//! ## Multi-cell spans
80//!
81//! [`write_span`](crate::grid::Grid::write_span) writes one piece of artwork across a `w x h` block of cells:
82//! the top-left cell is the **anchor** ([`TileFlags::SPAN_ANCHOR`], carrying the footprint), and
83//! every other cell is **covered** ([`TileFlags::SPAN_COVERED`], carrying its offset back to the
84//! anchor). [`span_owner`](crate::grid::Grid::span_owner) resolves any cell of a span to its anchor in O(1),
85//! so hit-testing a multi-cell sprite is one lookup rather than a rectangle scan.
86//!
87//! Covered cells keep **real glyphs**, and that is the point: they are the span's text fallback.
88//! One `write_span` call renders correctly on every backend without a capability check.
89//!
90//! - A **cell backend** ignores `SPAN_COVERED` and prints all `w * h` glyphs, so `["C=", "[]"]`
91//!   reads as a little piece of ASCII art.
92//! - A **pixel backend** looks the anchor glyph up in its sprite cache, draws that one sprite
93//!   across the whole footprint, and skips every covered cell's glyph.
94//!
95//! This is the deliberate difference from [`TileFlags::WIDE_CHAR_SPACER`], which every backend
96//! skips: a wide character's spacer has no content of its own, whereas a covered cell does.
97//!
98//! A span is written and cleared whole. Any ordinary write into one of its cells
99//! ([`put_tile`](crate::grid::Grid::put_tile), [`write_grapheme`](crate::grid::Grid::write_grapheme))
100//! clears the entire span first, so an anchor can never be left claiming cells it no longer owns.
101//! The exceptions are the escape hatches that hand out a `&mut Tile` directly
102//! ([`tile_mut`](crate::grid::Grid::tile_mut), `IndexMut`), which cannot intercept the
103//! write; use [`clear_span`](crate::grid::Grid::clear_span) first if you reach for one of those on a grid
104//! that uses spans.
105//!
106//! ## No short-circuiting: every allocated layer is visited, for every cell
107//!
108//! Compositing does not stop early when it hits an opaque tile on a high
109//! layer. Both `flatten_into` (crate-private) and the software
110//! backend's per-pixel compositor walk layers `0..=max_layer` in order for
111//! *every* cell, unconditionally, even if a fully opaque tile on layer 5
112//! makes layers 6-50 invisible at that position. An opaque high layer hides
113//! the layers below it visually but never occludes them from the pass, so
114//! prefer low, contiguous layer ids for frequently-updated content and
115//! reserve high ids for rarely-touched overlays (e.g. a debug HUD pinned to
116//! layer 255). See [`max_layer`](crate::grid::Grid::max_layer) for the iteration cost this
117//! implies and [`Grid::new`](crate::grid::Grid::new) for the allocation cost of a first write.
118
119use crate::color::Tint;
120use crate::tile::Tile;
121use crate::tile::TileFlags;
122use alloc::collections::BTreeMap;
123use alloc::sync::Arc;
124use alloc::vec::Vec;
125// Aliased rather than imported as `BlendMode`: this module already defines its own `BlendMode`
126// (below), and alpha-blend 0.3 renamed `blend_modes::SeparableBlendMode` to a top-level
127// `BlendMode` of its own, which would otherwise collide.
128use alpha_blend::BlendMode as SeparableBlendMode;
129use grixy::buf::GridBuf;
130use grixy::ops::layout::{LinearLayout, RowMajor};
131
132/// `.width()`/`.height()` accessors for [`Size`](crate::grid::Size) (and [`Rect`](crate::grid::Rect)): re-exported so callers don't need
133/// a direct `ixy` dependency just to call them on this crate's own type aliases.
134pub use ixy::HasSize;
135
136mod api;
137mod diff;
138mod layers;
139mod spans;
140mod trait_impls;
141
142/// Blend mode for [`Grid::blit_alpha`](crate::grid::Grid::blit_alpha), selecting how source and destination colors combine
143/// before the `fg_alpha`/`bg_alpha` factor is applied.
144///
145/// [`Linear`](Self::Linear) is a straight per-channel color lerp, delegated to [`gem::Mix`]. The
146/// remaining variants are the [W3C separable blend modes] libtcod also offers: each computes a
147/// fully blended color per channel via [`alpha_blend::BlendMode`] (imported here under its old
148/// name, [`SeparableBlendMode`], to avoid colliding with this module's own [`BlendMode`](crate::grid::BlendMode)), and
149/// *that* result is what gets lerped against the destination by the alpha factor, in place of the
150/// source color `Linear` would use.
151///
152/// [W3C separable blend modes]: https://www.w3.org/TR/compositing-1/#blending
153#[non_exhaustive]
154#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
155pub enum BlendMode {
156    /// Straight per-channel RGB lerp between destination and source.
157    #[default]
158    Linear,
159    /// Lightens: `dst + src - dst * src`. Always at least as light as either input.
160    Screen,
161    /// Brightens the destination to reflect the source (aka "color dodge").
162    Dodge,
163    /// Darkens the destination to reflect the source (aka "color burn").
164    Burn,
165    /// Multiplies or screens the colors, depending on the destination.
166    Overlay,
167    /// Darkens: `dst * src`. Always at least as dark as either input; the complement of Screen.
168    Multiply,
169}
170
171impl BlendMode {
172    /// The equivalent [`SeparableBlendMode`], or `None` for [`Linear`](Self::Linear) (which uses
173    /// [`gem::Mix`] instead: see [`blend_color`]).
174    const fn separable(self) -> Option<SeparableBlendMode> {
175        match self {
176            Self::Linear => None,
177            Self::Screen => Some(SeparableBlendMode::Screen),
178            Self::Dodge => Some(SeparableBlendMode::ColorDodge),
179            Self::Burn => Some(SeparableBlendMode::ColorBurn),
180            Self::Overlay => Some(SeparableBlendMode::Overlay),
181            Self::Multiply => Some(SeparableBlendMode::Multiply),
182        }
183    }
184}
185
186/// Size of the grid.
187///
188/// # Examples
189///
190/// ```
191/// use retroglyph_core::grid::Size;
192///
193/// let size = Size::new(80, 24);
194/// assert_eq!(size.width, 80);
195/// ```
196///
197/// This crate's `serde` feature forwards to [`ixy`]'s own `serde` feature, so `Size` gains
198/// `Serialize`/`Deserialize` from its upstream definition rather than one defined here.
199pub type Size = ixy::Size<u16>;
200
201/// Pos in the grid, in (x = column, y = row) order.
202///
203/// Implements [`Ord`] in row-major order (y primary, then x), which is the
204/// natural ordering for terminal rendering: top-to-bottom, left-to-right within
205/// each row.
206///
207/// # Examples
208///
209/// ```
210/// use retroglyph_core::grid::Pos;
211///
212/// let pos = Pos::new(2, 1);
213/// assert_eq!(pos.x, 2);
214/// assert_eq!(pos.y, 1);
215/// ```
216///
217/// This crate's `serde` feature forwards to [`ixy`]'s own `serde` feature, so `Pos` gains
218/// `Serialize`/`Deserialize` from its upstream definition rather than one defined here.
219pub type Pos = ixy::Pos<u16>;
220
221/// Rectangle in the grid.
222///
223/// # Examples
224///
225/// ```
226/// use retroglyph_core::grid::Rect;
227///
228/// let rect = Rect::new(0, 0, 10, 4);
229/// assert_eq!(rect.width(), 10);
230/// assert_eq!(rect.height(), 4);
231/// ```
232///
233/// This crate's `serde` feature forwards to [`ixy`]'s own `serde` feature, so `Rect` gains
234/// `Serialize`/`Deserialize` from its upstream definition rather than one defined here.
235pub type Rect = ixy::Rect<u16>;
236
237/// A sub-cell pixel offset `(dx, dy)`, distinct from [`Pos`] so a caller can't transpose a
238/// position and an offset in a call like [`Surface::put_offset`](crate::surface::Surface::put_offset).
239///
240/// Visual only: an offset shifts where a glyph is painted within its cell on backends that
241/// support sub-cell placement (e.g. `retroglyph-software`); it never changes which cell a glyph
242/// occupies, and cell-mode backends (e.g. `retroglyph-crossterm`) ignore it entirely.
243///
244/// This crate's `serde` feature adds `Serialize`/`Deserialize` impls for `Offset` directly (unlike
245/// [`Size`]/[`Pos`]/[`Rect`], which forward to [`ixy`]'s own `serde` feature).
246///
247/// # Examples
248///
249/// ```
250/// use retroglyph_core::grid::Offset;
251///
252/// let offset = Offset::new(3, -2);
253/// assert_eq!(offset.dx, 3);
254/// assert_eq!(offset.dy, -2);
255/// ```
256#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
257#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
258pub struct Offset {
259    /// Horizontal pixel offset.
260    pub dx: i16,
261    /// Vertical pixel offset.
262    pub dy: i16,
263}
264
265impl Offset {
266    /// Creates a new offset from `(dx, dy)`.
267    #[must_use]
268    pub const fn new(dx: i16, dy: i16) -> Self {
269        Self { dx, dy }
270    }
271}
272
273impl From<(i16, i16)> for Offset {
274    fn from((dx, dy): (i16, i16)) -> Self {
275        Self { dx, dy }
276    }
277}
278
279impl From<Offset> for (i16, i16) {
280    fn from(offset: Offset) -> Self {
281        (offset.dx, offset.dy)
282    }
283}
284
285// ---------------------------------------------------------------------------
286// Helpers: coordinate conversion between u16 and usize
287// ---------------------------------------------------------------------------
288
289fn to_grixy_pos(pos: Pos) -> grixy::core::Pos {
290    grixy::core::Pos::new(usize::from(pos.x), usize::from(pos.y))
291}
292
293/// Decodes a flat row-major buffer index into `(x, y)`, given the buffer's `width`.
294///
295/// Delegates to [`RowMajor`]'s [`LinearLayout::index_to_pos`](grixy::ops::layout::LinearLayout)
296/// instead of hand-rolling `i % width` / `i / width` at each flat-buffer iterator below.
297fn flat_index_to_xy(i: usize, width: usize) -> (u16, u16) {
298    let pos = RowMajor::index_to_pos(i, width);
299    #[allow(clippy::cast_possible_truncation)]
300    (pos.x as u16, pos.y as u16)
301}
302
303// ---------------------------------------------------------------------------
304// LayerBuf: a single layer's flat buffer
305// ---------------------------------------------------------------------------
306
307/// A single layer in the grid: a flat 2D buffer of one tile per cell.
308///
309/// Layer 0 is always allocated. Layers 1–255 are allocated on first write
310/// (see [`Grid::put_tile`](crate::grid::Grid::put_tile)).
311#[derive(Clone)]
312pub(crate) struct LayerBuf {
313    pub(crate) buf: GridBuf<Tile, Vec<Tile>, RowMajor>,
314    /// Sparse side-table: flat row-major index -> the cell's out-of-line data, for tiles with
315    /// [`TileFlags::HAS_EXTRA`] set. Empty until something writes a multi-codepoint grapheme or
316    /// a tint, which is what keeps [`Tile`](crate::tile::Tile) itself small (see
317    /// [`DrawCell::grapheme`](crate::backend::DrawCell::grapheme) and [`Grid::tint`](crate::grid::Grid::tint)).
318    ///
319    /// The `HAS_EXTRA` flag is authoritative: readers must check it before
320    /// consulting this map, since some write paths (`put_tile`,
321    /// `IndexMut`) can leave a stale entry behind when they
322    /// overwrite a tile that used to carry extra data without an explicit
323    /// cleanup call. Since those paths only ever hand out or store tiles
324    /// with `HAS_EXTRA` clear, a stale entry is harmless: it is simply
325    /// never looked up until the slot is reused by `write_grapheme` or `set_tint`, which
326    /// always overwrite it.
327    extras: BTreeMap<usize, TileExtra>,
328}
329
330/// One cell's out-of-line data: everything that belongs to a tile but does not fit in one.
331///
332/// [`Tile`](crate::tile::Tile) is exactly 20 bytes with no padding to spare, and both members here are rare enough
333/// per cell that inlining either would grow every tile of every layer to pay for a minority of
334/// them. They share one table, one flag, and one set of rekeying paths rather than each bringing
335/// their own.
336#[derive(Clone, Debug, Default, PartialEq, Eq)]
337pub(crate) struct TileExtra {
338    /// The full grapheme cluster, when [`Tile::glyph`](crate::tile::Tile::glyph) holds only its first codepoint.
339    pub(crate) grapheme: Option<Arc<str>>,
340    /// How a pixel backend recolours this cell's sprite.
341    pub(crate) tint: Tint,
342}
343
344impl TileExtra {
345    /// Whether this entry carries nothing, and so should be dropped rather than stored.
346    ///
347    /// Keeping the table free of empty entries is what lets `HAS_EXTRA` be set exactly when an
348    /// entry exists, instead of the flag and the table disagreeing about an all-default value.
349    fn is_empty(&self) -> bool {
350        self.grapheme.is_none() && self.tint == Tint::None
351    }
352}
353
354impl LayerBuf {
355    fn new(width: u16, height: u16) -> Self {
356        let n = usize::from(width) * usize::from(height);
357        Self {
358            buf: GridBuf::from_buffer(alloc::vec![Tile::default(); n], usize::from(width)),
359            extras: BTreeMap::new(),
360        }
361    }
362
363    /// Returns the side-table entry for the tile at flat index `idx`, or `None` if `tile`
364    /// doesn't have [`TileFlags::HAS_EXTRA`] set.
365    fn entry_for(&self, idx: usize, tile: &Tile) -> Option<&TileExtra> {
366        if tile.flags.contains(TileFlags::HAS_EXTRA) {
367            self.extras.get(&idx)
368        } else {
369            None
370        }
371    }
372
373    /// Returns the grapheme text for the tile at flat index `idx`, or `None`
374    /// if `tile` doesn't have [`TileFlags::HAS_EXTRA`] set.
375    fn extra_for(&self, idx: usize, tile: &Tile) -> Option<&str> {
376        self.entry_for(idx, tile)?.grapheme.as_deref()
377    }
378
379    /// Returns the tint for the tile at flat index `idx`, or [`Tint::None`](crate::color::Tint::None) if `tile` doesn't
380    /// have [`TileFlags::HAS_EXTRA`] set.
381    fn tint_for(&self, idx: usize, tile: &Tile) -> Tint {
382        self.entry_for(idx, tile).map_or(Tint::None, |e| e.tint)
383    }
384
385    /// Returns a clone of the side-table entry at flat index `idx`, or `None` if `tile` doesn't
386    /// have [`TileFlags::HAS_EXTRA`] set. Used to copy a cell's out-of-line data between grids
387    /// (e.g. [`Grid::blit`](crate::grid::Grid::blit)); the grapheme rides along as an `Arc` clone rather than a fresh
388    /// allocation.
389    fn extra_entry_for(&self, idx: usize, tile: &Tile) -> Option<TileExtra> {
390        self.entry_for(idx, tile).cloned()
391    }
392}
393
394// ---------------------------------------------------------------------------
395// Grid
396// ---------------------------------------------------------------------------
397
398/// A 2D buffer of [`Tile`](crate::tile::Tile)s, addressable across up to 256 stacked layers.
399///
400/// Layer 0 is always allocated; higher layers are allocated on first write, growing the
401/// layer-table `Vec` up to that layer's id as needed (see [`Grid::new`](crate::grid::Grid::new)). Single-layer use pays
402/// no overhead: layers 1+ stay unallocated until used, and the layer table itself never grows
403/// past a single slot.
404///
405/// # Out-of-bounds drawing
406///
407/// Drawing off the grid is a no-op, the same convention as drawing off-screen: every write method
408/// that names a position or region (e.g. [`put_tile`](Self::put_tile), [`write_grapheme`](Self::write_grapheme),
409/// [`write_span`](Self::write_span), [`blit`](Self::blit)) silently discards any part of the
410/// write that falls outside `0..width` / `0..height`, rather than panicking. The one deliberate
411/// exception is indexing (`Index<Pos>`/`IndexMut<Pos>`, and by extension anything built on it),
412/// which panics on an out-of-bounds `Pos` the same way indexing a slice does. Read accessors
413/// that take a position (e.g. [`tile`](Self::tile)) report an out-of-bounds position as `None`,
414/// indistinguishable from an unallocated layer.
415///
416/// Requires an allocator (backed by `alloc::vec::Vec`), so it is unavailable
417/// in strictly static, no-alloc environments.
418///
419/// # Examples
420///
421/// ```
422/// use retroglyph_core::color::{Color, Style};
423/// use retroglyph_core::grid::{Grid, Pos};
424///
425/// let mut grid = Grid::new(10, 5);
426/// grid.put_tile(0, Pos::new(2, 1), retroglyph_core::tile::Tile::new('@', Style::new().fg(Color::GREEN)));
427/// assert_eq!(grid[Pos::new(2, 1)].glyph(), '@');
428/// ```
429#[derive(Clone)]
430pub struct Grid {
431    width: u16,
432    height: u16,
433    /// Indexed by layer ID (0–255), but only as long as the highest layer id ever written to
434    /// (see [`layer_or_alloc`](Self::layer_or_alloc)), not always all 256 slots. Index 0 is
435    /// always `Some`. Unwritten layers within the current length are `None`; ids past the end
436    /// are treated identically to a `None` slot (see [`layer`](Self::layer)).
437    layers: Vec<Option<LayerBuf>>,
438    /// Highest layer ID that has been allocated. Always at least 0.
439    max_layer: u8,
440    /// Whether any multi-cell span has ever been written to this grid (see
441    /// [`write_span`](Self::write_span)).
442    ///
443    /// Conservative and one-way: set on the first `write_span`, never cleared. Every ordinary
444    /// write has to clear a span it would partially overwrite
445    /// (`clear_span_overlap`), and this flag is what keeps that check from
446    /// costing a buffer read per `put` in the overwhelmingly common grid that never uses a span
447    /// at all: it degrades to one `bool` test. Clearing it again on the last span's removal
448    /// would need span refcounting for no observable gain.
449    has_spans: bool,
450}
451
452// ---------------------------------------------------------------------------
453// Internal helpers
454// ---------------------------------------------------------------------------
455
456impl Grid {
457    /// Borrows a specific layer, or `None` if unallocated.
458    ///
459    /// `id` may be beyond the current layer-table `Vec`'s length: the table only grows as far
460    /// as the highest layer id ever written (see [`layer_or_alloc`](Self::layer_or_alloc)), so an
461    /// id past the end simply means "never written", same as an in-bounds `None` slot.
462    fn layer(&self, id: u8) -> Option<&LayerBuf> {
463        self.layers.get(usize::from(id))?.as_ref()
464    }
465
466    /// Borrows a specific layer mutably, allocating it if necessary.
467    ///
468    /// Grows the layer-table `Vec` up to `id + 1` slots on demand, rather than the table always
469    /// holding all 256 possible slots (see retroglyph#264): a `Grid` that only ever writes to
470    /// layer 0, or a handful of low ids, never pays for the 250+ slots it never touches.
471    ///
472    /// A first write allocates one `width x height` buffer of [`Tile`](crate::tile::Tile)s, the same cost regardless
473    /// of the layer's id, plus that one-time table growth up to `id + 1`. Writing to layer 200
474    /// first grows the table to 201 slots and allocates layer 200's buffer; the untouched slots
475    /// 1-199 in between are a cheap `None`. What the id costs afterward is steady-state iteration,
476    /// not allocation: see [`max_layer`](Self::max_layer).
477    fn layer_or_alloc(&mut self, id: u8) -> &mut LayerBuf {
478        let idx = usize::from(id);
479        if idx >= self.layers.len() {
480            self.layers.resize_with(idx + 1, || None);
481        }
482        if self.layers[idx].is_none() {
483            self.layers[idx] = Some(LayerBuf::new(self.width, self.height));
484        }
485        if id > self.max_layer {
486            self.max_layer = id;
487        }
488        self.layers[idx]
489            .as_mut()
490            .expect("idx was just allocated above if it wasn't already Some")
491    }
492
493    /// Borrows layer 0 (always allocated).
494    fn layer0(&self) -> &LayerBuf {
495        // INVARIANT: layer 0 is always `Some` (set in `new`).
496        self.layers[0]
497            .as_ref()
498            .expect("layer 0 is always Some (set in Grid::new)")
499    }
500
501    /// Borrows layer 0 mutably (always allocated).
502    fn layer0_mut(&mut self) -> &mut LayerBuf {
503        self.layers[0]
504            .as_mut()
505            .expect("layer 0 is always Some (set in Grid::new)")
506    }
507
508    /// Copy `layer` from `src` into `self` verbatim: the raw tile buffer (including every
509    /// flag, so [`TileFlags::SPAN_ANCHOR`]/
510    /// [`TileFlags::SPAN_COVERED`] survive) and every
511    /// extra (grapheme, tint), with no transparency rule skipping empty cells and no span
512    /// degradation.
513    ///
514    /// Unlike [`blit`](Self::blit), this is not a clipping/positioning copy: it requires `self`
515    /// and `src` to share the same dimensions and always writes `layer` at the same coordinates
516    /// it reads it from, so a caller can't use it to move or crop content, only to make one
517    /// grid's layer an exact replica of another's. That is exactly what [`crate::terminal::Terminal::present`]'s
518    /// `retain_layer` support needs: a retained layer has to be indistinguishable from what was
519    /// presented last frame, and `blit`'s clipping-copy contract (degrade spans to their text
520    /// fallback, treat empty tiles as transparent) is wrong for a copy that is supposed to be a
521    /// full replacement of identical geometry.
522    ///
523    /// If `layer` is unallocated on `src`, it becomes unallocated on `self` too (mirroring an
524    /// always-empty layer exactly). Layer 0 can never hit this case: it is always allocated on
525    /// every `Grid` (see [`Grid::new`](crate::grid::Grid::new)), on `src` as much as on `self`.
526    ///
527    /// # Panics
528    ///
529    /// Panics if `self` and `src` do not have the same dimensions.
530    pub(crate) fn copy_layer_from(&mut self, layer: u8, src: &Self) {
531        assert_eq!(
532            (self.width, self.height),
533            (src.width, src.height),
534            "copy_layer_from requires matching dimensions"
535        );
536        let idx = usize::from(layer);
537        match src.layers.get(idx).and_then(Option::as_ref) {
538            Some(src_lb) => {
539                if idx >= self.layers.len() {
540                    self.layers.resize_with(idx + 1, || None);
541                }
542                self.layers[idx] = Some(src_lb.clone());
543                if layer > self.max_layer {
544                    self.max_layer = layer;
545                }
546            }
547            None => {
548                if idx < self.layers.len() {
549                    self.layers[idx] = None;
550                }
551            }
552        }
553        self.has_spans |= src.has_spans;
554    }
555}
556
557/// Test-only readback for a tile's grapheme text, standing in for the removed
558/// `Grid::grapheme` (see retroglyph#1016): every real consumer reads this off the
559/// [`DrawCell`](crate::backend::DrawCell) stream returned by [`Grid::layers`], so tests do too.
560#[cfg(test)]
561pub(crate) fn grapheme_at(grid: &Grid, layer: u8, x: u16, y: u16) -> Option<&str> {
562    grid.layers()
563        .find(|c| c.layer == layer && c.pos == Pos::new(x, y))
564        .and_then(|c| c.grapheme)
565}
566
567#[cfg(test)]
568mod tests {
569    use super::*;
570
571    #[test]
572    fn contains_pos_is_true_for_edges_and_false_for_the_exclusive_far_edge() {
573        let r = Rect::new(2, 3, 4, 5);
574        assert!(r.contains_pos(Pos::new(2, 3)));
575        assert!(r.contains_pos(Pos::new(5, 7)));
576        assert!(!r.contains_pos(Pos::new(6, 3))); // x == x+width, exclusive
577        assert!(!r.contains_pos(Pos::new(2, 8))); // y == y+height, exclusive
578        assert!(!r.contains_pos(Pos::new(1, 3)));
579    }
580
581    #[test]
582    fn area_is_width_times_height_and_zero_for_default() {
583        assert_eq!(Rect::new(0, 0, 5, 3).area(), 15);
584        assert_eq!(Rect::default().area(), 0);
585    }
586
587    #[test]
588    fn top_left_and_bottom_right_reflect_position_and_size() {
589        let r = Rect::new(1, 2, 3, 4);
590        assert_eq!(r.top_left(), Pos::new(1, 2));
591        assert_eq!(r.bottom_right(), Pos::new(4, 6));
592    }
593
594    #[test]
595    fn intersect_is_non_empty_only_when_rects_overlap() {
596        let a = Rect::new(0, 0, 4, 4);
597        let b = Rect::new(2, 2, 4, 4);
598        let c = Rect::new(4, 0, 4, 4); // touches edge, no overlap
599        assert!(!a.intersect(b).is_empty());
600        assert!(a.intersect(c).is_empty());
601    }
602
603    #[test]
604    fn pos_iter_yields_every_position_row_major() {
605        use alloc::vec;
606        use alloc::vec::Vec;
607
608        let r = Rect::new(1, 2, 2, 2);
609        let pts: Vec<Pos> = r.pos_iter().collect();
610        assert_eq!(
611            pts,
612            vec![
613                Pos::new(1, 2),
614                Pos::new(2, 2),
615                Pos::new(1, 3),
616                Pos::new(2, 3),
617            ]
618        );
619    }
620
621    #[test]
622    fn pos_converts_to_and_from_a_tuple() {
623        let p: Pos = (3u16, 7u16).into();
624        assert_eq!(p, Pos::new(3, 7));
625        let t: (u16, u16) = p.into();
626        assert_eq!(t, (3, 7));
627    }
628
629    #[test]
630    fn size_converts_to_and_from_a_tuple() {
631        let s: Size = (80u16, 25u16).into();
632        assert_eq!(s, Size::new(80, 25));
633        let t: (u16, u16) = s.into();
634        assert_eq!(t, (80, 25));
635    }
636
637    #[test]
638    fn offset_converts_to_and_from_a_tuple() {
639        let o: Offset = (-3i16, 7i16).into();
640        assert_eq!(o, Offset::new(-3, 7));
641        let t: (i16, i16) = o.into();
642        assert_eq!(t, (-3, 7));
643    }
644
645    #[test]
646    fn offset_default_is_zero() {
647        assert_eq!(Offset::default(), Offset::new(0, 0));
648    }
649
650    #[test]
651    fn pos_ord_sorts_row_major() {
652        use alloc::vec;
653
654        let mut positions = vec![Pos::new(5, 0), Pos::new(0, 1), Pos::new(3, 0)];
655        positions.sort();
656        assert_eq!(
657            positions,
658            vec![Pos::new(3, 0), Pos::new(5, 0), Pos::new(0, 1),]
659        );
660    }
661
662    #[test]
663    fn size_orders_by_width_then_height() {
664        assert!(Size::new(1, 2) < Size::new(2, 1));
665    }
666
667    #[cfg(feature = "serde")]
668    #[test]
669    fn size_serializes_and_deserializes() {
670        let size = Size::new(80, 25);
671        let json = serde_json::to_string(&size).expect("serialize");
672        assert_eq!(
673            serde_json::from_str::<Size>(&json).expect("deserialize"),
674            size
675        );
676    }
677
678    #[cfg(feature = "serde")]
679    #[test]
680    fn pos_and_rect_serialize_via_ixy() {
681        let pos = Pos::new(2, 1);
682        let json = serde_json::to_string(&pos).expect("serialize");
683        assert_eq!(
684            serde_json::from_str::<Pos>(&json).expect("deserialize"),
685            pos
686        );
687
688        let rect = Rect::new(0, 0, 10, 4);
689        let json = serde_json::to_string(&rect).expect("serialize");
690        assert_eq!(
691            serde_json::from_str::<Rect>(&json).expect("deserialize"),
692            rect
693        );
694    }
695
696    #[cfg(feature = "serde")]
697    #[test]
698    fn offset_serializes_and_deserializes() {
699        let offset = Offset::new(3, -2);
700        let json = serde_json::to_string(&offset).expect("serialize");
701        assert_eq!(
702            serde_json::from_str::<Offset>(&json).expect("deserialize"),
703            offset
704        );
705    }
706}