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