use super::geometry::Rect;
pub use smelt_style::style::{Color, Style};
pub(crate) fn to_crossterm_color(c: Color) -> crossterm::style::Color {
use crossterm::style::Color as X;
match c {
Color::Reset => X::Reset,
Color::Black => X::Black,
Color::DarkGrey => X::DarkGrey,
Color::Red => X::Red,
Color::DarkRed => X::DarkRed,
Color::Green => X::Green,
Color::DarkGreen => X::DarkGreen,
Color::Yellow => X::Yellow,
Color::DarkYellow => X::DarkYellow,
Color::Blue => X::Blue,
Color::DarkBlue => X::DarkBlue,
Color::Magenta => X::Magenta,
Color::DarkMagenta => X::DarkMagenta,
Color::Cyan => X::Cyan,
Color::DarkCyan => X::DarkCyan,
Color::White => X::White,
Color::Grey => X::Grey,
Color::Rgb { r, g, b } => X::Rgb { r, g, b },
Color::AnsiValue(v) => X::AnsiValue(v),
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TextAlign {
Left,
Center,
Right,
}
pub fn display_width(text: &str) -> u16 {
use unicode_width::UnicodeWidthStr;
UnicodeWidthStr::width(text).min(u16::MAX as usize) as u16
}
pub fn truncate_width(text: &str, max_width: u16) -> String {
let mut out = String::new();
write_text(0, max_width, text, |_, ch| out.push(ch));
out
}
pub(crate) fn char_width(ch: char) -> u16 {
use unicode_width::UnicodeWidthChar;
UnicodeWidthChar::width(ch).unwrap_or(1).max(1) as u16
}
fn write_text(mut col: u16, limit: u16, text: &str, mut write: impl FnMut(u16, char)) -> u16 {
for ch in text.chars() {
let width = char_width(ch);
if col.saturating_add(width) > limit {
break;
}
write(col, ch);
col = col.saturating_add(width);
}
col.min(limit)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Cell {
pub symbol: char,
pub style: Style,
}
impl Default for Cell {
fn default() -> Self {
Self {
symbol: ' ',
style: Style::default(),
}
}
}
#[derive(Clone)]
pub struct Grid {
cells: Vec<Cell>,
width: u16,
height: u16,
}
impl Grid {
pub fn new(width: u16, height: u16) -> Self {
let len = width as usize * height as usize;
Self {
cells: vec![Cell::default(); len],
width,
height,
}
}
pub fn width(&self) -> u16 {
self.width
}
pub fn height(&self) -> u16 {
self.height
}
pub fn resize(&mut self, width: u16, height: u16) {
self.width = width;
self.height = height;
self.cells
.resize(width as usize * height as usize, Cell::default());
self.clear_all();
}
pub fn cell(&self, x: u16, y: u16) -> &Cell {
&self.cells[self.idx(x, y)]
}
pub fn cell_mut(&mut self, x: u16, y: u16) -> Option<&mut Cell> {
if x < self.width && y < self.height {
let idx = self.idx(x, y);
Some(&mut self.cells[idx])
} else {
None
}
}
pub fn set(&mut self, x: u16, y: u16, symbol: char, style: Style) {
self.write_cell(x, y, Cell { symbol, style });
}
fn write_cell(&mut self, x: u16, y: u16, new_cell: Cell) {
use unicode_width::UnicodeWidthChar;
if x >= self.width || y >= self.height {
return;
}
let idx = self.idx(x, y);
let old_symbol = self.cells[idx].symbol;
if x + 1 < self.width && UnicodeWidthChar::width(old_symbol).unwrap_or(1) == 2 {
let cont = self.idx(x + 1, y);
if self.cells[cont].symbol == '\0' {
self.cells[cont] = Cell {
symbol: ' ',
style: new_cell.style,
};
}
}
if old_symbol == '\0' && x > 0 {
let lead = self.idx(x - 1, y);
if UnicodeWidthChar::width(self.cells[lead].symbol).unwrap_or(1) == 2 {
self.cells[lead] = Cell {
symbol: ' ',
style: new_cell.style,
};
}
}
self.cells[idx] = new_cell;
if UnicodeWidthChar::width(new_cell.symbol).unwrap_or(1) == 2 && x + 1 < self.width {
let cont = self.idx(x + 1, y);
let displaced = self.cells[cont].symbol;
if UnicodeWidthChar::width(displaced).unwrap_or(1) == 2 && x + 2 < self.width {
let dispcont = self.idx(x + 2, y);
if self.cells[dispcont].symbol == '\0' {
self.cells[dispcont] = Cell {
symbol: ' ',
style: new_cell.style,
};
}
}
self.cells[cont] = Cell {
symbol: '\0',
style: new_cell.style,
};
}
}
pub fn put_str(&mut self, x: u16, y: u16, text: &str, style: Style) -> u16 {
if y >= self.height {
return x.min(self.width);
}
write_text(x, self.width, text, |col, ch| self.set(col, y, ch, style))
}
pub fn put_char(&mut self, x: u16, y: u16, symbol: char, fg: Color) {
if x >= self.width || y >= self.height {
return;
}
let idx = self.idx(x, y);
let mut style = self.cells[idx].style;
style.fg = Some(fg);
self.write_cell(x, y, Cell { symbol, style });
}
pub fn put_str_fg(&mut self, x: u16, y: u16, text: &str, fg: Color) -> u16 {
if y >= self.height {
return x.min(self.width);
}
write_text(x, self.width, text, |col, ch| self.put_char(col, y, ch, fg))
}
pub fn put_line(&mut self, x: u16, y: u16, line: &crate::line::Line<'_>) -> u16 {
let mut col = x;
for span in &line.spans {
if col >= self.width {
break;
}
let before = col;
col = self.put_str(col, y, span.text.as_ref(), span.style);
if col == before {
break;
}
}
col.min(self.width)
}
pub fn fill(&mut self, area: Rect, symbol: char, style: Style) {
let new_cell = Cell { symbol, style };
for row in area.top..area.bottom().min(self.height) {
for col in area.left..area.right().min(self.width) {
self.write_cell(col, row, new_cell);
}
}
}
pub fn clear(&mut self, area: Rect) {
self.fill(area, ' ', Style::default());
}
pub fn clear_all(&mut self) {
for cell in &mut self.cells {
*cell = Cell::default();
}
}
pub fn slice_mut(&mut self, area: Rect) -> GridSlice<'_> {
let area = Rect::new(
area.top.min(self.height),
area.left.min(self.width),
area.width.min(self.width.saturating_sub(area.left)),
area.height.min(self.height.saturating_sub(area.top)),
);
GridSlice { grid: self, area }
}
pub fn diff<'a>(&'a self, prev: &'a Grid) -> impl Iterator<Item = CellUpdate<'a>> {
self.cells.iter().enumerate().filter_map(move |(i, cell)| {
if cell.symbol == '\0' {
return None;
}
let prev_cell = prev.cells.get(i)?;
if cell != prev_cell {
let x = (i % self.width as usize) as u16;
let y = (i / self.width as usize) as u16;
Some(CellUpdate { x, y, cell })
} else {
None
}
})
}
pub fn swap_with(&mut self, other: &mut Grid) {
std::mem::swap(&mut self.cells, &mut other.cells);
std::mem::swap(&mut self.width, &mut other.width);
std::mem::swap(&mut self.height, &mut other.height);
}
fn idx(&self, x: u16, y: u16) -> usize {
y as usize * self.width as usize + x as usize
}
}
pub struct CellUpdate<'a> {
pub x: u16,
pub y: u16,
pub cell: &'a Cell,
}
pub struct GridSlice<'a> {
grid: &'a mut Grid,
area: Rect,
}
impl<'a> GridSlice<'a> {
pub fn width(&self) -> u16 {
self.area.width
}
pub fn height(&self) -> u16 {
self.area.height
}
pub fn grid_rect(&self) -> Rect {
self.area
}
pub fn to_grid_rect(&self, rect: Rect) -> Rect {
rect.to_grid(self.area)
}
pub fn to_local_rect(&self, rect: Rect) -> Rect {
rect.to_local(self.area)
}
pub fn slice_mut(&mut self, area: Rect) -> GridSlice<'_> {
let parent = Rect::new(0, 0, self.area.width, self.area.height);
let clipped = area.clip_to(parent);
let area = Rect::new(
self.area.top.saturating_add(clipped.top),
self.area.left.saturating_add(clipped.left),
clipped.width,
clipped.height,
);
GridSlice {
grid: self.grid,
area,
}
}
pub fn set(&mut self, x: u16, y: u16, symbol: char, style: Style) {
if x < self.area.width && y < self.area.height {
self.grid
.set(self.area.left + x, self.area.top + y, symbol, style);
}
}
pub fn cell(&self, x: u16, y: u16) -> Cell {
if x < self.area.width && y < self.area.height {
*self.grid.cell(self.area.left + x, self.area.top + y)
} else {
Cell::default()
}
}
pub fn cell_mut(&mut self, x: u16, y: u16) -> Option<&mut Cell> {
if x < self.area.width && y < self.area.height {
self.grid.cell_mut(self.area.left + x, self.area.top + y)
} else {
None
}
}
pub fn put_str(&mut self, x: u16, y: u16, text: &str, style: Style) -> u16 {
self.write_str_at(x, y, text, None, style)
}
fn write_str_at(
&mut self,
x: u16,
y: u16,
text: &str,
max_width: Option<u16>,
style: Style,
) -> u16 {
if y >= self.area.height {
return x.min(self.area.width);
}
let abs_y = self.area.top + y;
let limit = x
.saturating_add(max_width.unwrap_or(u16::MAX))
.min(self.area.width);
write_text(x, limit, text, |col, ch| {
self.grid.set(self.area.left + col, abs_y, ch, style)
})
}
pub fn put_padded(&mut self, x: u16, y: u16, width: u16, text: &str, style: Style) -> u16 {
if y >= self.area.height {
return x.min(self.area.width);
}
let end = x.saturating_add(width).min(self.area.width);
let mut col = self
.write_str_at(x, y, text, Some(end.saturating_sub(x)), style)
.min(end);
while col < end {
self.set(col, y, ' ', style);
col = col.saturating_add(1);
}
end
}
pub fn put_str_aligned(
&mut self,
x: u16,
y: u16,
width: u16,
text: &str,
align: TextAlign,
style: Style,
) -> u16 {
if y >= self.area.height {
return x.min(self.area.width);
}
let width = width.min(self.area.width.saturating_sub(x));
let text_width = display_width(text).min(width);
let col = match align {
TextAlign::Left => x,
TextAlign::Center => x.saturating_add(width.saturating_sub(text_width) / 2),
TextAlign::Right => x.saturating_add(width.saturating_sub(text_width)),
};
self.put_str(col, y, text, style)
}
pub fn fill_row(&mut self, y: u16, style: Style) {
self.fill(Rect::new(y, 0, self.area.width, 1), ' ', style);
}
pub fn rule_h(&mut self, y: u16, style: Style) {
self.rule_h_range(0, y, self.area.width, style);
}
pub fn rule_v(&mut self, x: u16, style: Style) {
self.rule_v_range(x, 0, self.area.height, style);
}
pub fn rule_h_range(&mut self, x: u16, y: u16, width: u16, style: Style) {
if y >= self.area.height || x >= self.area.width {
return;
}
let end = x.saturating_add(width).min(self.area.width);
for col in x..end {
self.set(col, y, '─', style);
}
}
pub fn rule_v_range(&mut self, x: u16, y: u16, height: u16, style: Style) {
if x >= self.area.width || y >= self.area.height {
return;
}
let end = y.saturating_add(height).min(self.area.height);
for row in y..end {
self.set(x, row, '│', style);
}
}
pub fn put_char(&mut self, x: u16, y: u16, symbol: char, fg: Color) {
if x < self.area.width && y < self.area.height {
self.grid
.put_char(self.area.left + x, self.area.top + y, symbol, fg);
}
}
pub fn put_line(&mut self, x: u16, y: u16, line: &crate::line::Line<'_>) -> u16 {
if y >= self.area.height {
return x.min(self.area.width);
}
let mut col = x;
for span in &line.spans {
if col >= self.area.width {
break;
}
let before = col;
col = self.put_str(col, y, span.text.as_ref(), span.style);
if col == before {
break;
}
}
col.min(self.area.width)
}
pub fn put_str_fg(&mut self, x: u16, y: u16, text: &str, fg: Color) -> u16 {
if y >= self.area.height {
return x.min(self.area.width);
}
let abs_y = self.area.top + y;
write_text(x, self.area.width, text, |col, ch| {
self.grid.put_char(self.area.left + col, abs_y, ch, fg)
})
}
pub fn fill(&mut self, area: Rect, symbol: char, style: Style) {
let abs = Rect::new(
self.area.top + area.top,
self.area.left + area.left,
area.width.min(self.area.width.saturating_sub(area.left)),
area.height.min(self.area.height.saturating_sub(area.top)),
);
self.grid.fill(abs, symbol, style);
}
pub fn clear(&mut self) {
self.grid.clear(self.area);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_grid_filled_with_spaces() {
let grid = Grid::new(10, 5);
assert_eq!(grid.width(), 10);
assert_eq!(grid.height(), 5);
assert_eq!(grid.cell(0, 0).symbol, ' ');
assert_eq!(grid.cell(9, 4).symbol, ' ');
}
#[test]
fn set_and_read_cell() {
let mut grid = Grid::new(10, 5);
let style = Style::new().fg(Color::Red);
grid.set(3, 2, 'X', style);
assert_eq!(grid.cell(3, 2).symbol, 'X');
assert_eq!(grid.cell(3, 2).style.fg, Some(Color::Red));
}
#[test]
fn put_str_writes_chars() {
let mut grid = Grid::new(10, 5);
grid.put_str(2, 1, "hello", Style::default());
assert_eq!(grid.cell(2, 1).symbol, 'h');
assert_eq!(grid.cell(3, 1).symbol, 'e');
assert_eq!(grid.cell(6, 1).symbol, 'o');
assert_eq!(grid.cell(7, 1).symbol, ' ');
}
#[test]
fn put_str_clips_at_width() {
let mut grid = Grid::new(5, 1);
grid.put_str(3, 0, "hello", Style::default());
assert_eq!(grid.cell(3, 0).symbol, 'h');
assert_eq!(grid.cell(4, 0).symbol, 'e');
}
#[test]
fn fill_region() {
let mut grid = Grid::new(10, 5);
let style = Style::new().bg(Color::Blue);
grid.fill(Rect::new(1, 2, 3, 2), '#', style);
assert_eq!(grid.cell(2, 1).symbol, '#');
assert_eq!(grid.cell(4, 2).symbol, '#');
assert_eq!(grid.cell(5, 1).symbol, ' ');
}
#[test]
fn diff_yields_changed_cells() {
let prev = Grid::new(5, 3);
let mut curr = Grid::new(5, 3);
curr.set(1, 0, 'A', Style::default());
curr.set(3, 2, 'B', Style::default());
let updates: Vec<_> = curr.diff(&prev).collect();
assert_eq!(updates.len(), 2);
assert_eq!((updates[0].x, updates[0].y), (1, 0));
assert_eq!((updates[1].x, updates[1].y), (3, 2));
}
#[test]
fn diff_empty_for_identical_grids() {
let a = Grid::new(5, 3);
let b = Grid::new(5, 3);
assert_eq!(a.diff(&b).count(), 0);
}
#[test]
fn slice_writes_offset_correctly() {
let mut grid = Grid::new(20, 10);
let area = Rect::new(2, 5, 10, 4);
{
let mut slice = grid.slice_mut(area);
assert_eq!(slice.width(), 10);
assert_eq!(slice.height(), 4);
slice.set(0, 0, 'A', Style::default());
slice.put_str(1, 1, "hi", Style::default());
}
assert_eq!(grid.cell(5, 2).symbol, 'A');
assert_eq!(grid.cell(6, 3).symbol, 'h');
assert_eq!(grid.cell(7, 3).symbol, 'i');
}
#[test]
fn slice_clips_to_bounds() {
let mut grid = Grid::new(10, 5);
let mut slice = grid.slice_mut(Rect::new(0, 0, 3, 2));
slice.put_str(0, 0, "hello world", Style::default());
assert_eq!(grid.cell(2, 0).symbol, 'l');
assert_eq!(grid.cell(3, 0).symbol, ' ');
}
#[test]
fn slice_put_str_returns_clipped_end_column() {
let mut grid = Grid::new(6, 1);
let mut slice = grid.slice_mut(Rect::new(0, 0, 6, 1));
let end = slice.put_str(4, 0, "abc", Style::default());
assert_eq!(end, 6);
assert_eq!(grid.cell(4, 0).symbol, 'a');
assert_eq!(grid.cell(5, 0).symbol, 'b');
}
#[test]
fn slice_put_str_counts_wide_chars() {
let mut grid = Grid::new(6, 1);
let mut slice = grid.slice_mut(Rect::new(0, 0, 6, 1));
let end = slice.put_str(1, 0, "a語b", Style::default());
assert_eq!(end, 5);
assert_eq!(grid.cell(1, 0).symbol, 'a');
assert_eq!(grid.cell(2, 0).symbol, '語');
assert_eq!(grid.cell(3, 0).symbol, '\0');
assert_eq!(grid.cell(4, 0).symbol, 'b');
}
#[test]
fn slice_put_padded_fills_remaining_width() {
let mut grid = Grid::new(8, 1);
let style = Style::new().bg(Color::Blue);
let mut slice = grid.slice_mut(Rect::new(0, 0, 8, 1));
let end = slice.put_padded(1, 0, 4, "hi", style);
assert_eq!(end, 5);
assert_eq!(grid.cell(1, 0).symbol, 'h');
assert_eq!(grid.cell(2, 0).symbol, 'i');
assert_eq!(grid.cell(3, 0).symbol, ' ');
assert_eq!(grid.cell(4, 0).style.bg, Some(Color::Blue));
}
#[test]
fn slice_put_padded_does_not_split_wide_chars() {
let mut grid = Grid::new(4, 1);
let mut slice = grid.slice_mut(Rect::new(0, 0, 4, 1));
let end = slice.put_padded(0, 0, 2, "a語", Style::default());
assert_eq!(end, 2);
assert_eq!(grid.cell(0, 0).symbol, 'a');
assert_eq!(grid.cell(1, 0).symbol, ' ');
assert_eq!(grid.cell(2, 0).symbol, ' ');
}
#[test]
fn slice_put_str_aligned_places_text() {
let mut grid = Grid::new(12, 3);
let mut slice = grid.slice_mut(Rect::new(0, 0, 12, 3));
slice.put_str_aligned(2, 0, 8, "ab", TextAlign::Left, Style::default());
slice.put_str_aligned(2, 1, 8, "ab", TextAlign::Center, Style::default());
slice.put_str_aligned(2, 2, 8, "ab", TextAlign::Right, Style::default());
assert_eq!(grid.cell(2, 0).symbol, 'a');
assert_eq!(grid.cell(5, 1).symbol, 'a');
assert_eq!(grid.cell(8, 2).symbol, 'a');
}
#[test]
fn slice_put_str_aligned_ignores_rows_outside_slice() {
let mut grid = Grid::new(8, 2);
let mut slice = grid.slice_mut(Rect::new(0, 0, 8, 2));
let end = slice.put_str_aligned(2, 9, 4, "nope", TextAlign::Left, Style::default());
assert_eq!(end, 2);
assert_eq!(grid.cell(2, 0).symbol, ' ');
}
#[test]
fn slice_rules_clip_safely() {
let mut grid = Grid::new(4, 3);
let mut slice = grid.slice_mut(Rect::new(0, 0, 4, 3));
slice.rule_h_range(2, 1, 99, Style::default());
slice.rule_v_range(3, 1, 99, Style::default());
slice.rule_h(9, Style::default());
slice.rule_v(9, Style::default());
assert_eq!(grid.cell(2, 1).symbol, '─');
assert_eq!(grid.cell(3, 1).symbol, '│');
assert_eq!(grid.cell(3, 2).symbol, '│');
}
#[test]
fn slice_mut_is_relative_to_parent() {
let mut grid = Grid::new(20, 10);
let mut parent = grid.slice_mut(Rect::new(2, 5, 10, 5));
{
let mut child = parent.slice_mut(Rect::new(1, 2, 3, 2));
assert_eq!(child.grid_rect(), Rect::new(3, 7, 3, 2));
child.set(0, 0, 'x', Style::default());
}
assert_eq!(grid.cell(7, 3).symbol, 'x');
}
#[test]
fn slice_mut_clips_fully_outside_child_to_empty() {
let mut grid = Grid::new(20, 10);
let mut parent = grid.slice_mut(Rect::new(2, 5, 10, 5));
let child = parent.slice_mut(Rect::new(99, 99, 3, 2));
assert_eq!(child.grid_rect(), Rect::new(7, 15, 0, 0));
}
#[test]
fn truncate_width_respects_display_width() {
assert_eq!(display_width("a語b"), 4);
assert_eq!(truncate_width("a語b", 3), "a語");
assert_eq!(truncate_width("a語b", 2), "a");
}
#[test]
fn resize_clears_grid() {
let mut grid = Grid::new(5, 3);
grid.set(2, 1, 'A', Style::default());
grid.resize(10, 5);
assert_eq!(grid.width(), 10);
assert_eq!(grid.height(), 5);
assert_eq!(grid.cell(2, 1).symbol, ' ');
}
#[test]
fn put_char_preserves_bg_and_attrs() {
let mut grid = Grid::new(10, 3);
let base = Style::new().fg(Color::Yellow).bg(Color::Blue).bold();
grid.set(2, 1, '#', base);
grid.put_char(2, 1, 'X', Color::Red);
let cell = grid.cell(2, 1);
assert_eq!(cell.symbol, 'X');
assert_eq!(cell.style.fg, Some(Color::Red));
assert_eq!(cell.style.bg, Some(Color::Blue));
assert!(cell.style.bold);
}
#[test]
fn put_char_on_empty_cell_leaves_bg_none() {
let mut grid = Grid::new(5, 2);
grid.put_char(0, 0, 'A', Color::Green);
let cell = grid.cell(0, 0);
assert_eq!(cell.symbol, 'A');
assert_eq!(cell.style.fg, Some(Color::Green));
assert_eq!(cell.style.bg, None);
}
#[test]
fn put_line_paints_spans_with_their_styles() {
use crate::line::{Line, Span};
let mut grid = Grid::new(15, 1);
let red = Style::new().fg(Color::Red);
let line = Line::from_spans([Span::raw("ab"), Span::styled("CD", red), Span::raw("ef")]);
grid.put_line(1, 0, &line);
assert_eq!(grid.cell(1, 0).symbol, 'a');
assert_eq!(grid.cell(1, 0).style.fg, None);
assert_eq!(grid.cell(3, 0).symbol, 'C');
assert_eq!(grid.cell(3, 0).style.fg, Some(Color::Red));
assert_eq!(grid.cell(5, 0).symbol, 'e');
assert_eq!(grid.cell(5, 0).style.fg, None);
}
#[test]
fn slice_put_line_clips_at_right_edge() {
use crate::line::{Line, Span};
let mut grid = Grid::new(10, 1);
{
let mut slice = grid.slice_mut(Rect::new(0, 2, 5, 1));
slice.put_line(0, 0, &Line::from_spans([Span::raw("abcdefgh")]));
}
assert_eq!(grid.cell(2, 0).symbol, 'a');
assert_eq!(grid.cell(6, 0).symbol, 'e');
assert_eq!(grid.cell(7, 0).symbol, ' ');
}
#[test]
fn slice_put_str_fg_preserves_bg() {
let mut grid = Grid::new(10, 2);
grid.fill(Rect::new(0, 0, 6, 1), ' ', Style::new().bg(Color::Cyan));
{
let mut slice = grid.slice_mut(Rect::new(0, 0, 10, 1));
slice.put_str_fg(1, 0, "hi", Color::Red);
}
assert_eq!(grid.cell(1, 0).symbol, 'h');
assert_eq!(grid.cell(1, 0).style.fg, Some(Color::Red));
assert_eq!(grid.cell(1, 0).style.bg, Some(Color::Cyan));
assert_eq!(grid.cell(2, 0).style.bg, Some(Color::Cyan));
}
#[test]
fn swap_grids() {
let mut a = Grid::new(5, 3);
let mut b = Grid::new(5, 3);
a.set(0, 0, 'A', Style::default());
b.set(0, 0, 'B', Style::default());
a.swap_with(&mut b);
assert_eq!(a.cell(0, 0).symbol, 'B');
assert_eq!(b.cell(0, 0).symbol, 'A');
}
#[test]
fn set_wide_char_marks_next_cell_as_continuation() {
let mut grid = Grid::new(5, 1);
grid.set(1, 0, '漢', Style::default());
assert_eq!(grid.cell(1, 0).symbol, '漢');
assert_eq!(grid.cell(2, 0).symbol, '\0');
}
#[test]
fn put_str_lays_wide_chars_two_columns_apart() {
let mut grid = Grid::new(10, 1);
grid.put_str(0, 0, "a漢b", Style::default());
assert_eq!(grid.cell(0, 0).symbol, 'a');
assert_eq!(grid.cell(1, 0).symbol, '漢');
assert_eq!(grid.cell(3, 0).symbol, 'b');
}
#[test]
fn wide_char_continuation_is_marked_consistently_across_paths() {
let via_set = {
let mut g = Grid::new(5, 1);
g.set(0, 0, '漢', Style::default());
g
};
let via_put_str = {
let mut g = Grid::new(5, 1);
g.put_str(0, 0, "漢", Style::default());
g
};
let via_put_char = {
let mut g = Grid::new(5, 1);
g.put_char(0, 0, '漢', Color::Reset);
g
};
assert_eq!(via_set.cell(1, 0).symbol, via_put_str.cell(1, 0).symbol);
assert_eq!(via_set.cell(1, 0).symbol, via_put_char.cell(1, 0).symbol);
}
#[test]
fn diff_does_not_emit_update_for_cell_under_a_wide_char() {
let mut prev = Grid::new(5, 1);
prev.set(1, 0, 'X', Style::default());
let mut curr = Grid::new(5, 1);
curr.put_str(0, 0, "漢", Style::default());
let updates: Vec<_> = curr.diff(&prev).collect();
let cols: Vec<u16> = updates.iter().map(|u| u.x).collect();
assert_eq!(
cols,
vec![0],
"expected one update at the wide char's column only; got {cols:?}"
);
}
#[test]
fn slice_put_str_lays_wide_chars_two_columns_apart() {
let mut grid = Grid::new(10, 1);
{
let mut slice = grid.slice_mut(Rect::new(0, 0, 10, 1));
slice.put_str(0, 0, "a漢b", Style::default());
}
assert_eq!(grid.cell(0, 0).symbol, 'a');
assert_eq!(grid.cell(1, 0).symbol, '漢');
assert_eq!(grid.cell(3, 0).symbol, 'b');
}
#[test]
fn overwriting_wide_char_with_narrow_clears_continuation_marker() {
let mut grid = Grid::new(5, 1);
grid.set(0, 0, '漢', Style::default());
assert_eq!(grid.cell(1, 0).symbol, '\0');
grid.set(0, 0, 'A', Style::default());
assert_ne!(
grid.cell(1, 0).symbol,
'\0',
"narrow overwrite must clear stale continuation marker"
);
}
#[test]
fn overwriting_wide_continuation_with_narrow_clears_leading_wide_glyph() {
let mut grid = Grid::new(5, 1);
grid.set(0, 0, '漢', Style::default());
assert_eq!(grid.cell(0, 0).symbol, '漢');
assert_eq!(grid.cell(1, 0).symbol, '\0');
grid.set(1, 0, 'A', Style::default());
assert_ne!(
grid.cell(0, 0).symbol,
'漢',
"writing to a wide char's continuation must break the leading glyph"
);
}
#[test]
fn diff_clears_terminal_when_wide_is_overwritten_with_narrow_in_same_frame() {
let mut prev = Grid::new(5, 1);
prev.set(0, 0, '漢', Style::default());
let mut curr = Grid::new(5, 1);
curr.set(0, 0, '漢', Style::default()); curr.set(0, 0, 'A', Style::default());
let updates: Vec<_> = curr.diff(&prev).collect();
let cols: Vec<u16> = updates.iter().map(|u| u.x).collect();
assert!(
cols.contains(&1),
"expected diff to clear the orphaned continuation at col 1; got {cols:?}"
);
}
fn assert_grid_invariants(grid: &Grid) {
use unicode_width::UnicodeWidthChar;
for y in 0..grid.height() {
for x in 0..grid.width() {
let cell = grid.cell(x, y);
if cell.symbol == '\0' {
assert!(x > 0, "continuation at column 0 has no leading cell");
let lead = grid.cell(x - 1, y).symbol;
assert_eq!(
UnicodeWidthChar::width(lead).unwrap_or(1),
2,
"orphaned continuation at ({x}, {y}); leading cell symbol is {lead:?}"
);
}
if UnicodeWidthChar::width(cell.symbol).unwrap_or(1) == 2 && x + 1 < grid.width() {
let cont = grid.cell(x + 1, y).symbol;
assert_eq!(
cont, '\0',
"wide char at ({x}, {y}) is missing continuation; got {cont:?}"
);
}
}
}
}
#[test]
fn put_char_narrow_over_wide_keeps_invariant() {
let mut grid = Grid::new(5, 1);
grid.set(0, 0, '漢', Style::default());
grid.put_char(0, 0, 'A', Color::Red);
assert_grid_invariants(&grid);
assert_eq!(grid.cell(0, 0).symbol, 'A');
assert_ne!(grid.cell(1, 0).symbol, '\0');
}
#[test]
fn put_char_on_continuation_breaks_leading_wide() {
let mut grid = Grid::new(5, 1);
grid.set(0, 0, '漢', Style::default());
grid.put_char(1, 0, 'B', Color::Green);
assert_grid_invariants(&grid);
assert_ne!(grid.cell(0, 0).symbol, '漢');
assert_eq!(grid.cell(1, 0).symbol, 'B');
}
#[test]
fn fill_partially_overlapping_a_wide_char_keeps_invariant() {
let mut grid = Grid::new(6, 1);
grid.set(3, 0, '漢', Style::default());
grid.fill(Rect::new(0, 0, 4, 1), '#', Style::default());
assert_grid_invariants(&grid);
assert_eq!(grid.cell(3, 0).symbol, '#');
assert_ne!(grid.cell(4, 0).symbol, '\0');
}
#[test]
fn fill_starting_on_a_continuation_breaks_the_leading_wide() {
let mut grid = Grid::new(5, 1);
grid.set(0, 0, '漢', Style::default());
grid.fill(Rect::new(0, 1, 2, 1), '#', Style::default());
assert_grid_invariants(&grid);
assert_ne!(grid.cell(0, 0).symbol, '漢');
assert_eq!(grid.cell(1, 0).symbol, '#');
}
#[test]
fn writing_wide_over_a_wide_displaces_the_old_continuation() {
let mut grid = Grid::new(6, 1);
grid.set(0, 0, '漢', Style::default());
grid.set(2, 0, '字', Style::default());
grid.set(1, 0, '日', Style::default());
assert_grid_invariants(&grid);
assert_eq!(grid.cell(1, 0).symbol, '日');
assert_eq!(grid.cell(2, 0).symbol, '\0');
assert_ne!(grid.cell(0, 0).symbol, '漢');
assert_ne!(grid.cell(3, 0).symbol, '\0');
}
#[test]
fn clear_all_holds_invariant_after_arbitrary_paint() {
let mut grid = Grid::new(8, 2);
grid.set(0, 0, '漢', Style::default());
grid.set(3, 0, '字', Style::default());
grid.put_str(0, 1, "a漢b", Style::default());
grid.clear_all();
assert_grid_invariants(&grid);
for y in 0..2 {
for x in 0..8 {
assert_eq!(grid.cell(x, y).symbol, ' ');
}
}
}
#[test]
fn diff_emits_continuation_clear_when_wide_was_overwritten() {
let mut prev = Grid::new(6, 1);
prev.set(2, 0, '漢', Style::default());
let mut curr = Grid::new(6, 1);
curr.set(2, 0, '漢', Style::default());
curr.set(2, 0, 'A', Style::default());
let updates: Vec<_> = curr.diff(&prev).collect();
let cols: Vec<u16> = updates.iter().map(|u| u.x).collect();
assert!(
cols.contains(&3),
"expected diff to clear orphaned continuation at col 3; got {cols:?}"
);
assert_grid_invariants(&curr);
}
#[test]
fn slice_fill_partial_wide_overlap_keeps_invariant() {
let mut grid = Grid::new(10, 1);
grid.set(4, 0, '漢', Style::default());
{
let mut slice = grid.slice_mut(Rect::new(0, 0, 5, 1));
slice.fill(Rect::new(0, 0, 5, 1), '#', Style::default());
}
assert_grid_invariants(&grid);
}
#[test]
fn put_str_fg_over_wide_keeps_invariant() {
let mut grid = Grid::new(6, 1);
grid.set(1, 0, '漢', Style::default());
grid.put_str_fg(0, 0, "abc", Color::Red);
assert_grid_invariants(&grid);
assert_eq!(grid.cell(0, 0).symbol, 'a');
assert_eq!(grid.cell(1, 0).symbol, 'b');
assert_eq!(grid.cell(2, 0).symbol, 'c');
}
#[test]
fn put_str_breaks_when_wide_char_would_overflow() {
let mut grid = Grid::new(4, 1);
grid.put_str(0, 0, "abc漢", Style::default());
assert_eq!(grid.cell(0, 0).symbol, 'a');
assert_eq!(grid.cell(1, 0).symbol, 'b');
assert_eq!(grid.cell(2, 0).symbol, 'c');
assert_eq!(grid.cell(3, 0).symbol, ' ');
}
#[test]
fn diff_picks_up_style_only_change() {
let mut prev = Grid::new(5, 1);
prev.set(0, 0, 'X', Style::default());
let mut curr = Grid::new(5, 1);
curr.set(0, 0, 'X', Style::new().fg(Color::Red));
let updates: Vec<_> = curr.diff(&prev).collect();
assert_eq!(updates.len(), 1);
assert_eq!(updates[0].cell.style.fg, Some(Color::Red));
}
#[test]
fn set_out_of_bounds_is_silent_noop() {
let mut grid = Grid::new(3, 2);
grid.set(99, 99, 'X', Style::default());
assert_eq!(grid.cell(0, 0).symbol, ' ');
assert_eq!(grid.cell(2, 1).symbol, ' ');
}
#[test]
fn put_str_skips_when_y_out_of_bounds() {
let mut grid = Grid::new(5, 2);
grid.put_str(0, 99, "hello", Style::default());
assert_eq!(grid.cell(0, 0).symbol, ' ');
}
}