use crate::color::Color;
#[cfg(feature = "egc")]
use crate::style::Style;
use crate::tile::Tile;
use crate::tile::TileFlags;
#[cfg(feature = "egc")]
use crate::tile::cap_grapheme;
use alloc::vec::Vec;
use core::fmt;
use core::ops::{Index, IndexMut};
use grixy::buf::GridBuf;
use grixy::ops::layout::RowMajor;
use grixy::ops::{ExactSizeGrid, GridDiff, GridRead, GridWrite};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
pub struct Size {
pub width: u16,
pub height: u16,
}
pub type Pos = ixy::Pos<u16>;
pub type Rect = ixy::Rect<u16>;
impl From<(u16, u16)> for Size {
fn from((width, height): (u16, u16)) -> Self {
Self { width, height }
}
}
impl From<Size> for (u16, u16) {
fn from(s: Size) -> Self {
(s.width, s.height)
}
}
fn to_grixy_pos(pos: Pos) -> grixy::core::Pos {
grixy::core::Pos::new(usize::from(pos.x), usize::from(pos.y))
}
#[allow(clippy::missing_const_for_fn)]
fn from_grixy_pos(pos: grixy::core::Pos) -> Pos {
#[allow(clippy::cast_possible_truncation)]
Pos::new(pos.x as u16, pos.y as u16)
}
pub struct Cells<'a> {
iter: core::iter::Enumerate<core::slice::Iter<'a, Tile>>,
width: usize,
}
impl<'a> Iterator for Cells<'a> {
type Item = (u16, u16, &'a Tile);
fn next(&mut self) -> Option<Self::Item> {
self.iter.next().map(|(i, tile)| {
#[allow(clippy::cast_possible_truncation)]
let x = (i % self.width) as u16;
#[allow(clippy::cast_possible_truncation)]
let y = (i / self.width) as u16;
(x, y, tile)
})
}
}
pub struct CellsMut<'a> {
iter: core::iter::Enumerate<core::slice::IterMut<'a, Tile>>,
width: usize,
}
impl<'a> Iterator for CellsMut<'a> {
type Item = (u16, u16, &'a mut Tile);
fn next(&mut self) -> Option<Self::Item> {
self.iter.next().map(|(i, tile)| {
#[allow(clippy::cast_possible_truncation)]
let x = (i % self.width) as u16;
#[allow(clippy::cast_possible_truncation)]
let y = (i / self.width) as u16;
(x, y, tile)
})
}
}
pub(crate) struct LayerBuf {
pub(crate) buf: GridBuf<Tile, Vec<Tile>, RowMajor>,
}
impl LayerBuf {
fn new(width: u16, height: u16) -> Self {
let n = usize::from(width) * usize::from(height);
Self {
buf: GridBuf::from_buffer(alloc::vec![Tile::default(); n], usize::from(width)),
}
}
}
pub struct Grid {
width: u16,
height: u16,
layers: Vec<Option<LayerBuf>>,
max_layer: u8,
}
impl Grid {
fn layer(&self, id: u8) -> Option<&LayerBuf> {
self.layers[usize::from(id)].as_ref()
}
fn layer_or_alloc(&mut self, id: u8) -> &mut LayerBuf {
let idx = usize::from(id);
if self.layers[idx].is_none() {
self.layers[idx] = Some(LayerBuf::new(self.width, self.height));
}
if id > self.max_layer {
self.max_layer = id;
}
self.layers[idx].as_mut().unwrap()
}
fn layer0(&self) -> &LayerBuf {
self.layers[0].as_ref().unwrap()
}
fn layer0_mut(&mut self) -> &mut LayerBuf {
self.layers[0].as_mut().unwrap()
}
}
impl Grid {
#[must_use]
pub fn new(width: u16, height: u16) -> Self {
let mut layers = alloc::vec![];
layers.resize_with(256, || None);
layers[0] = Some(LayerBuf::new(width, height));
Self {
width,
height,
layers,
max_layer: 0,
}
}
#[must_use]
pub fn from_charmap<F>(map: &str, mut f: F) -> Self
where
F: FnMut(char) -> Tile,
{
let mut width: u16 = 0;
let mut height: u16 = 0;
for line in map.lines() {
let len = u16::try_from(line.chars().count()).unwrap_or(u16::MAX);
width = width.max(len);
height = height.saturating_add(1);
}
let mut grid = Self::new(width, height);
for (y, line) in map.lines().enumerate() {
#[allow(clippy::cast_possible_truncation)]
let y = y as u16;
for (x, ch) in line.chars().enumerate() {
#[allow(clippy::cast_possible_truncation)]
let x = x as u16;
grid.put_tile(0, x, y, f(ch));
}
}
grid
}
#[must_use]
pub const fn width(&self) -> u16 {
self.width
}
#[must_use]
pub const fn height(&self) -> u16 {
self.height
}
#[must_use]
pub const fn max_layer(&self) -> u8 {
self.max_layer
}
pub fn put(&mut self, x: u16, y: u16, tile: Tile) {
let pos = to_grixy_pos(Pos::new(x, y));
let lb = self.layer0_mut();
assert!(
lb.buf.contains(pos),
"coordinates out of bounds: ({x}, {y})"
);
lb.buf[pos] = tile;
}
#[must_use]
pub fn get(&self, x: u16, y: u16) -> &Tile {
&self.layer0().buf[to_grixy_pos(Pos::new(x, y))]
}
pub fn checked_put(&mut self, x: u16, y: u16, tile: Tile) -> Option<()> {
let pos = to_grixy_pos(Pos::new(x, y));
let lb = self.layer0_mut();
if lb.buf.contains(pos) {
lb.buf[pos] = tile;
Some(())
} else {
None
}
}
#[must_use]
pub fn checked_get(&self, x: u16, y: u16) -> Option<&Tile> {
let pos = to_grixy_pos(Pos::new(x, y));
self.layer0().buf.get(pos)
}
pub fn checked_get_mut(&mut self, x: u16, y: u16) -> Option<&mut Tile> {
let pos = to_grixy_pos(Pos::new(x, y));
self.layer0_mut().buf.get_mut(pos)
}
#[must_use]
pub fn cells(&self, layer: u8) -> Option<Cells<'_>> {
let lb = self.layer(layer)?;
Some(Cells {
iter: lb.buf.as_ref().iter().enumerate(),
width: usize::from(self.width),
})
}
pub fn cells_mut(&mut self, layer: u8) -> CellsMut<'_> {
let width = usize::from(self.width);
let lb = self.layer_or_alloc(layer);
CellsMut {
iter: lb.buf.as_mut().iter_mut().enumerate(),
width,
}
}
pub fn clear(&mut self, layer: u8) {
if let Some(lb) = self.layers[usize::from(layer)].as_mut() {
lb.buf.clear();
}
}
pub fn resize(&mut self, width: u16, height: u16) {
self.width = width;
self.height = height;
for layer in self.layers.iter_mut().flatten() {
layer.buf.resize(usize::from(width), usize::from(height));
}
}
#[cfg(feature = "egc")]
pub fn write_grapheme(&mut self, layer: u8, x: u16, y: u16, grapheme: &str, style: Style) {
use unicode_width::UnicodeWidthStr;
let width = u16::try_from(grapheme.width()).expect("grapheme width exceeds u16");
if width == 0 {
return;
}
let w = usize::from(self.width);
let cap = w * usize::from(self.height);
let idx = usize::from(y) * w + usize::from(x);
if idx >= cap {
return;
}
if width == 2 && x.saturating_add(1) as usize >= w {
return;
}
self.clear_overlap(layer, x, y, width);
let grid_w = usize::from(self.width);
let idx = usize::from(y) * grid_w + usize::from(x);
let lb = self.layer_or_alloc(layer);
let mut chars = grapheme.chars();
let first = chars.next().unwrap_or(' ');
let has_extra = chars.next().is_some();
let extra = if has_extra {
Some(alloc::sync::Arc::new(cap_grapheme(grapheme)))
} else {
None
};
let flags = if width == 2 {
TileFlags::WIDE_CHAR
} else {
TileFlags::empty()
};
lb.buf.as_mut()[idx].glyph = first;
lb.buf.as_mut()[idx].style = style;
lb.buf.as_mut()[idx].extra = extra;
lb.buf.as_mut()[idx].flags = flags;
if width == 2 {
let spacer_idx = usize::from(y) * grid_w + usize::from(x + 1);
if spacer_idx < cap {
let spacer = &mut lb.buf.as_mut()[spacer_idx];
spacer.glyph = ' ';
spacer.style = style;
spacer.extra = None;
spacer.flags = TileFlags::WIDE_CHAR_SPACER;
}
}
}
#[cfg(feature = "egc")]
fn clear_overlap(&mut self, layer: u8, x: u16, y: u16, width: u16) {
let w = usize::from(self.width);
let cap = w * usize::from(self.height);
let lb = self.layer_or_alloc(layer);
for cx in x..x.saturating_add(width) {
let idx = usize::from(y) * w + usize::from(cx);
if idx >= cap {
continue;
}
let flags = lb.buf.as_ref()[idx].flags;
if flags.contains(TileFlags::WIDE_CHAR_SPACER) && cx > 0 {
let pidx = usize::from(y) * w + usize::from(cx - 1);
if pidx < cap {
lb.buf.as_mut()[pidx].reset();
}
}
if flags.contains(TileFlags::WIDE_CHAR) {
let sidx = usize::from(y) * w + usize::from(cx + 1);
if sidx < cap {
lb.buf.as_mut()[sidx].reset();
}
}
}
}
}
impl Grid {
pub fn put_tile(&mut self, layer: u8, x: u16, y: u16, tile: Tile) -> Option<()> {
let pos = to_grixy_pos(Pos::new(x, y));
let lb = self.layer_or_alloc(layer);
if !lb.buf.contains(pos) {
return None;
}
lb.buf[pos] = tile;
Some(())
}
#[must_use]
pub fn get_tile(&self, layer: u8, x: u16, y: u16) -> Option<&Tile> {
let pos = to_grixy_pos(Pos::new(x, y));
self.layer(layer)?.buf.get(pos)
}
pub fn blit(&mut self, layer: u8, src: &Self, src_rect: Rect, dst_x: u16, dst_y: u16) {
for sy in src_rect.top()..src_rect.bottom() {
for sx in src_rect.left()..src_rect.right() {
let Some(tile) = src.get_tile(layer, sx, sy) else {
continue;
};
if !tile.flags.contains(TileFlags::EMPTY) {
let dx = dst_x + sx.saturating_sub(src_rect.left());
let dy = dst_y + sy.saturating_sub(src_rect.top());
self.put_tile(layer, dx, dy, tile.clone());
}
}
}
}
#[cfg(feature = "gem")]
#[allow(clippy::too_many_arguments, clippy::float_cmp)]
pub fn blit_alpha(
&mut self,
layer: u8,
src: &Self,
src_rect: Rect,
dst_x: u16,
dst_y: u16,
fg_alpha: f32,
bg_alpha: f32,
) {
for sy in src_rect.top()..src_rect.bottom() {
for sx in src_rect.left()..src_rect.right() {
let Some(tile) = src.get_tile(layer, sx, sy) else {
continue;
};
if !tile.flags.contains(TileFlags::EMPTY) {
let dx = dst_x + sx.saturating_sub(src_rect.left());
let dy = dst_y + sy.saturating_sub(src_rect.top());
let mut blended = tile.clone();
if let Some(dst) = self.get_tile(layer, dx, dy) {
if fg_alpha != 1.0 {
blended.style.fg = blend_fg(tile.style.fg, dst.style.fg, fg_alpha);
}
if bg_alpha != 1.0 {
blended.style.bg = blend_bg(tile.style.bg, dst.style.bg, bg_alpha);
}
}
self.put_tile(layer, dx, dy, blended);
}
}
}
}
pub fn layers(&self) -> impl Iterator<Item = (u8, Pos, &Tile)> + '_ {
let width = usize::from(self.width);
(0..=self.max_layer)
.filter_map(move |id| self.layer(id).map(|lb| (id, lb)))
.flat_map(move |(id, lb)| {
lb.buf.as_ref().iter().enumerate().map(move |(i, tile)| {
#[allow(clippy::cast_possible_truncation)]
let x = (i % width) as u16;
#[allow(clippy::cast_possible_truncation)]
let y = (i / width) as u16;
(id, Pos::new(x, y), tile)
})
})
}
pub fn clear_all(&mut self) {
for layer in self.layers.iter_mut().flatten() {
layer.buf.clear();
}
}
pub(crate) fn flatten_into(&self, dst: &mut Self) {
for y in 0..self.height {
for x in 0..self.width {
let mut out = self.get(x, y).clone();
for id in 1..=self.max_layer {
let Some(tile) = self.get_tile(id, x, y) else {
continue;
};
let contributes_glyph = !tile.flags.contains(TileFlags::EMPTY);
if contributes_glyph {
out.glyph = tile.glyph;
out.style.fg = tile.style.fg;
out.dx = tile.dx;
out.dy = tile.dy;
out.flags = tile.flags;
out.extra.clone_from(&tile.extra);
}
if tile.style.bg != Color::Default {
out.style.bg = tile.style.bg;
}
}
dst.put(x, y, out);
}
}
}
pub fn diff<'a>(&'a self, other: &'a Self) -> impl Iterator<Item = (u8, Pos, &'a Tile)> + 'a {
let width = usize::from(self.width);
let max = self.max_layer;
(0..=max).flat_map(move |id| {
match (self.layer(id), other.layer(id)) {
(None, _) => LayerDiff::Empty,
(Some(cur_lb), None) => LayerDiff::Full(
cur_lb
.buf
.as_ref()
.iter()
.enumerate()
.map(move |(i, tile)| {
#[allow(clippy::cast_possible_truncation)]
let x = (i % width) as u16;
#[allow(clippy::cast_possible_truncation)]
let y = (i / width) as u16;
(id, Pos::new(x, y), tile)
}),
),
(Some(cur_lb), Some(prev_lb)) => LayerDiff::Diff(
cur_lb
.buf
.diff(&prev_lb.buf)
.map(move |(pos, tile)| (id, from_grixy_pos(pos), tile)),
),
}
})
}
}
enum LayerDiff<F, D> {
Empty,
Full(F),
Diff(D),
}
impl<'a, F, D> Iterator for LayerDiff<F, D>
where
F: Iterator<Item = (u8, Pos, &'a Tile)>,
D: Iterator<Item = (u8, Pos, &'a Tile)>,
{
type Item = (u8, Pos, &'a Tile);
fn next(&mut self) -> Option<Self::Item> {
match self {
Self::Empty => None,
Self::Full(iter) => iter.next(),
Self::Diff(iter) => iter.next(),
}
}
}
#[cfg(feature = "gem")]
#[allow(clippy::float_cmp)]
fn blend_color(src: Color, dst: Color, t: f32) -> Color {
use gem::rgb::{HasBlue as _, HasGreen as _, HasRed as _, Lerp as _, Rgb888};
match (src, dst) {
(Color::Default, _) => Color::Default,
(
Color::Rgb {
r: sr,
g: sg,
b: sb,
},
Color::Rgb { r, g, b },
) if t != 1.0 => {
let out = Rgb888::from_rgb(sr, sg, sb).lerp(Rgb888::from_rgb(r, g, b), t);
Color::Rgb {
r: out.red(),
g: out.green(),
b: out.blue(),
}
}
(src, _) => src,
}
}
#[cfg(feature = "gem")]
fn blend_fg(src: Color, dst: Color, t: f32) -> Color {
blend_color(src, dst, t)
}
#[cfg(feature = "gem")]
fn blend_bg(src: Color, dst: Color, t: f32) -> Color {
blend_color(src, dst, t)
}
impl Index<Pos> for Grid {
type Output = Tile;
fn index(&self, pos: Pos) -> &Tile {
&self.layer0().buf[to_grixy_pos(pos)]
}
}
impl IndexMut<Pos> for Grid {
fn index_mut(&mut self, pos: Pos) -> &mut Tile {
let pos = to_grixy_pos(pos);
&mut self.layer0_mut().buf[pos]
}
}
impl fmt::Display for Grid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for y in 0..self.height() {
for x in 0..self.width() {
let tile = self.get(x, y);
#[cfg(feature = "egc")]
let is_spacer = tile.flags.contains(TileFlags::WIDE_CHAR_SPACER);
#[cfg(not(feature = "egc"))]
let is_spacer = tile.glyph == '\0';
let c = if is_spacer {
' ' } else if tile.glyph == ' ' {
'·' } else {
tile.glyph
};
write!(f, "{c}")?;
}
writeln!(f)?;
}
Ok(())
}
}
impl fmt::Debug for Grid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Grid")
.field("width", &self.width)
.field("height", &self.height)
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_grid_new() {
let grid = Grid::new(80, 25);
assert_eq!(grid.width(), 80);
assert_eq!(grid.height(), 25);
}
#[test]
fn test_grid_put_get() {
let mut grid = Grid::new(10, 10);
let tile = Tile::default().with_glyph('X');
grid.put(5, 5, tile);
assert_eq!(grid.get(5, 5).glyph(), 'X');
}
#[test]
fn test_grid_checked_put_get() {
let mut grid = Grid::new(10, 10);
let tile = Tile::default().with_glyph('Y');
assert!(grid.checked_put(5, 5, tile).is_some());
assert_eq!(grid.checked_get(5, 5).unwrap().glyph(), 'Y');
assert!(grid.checked_get(10, 0).is_none());
assert!(grid.checked_put(0, 10, Tile::default()).is_none());
}
#[test]
#[should_panic(expected = "coordinates out of bounds")]
fn test_grid_panic_put() {
let mut grid = Grid::new(10, 10);
grid.put(10, 0, Tile::default());
}
#[test]
fn test_grid_diff() {
let mut g1 = Grid::new(2, 2);
let g2 = Grid::new(2, 2);
g1.put(0, 0, Tile::default().with_glyph('A'));
let diffs: Vec<_> = g1.diff(&g2).collect();
assert_eq!(diffs.len(), 1);
assert_eq!(diffs[0], (0, Pos::new(0, 0), g1.get(0, 0)));
}
#[test]
fn test_grid_resize_expand() {
let mut grid = Grid::new(3, 3);
grid.put(1, 1, Tile::default().with_glyph('X'));
grid.resize(6, 6);
assert_eq!(grid.width(), 6);
assert_eq!(grid.height(), 6);
assert_eq!(grid.get(1, 1).glyph(), 'X'); assert_eq!(grid.get(5, 5).glyph(), ' '); }
#[test]
fn test_grid_resize_shrink() {
let mut grid = Grid::new(10, 10);
grid.put(1, 1, Tile::default().with_glyph('A'));
grid.resize(5, 5);
assert_eq!(grid.width(), 5);
assert_eq!(grid.height(), 5);
assert_eq!(grid.get(1, 1).glyph(), 'A'); }
#[test]
fn test_grid_resize_preserves_overlap() {
let mut grid = Grid::new(4, 4);
grid.put(0, 0, Tile::default().with_glyph('@'));
grid.put(3, 3, Tile::default().with_glyph('X'));
grid.resize(3, 3); assert_eq!(grid.get(0, 0).glyph(), '@');
assert_eq!(grid.get(2, 2).glyph(), ' '); }
#[test]
fn test_grid_display() {
let mut grid = Grid::new(3, 2);
grid.put(0, 0, Tile::default().with_glyph('A'));
let s = alloc::format!("{grid}");
assert_eq!(s, "A··\n···\n");
}
#[test]
fn test_grid_cells_count() {
let grid = Grid::new(4, 3);
assert_eq!(grid.cells(0).unwrap().count(), 12);
}
#[test]
fn test_grid_cells_coordinates() {
let grid = Grid::new(3, 2);
let coords: Vec<(u16, u16)> = grid.cells(0).unwrap().map(|(x, y, _)| (x, y)).collect();
assert_eq!(
coords,
vec![(0, 0), (1, 0), (2, 0), (0, 1), (1, 1), (2, 1),]
);
}
#[test]
fn test_grid_cells_mut() {
use crate::style::Style;
let mut grid = Grid::new(2, 2);
for (x, y, tile) in grid.cells_mut(0) {
#[allow(clippy::cast_possible_truncation)]
let idx = (y * 2 + x) as u8;
*tile = Tile::new(char::from(b'A' + idx), Style::default());
}
assert_eq!(grid.get(0, 0).glyph(), 'A');
assert_eq!(grid.get(1, 0).glyph(), 'B');
assert_eq!(grid.get(0, 1).glyph(), 'C');
assert_eq!(grid.get(1, 1).glyph(), 'D');
}
#[test]
fn test_rect_contains() {
let r = Rect::new(2, 3, 4, 5);
assert!(r.contains_pos(Pos::new(2, 3)));
assert!(r.contains_pos(Pos::new(5, 7)));
assert!(!r.contains_pos(Pos::new(6, 3))); assert!(!r.contains_pos(Pos::new(2, 8))); assert!(!r.contains_pos(Pos::new(1, 3)));
}
#[test]
fn test_rect_area() {
assert_eq!(Rect::new(0, 0, 5, 3).area(), 15);
assert_eq!(Rect::default().area(), 0);
}
#[test]
fn test_rect_top_left_bottom_right() {
let r = Rect::new(1, 2, 3, 4);
assert_eq!(r.top_left(), Pos::new(1, 2));
assert_eq!(r.bottom_right(), Pos::new(4, 6));
}
#[test]
fn test_rect_intersects() {
let a = Rect::new(0, 0, 4, 4);
let b = Rect::new(2, 2, 4, 4);
let c = Rect::new(4, 0, 4, 4); assert!(!a.intersect(b).is_empty());
assert!(a.intersect(c).is_empty());
}
#[test]
fn test_rect_positions() {
let r = Rect::new(1, 2, 2, 2);
let pts: Vec<Pos> = r.pos_iter().collect();
assert_eq!(
pts,
vec![
Pos::new(1, 2),
Pos::new(2, 2),
Pos::new(1, 3),
Pos::new(2, 3),
]
);
}
#[test]
fn test_index_position() {
let mut grid = Grid::new(5, 5);
let pos = Pos::new(2, 3);
grid[pos] = Tile::default().with_glyph('Z');
assert_eq!(grid[pos].glyph(), 'Z');
}
#[test]
fn test_position_from_tuple() {
let p: Pos = (3u16, 7u16).into();
assert_eq!(p, Pos::new(3, 7));
let t: (u16, u16) = p.into();
assert_eq!(t, (3, 7));
}
#[test]
fn test_size_from_tuple() {
let s: Size = (80u16, 25u16).into();
assert_eq!(
s,
Size {
width: 80,
height: 25
}
);
let t: (u16, u16) = s.into();
assert_eq!(t, (80, 25));
}
#[test]
fn test_position_ord_row_major() {
let mut positions = vec![Pos::new(5, 0), Pos::new(0, 1), Pos::new(3, 0)];
positions.sort();
assert_eq!(
positions,
vec![Pos::new(3, 0), Pos::new(5, 0), Pos::new(0, 1),]
);
}
#[test]
fn test_size_ord() {
assert!(
Size {
width: 1,
height: 2
} < Size {
width: 2,
height: 1
}
);
}
#[test]
fn test_grid_layer_zero_always_allocated() {
let g = Grid::new(5, 5);
assert!(g.layer(0).is_some());
for id in 1u8..=5 {
assert!(g.layer(id).is_none(), "layer {id} should be None");
}
}
#[test]
fn test_grid_put_tile_allocates_layer() {
let mut g = Grid::new(5, 5);
g.put_tile(3, 0, 0, Tile::new('@', Style::default()));
assert!(g.layer(3).is_some());
assert!(g.layer(4).is_none());
}
#[test]
fn test_grid_diff_empty_when_identical() {
let g = Grid::new(5, 5);
let prev = Grid::new(5, 5);
assert_eq!(g.diff(&prev).count(), 0);
}
#[test]
fn test_grid_diff_reports_changed_cell() {
let mut cur = Grid::new(5, 5);
let prev = Grid::new(5, 5);
cur.put_tile(0, 2, 3, Tile::new('X', Style::default()));
let diffs: Vec<_> = cur.diff(&prev).collect();
assert_eq!(diffs.len(), 1);
assert_eq!(diffs[0].0, 0);
assert_eq!(diffs[0].1, Pos::new(2, 3));
assert_eq!(diffs[0].2.glyph, 'X');
}
#[test]
fn test_grid_diff_new_layer_yields_all_cells() {
let mut cur = Grid::new(3, 4);
let prev = Grid::new(3, 4);
cur.put_tile(1, 0, 0, Tile::new('A', Style::default()));
let diffs: Vec<_> = cur.diff(&prev).collect();
assert_eq!(diffs.len(), 12);
assert!(diffs.iter().all(|(l, _, _)| *l == 1));
}
#[test]
fn test_grid_diff_layer_major_order() {
let mut cur = Grid::new(3, 3);
let prev = Grid::new(3, 3);
cur.put_tile(2, 0, 0, Tile::new('B', Style::default()));
cur.put_tile(0, 1, 0, Tile::new('A', Style::default()));
let layers: Vec<u8> = cur.diff(&prev).map(|(l, _, _)| l).collect();
assert_eq!(layers[0], 0);
assert!(layers[1..].iter().all(|&l| l == 2));
}
#[test]
fn test_grid_put_and_get_on_layer_2() {
use crate::style::Style;
let mut g = Grid::new(5, 5);
g.put_tile(2, 1, 1, Tile::new('Z', Style::default()));
assert_eq!(g.get_tile(2, 1, 1).unwrap().glyph, 'Z');
assert_eq!(g.get(1, 1).glyph, ' ');
assert!(g.get_tile(3, 0, 0).is_none());
}
#[test]
fn test_grid_clear_layer() {
let mut g = Grid::new(5, 5);
g.put_tile(1, 0, 0, Tile::new('Z', Style::default()));
g.put_tile(0, 0, 0, Tile::new('A', Style::default()));
g.clear(1);
assert_eq!(g.get_tile(0, 0, 0).unwrap().glyph, 'A');
assert!(g.get_tile(1, 0, 0).is_some());
assert_eq!(g.get_tile(1, 0, 0).unwrap().glyph, ' '); }
#[test]
fn test_grid_clear_all() {
let mut g = Grid::new(5, 5);
g.put_tile(1, 0, 0, Tile::new('Z', Style::default()));
g.put_tile(0, 0, 0, Tile::new('A', Style::default()));
g.clear_all();
assert_eq!(g.get(0, 0).glyph, ' ');
assert_eq!(g.get_tile(1, 0, 0).unwrap().glyph, ' ');
}
}
#[cfg(all(test, feature = "egc"))]
mod egc_proptests {
use super::*;
use crate::style::Style;
use proptest::prelude::*;
const W: u16 = 8;
const H: u16 = 4;
const GRAPHEMES: &[&str] = &["a", "\u{4e2d}", "e\u{0301}", "\u{1f600}"];
fn assert_wide_invariants(grid: &Grid) {
for y in 0..grid.height() {
for x in 0..grid.width() {
let flags = grid.get(x, y).flags();
let lead = flags.contains(TileFlags::WIDE_CHAR);
let spacer = flags.contains(TileFlags::WIDE_CHAR_SPACER);
assert!(
!(lead && spacer),
"cell ({x}, {y}) is both wide lead and spacer"
);
if lead {
assert!(x + 1 < grid.width(), "wide lead at ({x}, {y}) has no room");
assert!(
grid.get(x + 1, y)
.flags()
.contains(TileFlags::WIDE_CHAR_SPACER),
"wide lead at ({x}, {y}) is missing its spacer"
);
}
if spacer {
assert!(x > 0, "orphan spacer at ({x}, {y}) (no cell to the left)");
assert!(
grid.get(x - 1, y).flags().contains(TileFlags::WIDE_CHAR),
"orphan spacer at ({x}, {y}) (left cell is not a wide lead)"
);
}
}
}
}
proptest! {
#[test]
fn wide_char_bookkeeping_never_desyncs(
ops in prop::collection::vec(
(0u16..W, 0u16..H, 0usize..GRAPHEMES.len()),
0..64,
),
) {
let mut grid = Grid::new(W, H);
for (x, y, gi) in ops {
grid.write_grapheme(0, x, y, GRAPHEMES[gi], Style::default());
assert_wide_invariants(&grid);
}
}
}
}