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}