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}