pmacs/src/cell.rs

401 lines
13 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// cell.rs --- Cell-grid rendering target. TUI-shaped, GUI-extensible.
//! Cell-grid rendering target.
//!
//! Implements the cell-grid types from spec §3.3 (Cell Grid). The grid is a
//! `rows × cols` 2D array of [`Cell`]s; each cell carries a [`Glyph`], a
//! [`Style`], and an optional [`Attachment`]. The TUI ignores `Attachment`;
//! a future GUI backend interprets it.
//!
//! The full layout and helpers (composition, diffing) land in T M1.6. T M1.4
//! pulls in the public types so the [`crate::view::View`] trait can reference
//! them.
// ---------------------------------------------------------------------------
// Coordinates
// ---------------------------------------------------------------------------
/// Coordinate in the cell grid (row, col), measured in cells.
#[derive(Copy, Clone, Eq, PartialEq, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct CellCoord {
/// 0-based row.
pub row: u32,
/// 0-based column.
pub col: u32,
}
impl CellCoord {
/// Construct a cell coordinate.
#[must_use]
pub const fn new(row: u32, col: u32) -> Self {
Self { row, col }
}
}
/// Dimensions of a cell grid, measured in cells.
#[derive(Copy, Clone, Eq, PartialEq, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct CellSize {
/// Number of rows.
pub rows: u32,
/// Number of columns.
pub cols: u32,
}
impl CellSize {
/// Construct a cell size.
#[must_use]
pub const fn new(rows: u32, cols: u32) -> Self {
Self { rows, cols }
}
/// Number of cells in the grid (`rows * cols`).
#[must_use]
pub const fn area(self) -> u32 {
self.rows * self.cols
}
}
// ---------------------------------------------------------------------------
// Cell content
// ---------------------------------------------------------------------------
/// A glyph in a cell.
///
/// `Char` is the common case (single Unicode codepoint, single column).
/// `Cluster` carries a UTF-8 grapheme cluster spanning multiple codepoints
/// (e.g. emoji with modifiers, combining characters). `Continuation` is the
/// trailing column of a wide character: it has no glyph of its own; the
/// preceding cell's glyph occupies both columns.
#[derive(Clone, Eq, PartialEq, Debug, serde::Serialize, serde::Deserialize)]
pub enum Glyph {
/// A single Unicode codepoint occupying one column.
Char(char),
/// A grapheme cluster (one or more codepoints, encoded as UTF-8).
Cluster(Box<[u8]>),
/// The trailing column of a wide character. The preceding cell's glyph
/// renders into both columns; this cell's `glyph` and `style` are
/// ignored by frontends.
Continuation,
}
impl Default for Glyph {
fn default() -> Self {
Self::Char(' ')
}
}
/// A 24-bit RGB color, plus a `Default` sentinel meaning "use terminal
/// foreground/background".
#[derive(Copy, Clone, Eq, PartialEq, Debug, Default, serde::Serialize, serde::Deserialize)]
pub enum Color {
/// Use the terminal's default foreground or background.
#[default]
Default,
/// Truecolor RGB.
Rgb(u8, u8, u8),
/// 8-bit indexed terminal color (0..=255).
Indexed(u8),
}
/// Underline style.
#[derive(Copy, Clone, Eq, PartialEq, Debug, Default, serde::Serialize, serde::Deserialize)]
pub enum UnderlineStyle {
/// No underline.
#[default]
None,
/// Single straight underline.
Single,
/// Double underline.
Double,
/// Curly (wavy) underline, typical for diagnostics.
Curly,
/// Dotted underline.
Dotted,
/// Dashed underline.
Dashed,
}
/// Visual style applied to a cell.
#[derive(Copy, Clone, Eq, PartialEq, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct Style {
/// Foreground color.
pub fg: Color,
/// Background color.
pub bg: Color,
/// Bold.
pub bold: bool,
/// Italic.
pub italic: bool,
/// Underline.
pub underline: UnderlineStyle,
/// Reverse video.
pub reverse: bool,
}
/// A non-text attachment carried in a cell (TUI ignores this).
///
/// The TUI backend never inspects `Attachment`; a GUI backend interprets it
/// to render images, embedded widgets, and the like.
#[derive(Clone, Eq, PartialEq, Debug, serde::Serialize, serde::Deserialize)]
pub enum Attachment {
/// One cell of an image. The image is identified by `image_id` and the
/// cell's location within the image is `(sub_x, sub_y)`.
ImageCell {
/// Identifier into the frontend's image registry.
image_id: u32,
/// Sub-cell X offset.
sub_x: u16,
/// Sub-cell Y offset.
sub_y: u16,
},
}
/// One cell in the grid.
#[derive(Clone, Eq, PartialEq, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct Cell {
/// What is drawn in the cell.
pub glyph: Glyph,
/// How it is drawn.
pub style: Style,
/// Frontend-specific attachment (ignored by the TUI).
pub attachment: Option<Attachment>,
}
// ---------------------------------------------------------------------------
// Grid
// ---------------------------------------------------------------------------
/// A mutable view onto a row-major cell buffer.
///
/// The grid does not own its memory: the frontend owns a `Vec<Cell>` and
/// hands a borrow to views during render. Cells are addressed via
/// [`CellCoord`].
pub struct CellGrid<'a> {
/// Backing cell buffer, length `stride * size.rows` cells, row-major.
pub cells: &'a mut [Cell],
/// Stride in cells per row. Often equals `size.cols`, but allows the
/// frontend to keep extra columns for double-buffering.
pub stride: u32,
/// Visible rows × cols.
pub size: CellSize,
}
impl CellGrid<'_> {
/// Borrow the cell at `coord` mutably.
///
/// Panics if `coord` is outside the grid.
pub fn at(&mut self, coord: CellCoord) -> &mut Cell {
debug_assert!(coord.row < self.size.rows);
debug_assert!(coord.col < self.size.cols);
let idx = coord.row as usize * self.stride as usize + coord.col as usize;
&mut self.cells[idx]
}
/// Read the cell at `coord`.
///
/// Panics if `coord` is outside the grid.
#[must_use]
pub fn get(&self, coord: CellCoord) -> &Cell {
debug_assert!(coord.row < self.size.rows);
debug_assert!(coord.col < self.size.cols);
let idx = coord.row as usize * self.stride as usize + coord.col as usize;
&self.cells[idx]
}
/// Reset every visible cell to [`Cell::default`].
pub fn clear(&mut self) {
for row in 0..self.size.rows {
for col in 0..self.size.cols {
*self.at(CellCoord::new(row, col)) = Cell::default();
}
}
}
}
// ---------------------------------------------------------------------------
// Diff
// ---------------------------------------------------------------------------
/// A run of changed cells starting at one position.
///
/// Frontend translation: emit one cursor-move escape and then write the
/// cells in order. Wide characters appear as a leading `Char(_)` followed
/// by a [`Glyph::Continuation`] in the same span; the frontend consumes
/// both cells but only emits the leading glyph (the terminal handles the
/// width).
#[derive(Clone, Eq, PartialEq, Debug, serde::Serialize, serde::Deserialize)]
pub struct DiffSpan {
/// First cell of the span.
pub start: CellCoord,
/// New contents of the cells in the span, in row-major order. The
/// span occupies a contiguous run on `start.row`.
pub cells: Vec<Cell>,
}
/// Compute the diff between two cell buffers of identical layout.
///
/// `prev` and `next` are row-major slices, each of length at least
/// `stride * size.rows`; `stride` is the cells-per-row stride (typically
/// equals `size.cols`). The result is a list of [`DiffSpan`]s, one per
/// contiguous run of changed cells, in row-major order. Identical buffers
/// yield an empty `Vec`.
///
/// Threading: any thread.
#[must_use]
pub fn diff(prev: &[Cell], next: &[Cell], stride: u32, size: CellSize) -> Vec<DiffSpan> {
debug_assert!(prev.len() >= (stride as usize) * (size.rows as usize));
debug_assert!(next.len() >= (stride as usize) * (size.rows as usize));
let mut spans = Vec::new();
for row in 0..size.rows {
let row_offset = (row as usize) * (stride as usize);
let mut col = 0u32;
while col < size.cols {
let idx = row_offset + col as usize;
if prev[idx] == next[idx] {
col += 1;
continue;
}
let start = CellCoord::new(row, col);
let mut cells = Vec::new();
while col < size.cols {
let idx = row_offset + col as usize;
if prev[idx] == next[idx] {
break;
}
cells.push(next[idx].clone());
col += 1;
}
spans.push(DiffSpan { start, cells });
}
}
spans
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
fn ch(c: char) -> Cell {
Cell {
glyph: Glyph::Char(c),
style: Style::default(),
attachment: None,
}
}
fn cont() -> Cell {
Cell {
glyph: Glyph::Continuation,
style: Style::default(),
attachment: None,
}
}
#[test]
fn cell_grid_at_round_trip() {
let mut storage = vec![Cell::default(); 12];
let mut grid = CellGrid {
cells: &mut storage,
stride: 4,
size: CellSize::new(3, 4),
};
*grid.at(CellCoord::new(1, 2)) = ch('X');
assert_eq!(grid.get(CellCoord::new(1, 2)).glyph, Glyph::Char('X'));
// The cell at (1, 2) lives at offset row*stride + col = 1*4 + 2 = 6.
assert_eq!(storage[6].glyph, Glyph::Char('X'));
}
#[test]
fn cell_grid_clear_resets() {
let mut storage = vec![ch('Z'); 6];
let mut grid = CellGrid {
cells: &mut storage,
stride: 3,
size: CellSize::new(2, 3),
};
grid.clear();
assert!(storage.iter().all(|c| *c == Cell::default()));
}
// ----- diff -----
#[test]
fn diff_identical_is_empty() {
let prev = vec![ch('a'); 4];
let next = vec![ch('a'); 4];
assert!(diff(&prev, &next, 4, CellSize::new(1, 4)).is_empty());
}
#[test]
fn diff_single_cell_change() {
let prev = vec![ch('a'), ch('b'), ch('c'), ch('d')];
let next = vec![ch('a'), ch('B'), ch('c'), ch('d')];
let spans = diff(&prev, &next, 4, CellSize::new(1, 4));
assert_eq!(spans.len(), 1);
assert_eq!(spans[0].start, CellCoord::new(0, 1));
assert_eq!(spans[0].cells, vec![ch('B')]);
}
#[test]
fn diff_contiguous_run_is_one_span() {
let prev = vec![ch('a'), ch('b'), ch('c'), ch('d')];
let next = vec![ch('a'), ch('B'), ch('C'), ch('d')];
let spans = diff(&prev, &next, 4, CellSize::new(1, 4));
assert_eq!(spans.len(), 1);
assert_eq!(spans[0].start, CellCoord::new(0, 1));
assert_eq!(spans[0].cells, vec![ch('B'), ch('C')]);
}
#[test]
fn diff_split_by_unchanged_cell_is_two_spans() {
let prev = vec![ch('a'), ch('b'), ch('c'), ch('d')];
let next = vec![ch('A'), ch('b'), ch('C'), ch('d')];
let spans = diff(&prev, &next, 4, CellSize::new(1, 4));
assert_eq!(spans.len(), 2);
assert_eq!(spans[0].start, CellCoord::new(0, 0));
assert_eq!(spans[0].cells, vec![ch('A')]);
assert_eq!(spans[1].start, CellCoord::new(0, 2));
assert_eq!(spans[1].cells, vec![ch('C')]);
}
#[test]
fn diff_separate_rows_produce_separate_spans() {
// 2 rows × 3 cols, stride 3.
let prev = vec![ch('a'), ch('b'), ch('c'), ch('d'), ch('e'), ch('f')];
let next = vec![ch('a'), ch('B'), ch('c'), ch('d'), ch('e'), ch('F')];
let spans = diff(&prev, &next, 3, CellSize::new(2, 3));
assert_eq!(spans.len(), 2);
assert_eq!(spans[0].start, CellCoord::new(0, 1));
assert_eq!(spans[1].start, CellCoord::new(1, 2));
}
#[test]
fn diff_handles_wide_char_continuations() {
// Old: narrow 'a', 'b'. New: wide '中' + Continuation.
// Both cells differ, span includes both.
let prev = vec![ch('a'), ch('b'), ch('c')];
let next = vec![ch('中'), cont(), ch('c')];
let spans = diff(&prev, &next, 3, CellSize::new(1, 3));
assert_eq!(spans.len(), 1);
assert_eq!(spans[0].start, CellCoord::new(0, 0));
assert_eq!(spans[0].cells, vec![ch('中'), cont()]);
}
#[test]
fn diff_respects_stride_with_padding() {
// 1 row × 2 cols, but stride is 4 (extra padding columns ignored).
let prev = vec![ch('a'), ch('b'), ch('!'), ch('!')];
let next = vec![ch('A'), ch('b'), ch('?'), ch('?')];
let spans = diff(&prev, &next, 4, CellSize::new(1, 2));
// Padding columns 2..4 are not in size and must not be reported.
assert_eq!(spans.len(), 1);
assert_eq!(spans[0].start, CellCoord::new(0, 0));
assert_eq!(spans[0].cells, vec![ch('A')]);
}
}