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::collections::BTreeMap;
use alloc::sync::Arc;
use alloc::vec::Vec;
#[cfg(feature = "gem")]
use alpha_blend::blend_modes::SeparableBlendMode;
use core::fmt;
use core::ops::{Index, IndexMut};
use grixy::buf::GridBuf;
use grixy::ops::layout::RowMajor;
use grixy::ops::{ExactSizeGrid, GridRead, GridWrite};
#[cfg(feature = "gem")]
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum BlendMode {
#[default]
Linear,
Screen,
Dodge,
Burn,
Overlay,
}
#[cfg(feature = "gem")]
impl BlendMode {
const fn separable(self) -> Option<SeparableBlendMode> {
match self {
Self::Linear => None,
Self::Screen => Some(SeparableBlendMode::Screen),
Self::Dodge => Some(SeparableBlendMode::ColorDodge),
Self::Burn => Some(SeparableBlendMode::ColorBurn),
Self::Overlay => Some(SeparableBlendMode::Overlay),
}
}
}
#[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))
}
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)
})
}
}
#[derive(Clone)]
pub(crate) struct LayerBuf {
pub(crate) buf: GridBuf<Tile, Vec<Tile>, RowMajor>,
extras: BTreeMap<usize, Arc<str>>,
}
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)),
extras: BTreeMap::new(),
}
}
fn extra_for(&self, idx: usize, tile: &Tile) -> Option<&str> {
if tile.flags.contains(TileFlags::HAS_EXTRA) {
self.extras.get(&idx).map(|s| &**s)
} else {
None
}
}
fn extra_arc_for(&self, idx: usize, tile: &Tile) -> Option<Arc<str>> {
if tile.flags.contains(TileFlags::HAS_EXTRA) {
self.extras.get(&idx).cloned()
} else {
None
}
}
}
#[derive(Clone)]
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 idx = usize::from(y) * usize::from(self.width) + usize::from(x);
let lb = self.layer0_mut();
assert!(
lb.buf.contains(pos),
"coordinates out of bounds: ({x}, {y})"
);
lb.extras.remove(&idx);
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))]
}
#[must_use]
pub fn grapheme(&self, layer: u8, x: u16, y: u16) -> Option<&str> {
let lb = self.layer(layer)?;
let pos = to_grixy_pos(Pos::new(x, y));
let tile = lb.buf.get(pos)?;
let idx = usize::from(y) * usize::from(self.width) + usize::from(x);
lb.extra_for(idx, tile)
}
pub fn checked_put(&mut self, x: u16, y: u16, tile: Tile) -> Option<()> {
let pos = to_grixy_pos(Pos::new(x, y));
let idx = usize::from(y) * usize::from(self.width) + usize::from(x);
let lb = self.layer0_mut();
if lb.buf.contains(pos) {
lb.extras.remove(&idx);
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();
lb.extras.clear();
}
}
pub fn resize(&mut self, width: u16, height: u16) {
let old_width = usize::from(self.width);
let new_width = usize::from(width);
let new_height = usize::from(height);
self.width = width;
self.height = height;
for layer in self.layers.iter_mut().flatten() {
if !layer.extras.is_empty() {
layer.extras = layer
.extras
.iter()
.filter_map(|(&old_idx, s)| {
let x = old_idx % old_width;
let y = old_idx / old_width;
(x < new_width && y < new_height).then(|| (y * new_width + x, s.clone()))
})
.collect();
}
layer.buf.resize(new_width, new_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 flags = if width == 2 {
TileFlags::WIDE_CHAR
} else {
TileFlags::empty()
};
let flags = if has_extra {
flags | TileFlags::HAS_EXTRA
} else {
flags
};
lb.buf.as_mut()[idx].glyph = first;
lb.buf.as_mut()[idx].style = style;
lb.buf.as_mut()[idx].flags = flags;
if has_extra {
lb.extras.insert(idx, Arc::from(cap_grapheme(grapheme)));
} else {
lb.extras.remove(&idx);
}
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.flags = TileFlags::WIDE_CHAR_SPACER;
lb.extras.remove(&spacer_idx);
}
}
}
#[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();
lb.extras.remove(&pidx);
}
}
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();
lb.extras.remove(&sidx);
}
}
}
}
}
impl Grid {
pub fn put_tile(&mut self, layer: u8, x: u16, y: u16, mut tile: Tile) -> Option<()> {
let pos = to_grixy_pos(Pos::new(x, y));
let idx = usize::from(y) * usize::from(self.width) + usize::from(x);
let lb = self.layer_or_alloc(layer);
if !lb.buf.contains(pos) {
return None;
}
lb.extras.remove(&idx);
tile.flags.remove(TileFlags::HAS_EXTRA);
lb.buf[pos] = tile;
Some(())
}
pub(crate) fn set_extra(&mut self, layer: u8, x: u16, y: u16, extra: Arc<str>) {
let pos = to_grixy_pos(Pos::new(x, y));
let idx = usize::from(y) * usize::from(self.width) + usize::from(x);
let lb = self.layer_or_alloc(layer);
if lb.buf.contains(pos) {
lb.buf[pos].flags.insert(TileFlags::HAS_EXTRA);
lb.extras.insert(idx, extra);
}
}
#[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());
let src_idx = usize::from(sy) * usize::from(src.width) + usize::from(sx);
let extra = src
.layer(layer)
.and_then(|lb| lb.extra_arc_for(src_idx, tile));
self.put_tile(layer, dx, dy, *tile);
if let Some(extra) = extra {
self.set_extra(layer, dx, dy, extra);
}
}
}
}
}
#[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,
mode: BlendMode,
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;
if let Some(dst) = self.get_tile(layer, dx, dy) {
if mode != BlendMode::Linear || fg_alpha != 1.0 {
blended.style.fg =
blend_fg(mode, tile.style.fg, dst.style.fg, fg_alpha);
}
if mode != BlendMode::Linear || bg_alpha != 1.0 {
blended.style.bg =
blend_bg(mode, tile.style.bg, dst.style.bg, bg_alpha);
}
}
let src_idx = usize::from(sy) * usize::from(src.width) + usize::from(sx);
let extra = src
.layer(layer)
.and_then(|lb| lb.extra_arc_for(src_idx, tile));
self.put_tile(layer, dx, dy, blended);
if let Some(extra) = extra {
self.set_extra(layer, dx, dy, extra);
}
}
}
}
}
pub fn layers(&self) -> impl Iterator<Item = (u8, Pos, &Tile, Option<&str>)> + '_ {
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, lb.extra_for(i, tile))
})
})
}
pub fn clear_all(&mut self) {
for layer in self.layers.iter_mut().flatten() {
layer.buf.clear();
layer.extras.clear();
}
}
pub(crate) fn flatten_into(&self, dst: &mut Self) {
let width = usize::from(self.width);
for y in 0..self.height {
for x in 0..self.width {
let mut out = *self.get(x, y);
let idx = usize::from(y) * width + usize::from(x);
let mut out_extra = self.layer0().extra_arc_for(idx, &out);
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 = self.layer(id).and_then(|lb| lb.extra_arc_for(idx, tile));
}
if tile.style.bg != Color::Default {
out.style.bg = tile.style.bg;
}
}
dst.put(x, y, out);
if let Some(extra) = out_extra {
dst.set_extra(0, x, y, extra);
}
}
}
}
pub fn diff<'a>(
&'a self,
other: &'a Self,
) -> impl Iterator<Item = (u8, Pos, &'a Tile, Option<&'a str>)> + '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, cur_lb.extra_for(i, tile))
}),
),
(Some(cur_lb), Some(prev_lb)) => {
LayerDiff::Diff(cur_lb.buf.as_ref().iter().enumerate().filter_map(
move |(i, tile)| {
let prev_tile = &prev_lb.buf.as_ref()[i];
let cur_extra = cur_lb.extra_for(i, tile);
let prev_extra = prev_lb.extra_for(i, prev_tile);
if tile == prev_tile && cur_extra == prev_extra {
return None;
}
#[allow(clippy::cast_possible_truncation)]
let x = (i % width) as u16;
#[allow(clippy::cast_possible_truncation)]
let y = (i / width) as u16;
Some((id, Pos::new(x, y), tile, cur_extra))
},
))
}
}
})
}
}
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, Option<&'a str>)>,
D: Iterator<Item = (u8, Pos, &'a Tile, Option<&'a str>)>,
{
type Item = (u8, Pos, &'a Tile, Option<&'a str>);
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(mode: BlendMode, 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: dr,
g: dg,
b: db,
},
) if mode != BlendMode::Linear || t != 1.0 => {
let (r, g, b) = mode.separable().map_or_else(
|| {
let out = Rgb888::from_rgb(dr, dg, db).lerp(Rgb888::from_rgb(sr, sg, sb), t);
(out.red(), out.green(), out.blue())
},
|sep| {
(
blend_separable_channel(sep, sr, dr, t),
blend_separable_channel(sep, sg, dg, t),
blend_separable_channel(sep, sb, db, t),
)
},
);
Color::Rgb { r, g, b }
}
(src, _) => src,
}
}
#[cfg(feature = "gem")]
fn blend_separable_channel(sep: SeparableBlendMode, src: u8, dst: u8, t: f32) -> u8 {
let cs = f32::from(src) / 255.0;
let cb = f32::from(dst) / 255.0;
let mixed = sep.mix(cb, cs);
let blended = libm::fmaf(mixed - cb, t, cb);
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let out = libm::roundf(blended.clamp(0.0, 1.0) * 255.0) as u8;
out
}
#[cfg(feature = "gem")]
fn blend_fg(mode: BlendMode, src: Color, dst: Color, t: f32) -> Color {
blend_color(mode, src, dst, t)
}
#[cfg(feature = "gem")]
fn blend_bg(mode: BlendMode, src: Color, dst: Color, t: f32) -> Color {
blend_color(mode, 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), None));
}
#[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, ' ');
}
#[test]
fn test_grid_clone_is_independent() {
let mut g = Grid::new(3, 3);
g.put_tile(0, 0, 0, Tile::new('A', Style::default()));
g.put_tile(2, 1, 1, Tile::new('B', Style::default()));
let mut cloned = g.clone();
assert_eq!(cloned.get(0, 0).glyph, 'A');
assert_eq!(cloned.get_tile(2, 1, 1).unwrap().glyph, 'B');
assert_eq!(cloned.max_layer(), g.max_layer());
cloned.put_tile(0, 0, 0, Tile::new('Z', Style::default()));
assert_eq!(cloned.get(0, 0).glyph, 'Z');
assert_eq!(g.get(0, 0).glyph, 'A');
}
#[cfg(feature = "egc")]
#[test]
fn test_grid_write_grapheme_stores_and_reads_extra() {
let mut g = Grid::new(5, 5);
g.write_grapheme(0, 1, 1, "e\u{0301}", Style::default());
assert_eq!(g.get(1, 1).glyph, 'e');
assert_eq!(g.grapheme(0, 1, 1), Some("e\u{0301}"));
g.write_grapheme(0, 2, 2, "a", Style::default());
assert_eq!(g.grapheme(0, 2, 2), None);
}
#[cfg(feature = "egc")]
#[test]
fn test_grid_overwrite_clears_extra() {
let mut g = Grid::new(5, 5);
g.write_grapheme(0, 0, 0, "e\u{0301}", Style::default());
assert_eq!(g.grapheme(0, 0, 0), Some("e\u{0301}"));
g.put(0, 0, Tile::new('X', Style::default()));
assert_eq!(g.grapheme(0, 0, 0), None);
assert!(!g.get(0, 0).flags().contains(TileFlags::HAS_EXTRA));
}
#[cfg(feature = "egc")]
#[test]
fn test_grid_resize_remaps_extras_to_new_stride() {
let mut g = Grid::new(4, 4);
g.write_grapheme(0, 3, 1, "e\u{0301}", Style::default());
assert_eq!(g.grapheme(0, 3, 1), Some("e\u{0301}"));
g.resize(8, 4);
assert_eq!(g.get(3, 1).glyph, 'e');
assert_eq!(g.grapheme(0, 3, 1), Some("e\u{0301}"));
assert_eq!(g.grapheme(0, 7, 0), None);
g.resize(2, 4);
assert_eq!(g.grapheme(0, 3, 1), None);
}
#[cfg(feature = "egc")]
#[test]
fn test_grid_diff_detects_grapheme_only_change() {
let mut cur = Grid::new(2, 2);
let mut prev = Grid::new(2, 2);
cur.write_grapheme(0, 0, 0, "e\u{0301}", Style::default());
prev.write_grapheme(0, 0, 0, "e\u{0300}", Style::default());
let diffs: Vec<_> = cur.diff(&prev).collect();
assert_eq!(diffs.len(), 1);
assert_eq!(diffs[0].1, Pos::new(0, 0));
assert_eq!(diffs[0].3, Some("e\u{0301}"));
let mut prev2 = Grid::new(2, 2);
prev2.write_grapheme(0, 0, 0, "e\u{0301}", Style::default());
assert_eq!(cur.diff(&prev2).count(), 0);
}
#[cfg(feature = "egc")]
#[test]
fn test_grid_blit_preserves_extra() {
let mut src = Grid::new(2, 2);
src.write_grapheme(0, 0, 0, "e\u{0301}", Style::default());
let mut dst = Grid::new(2, 2);
dst.blit(0, &src, Rect::new(0, 0, 2, 2), 0, 0);
assert_eq!(dst.get(0, 0).glyph, 'e');
assert_eq!(dst.grapheme(0, 0, 0), Some("e\u{0301}"));
}
#[cfg(feature = "gem")]
#[test]
fn test_blend_separable_channel_screen() {
assert_eq!(
blend_separable_channel(SeparableBlendMode::Screen, 204, 102, 1.0),
224
);
assert_eq!(
blend_separable_channel(SeparableBlendMode::Screen, 204, 102, 0.5),
163
);
}
#[cfg(feature = "gem")]
#[test]
fn test_blend_separable_channel_dodge() {
assert_eq!(
blend_separable_channel(SeparableBlendMode::ColorDodge, 204, 51, 1.0),
255
);
assert_eq!(
blend_separable_channel(SeparableBlendMode::ColorDodge, 204, 51, 0.5),
153
);
}
#[cfg(feature = "gem")]
#[test]
fn test_blend_separable_channel_burn() {
assert_eq!(
blend_separable_channel(SeparableBlendMode::ColorBurn, 51, 204, 1.0),
0
);
assert_eq!(
blend_separable_channel(SeparableBlendMode::ColorBurn, 51, 204, 0.5),
102
);
}
#[cfg(feature = "gem")]
#[test]
fn test_blend_separable_channel_overlay() {
assert_eq!(
blend_separable_channel(SeparableBlendMode::Overlay, 204, 51, 1.0),
82
);
assert_eq!(
blend_separable_channel(SeparableBlendMode::Overlay, 204, 51, 0.5),
66
);
assert_eq!(
blend_separable_channel(SeparableBlendMode::Overlay, 51, 204, 1.0),
173
);
}
#[cfg(feature = "gem")]
#[test]
fn test_grid_blit_alpha_screen_blends_fg() {
let mut src = Grid::new(1, 1);
src.put(
0,
0,
Tile::default()
.with_glyph('X')
.with_style(Style::new().fg(Color::Rgb {
r: 204,
g: 204,
b: 204,
})),
);
let mut dst = Grid::new(1, 1);
dst.put(
0,
0,
Tile::default()
.with_glyph('_')
.with_style(Style::new().fg(Color::Rgb {
r: 102,
g: 102,
b: 102,
})),
);
dst.blit_alpha(
0,
&src,
Rect::new(0, 0, 1, 1),
0,
0,
BlendMode::Screen,
1.0,
1.0,
);
assert_eq!(
dst.get(0, 0).style.fg,
Color::Rgb {
r: 224,
g: 224,
b: 224
}
);
}
#[cfg(feature = "gem")]
#[test]
fn test_grid_blit_alpha_linear_direction() {
let mut src = Grid::new(1, 1);
src.put(
0,
0,
Tile::default()
.with_glyph('X')
.with_style(Style::new().fg(Color::Rgb {
r: 255,
g: 255,
b: 255,
})),
);
let dst_color = Color::Rgb { r: 0, g: 0, b: 0 };
let at = |t: f32| {
let mut dst = Grid::new(1, 1);
dst.put(
0,
0,
Tile::default()
.with_glyph('_')
.with_style(Style::new().fg(dst_color)),
);
dst.blit_alpha(
0,
&src,
Rect::new(0, 0, 1, 1),
0,
0,
BlendMode::Linear,
t,
1.0,
);
dst.get(0, 0).style.fg
};
assert_eq!(at(0.0), dst_color);
assert_eq!(
at(1.0),
Color::Rgb {
r: 255,
g: 255,
b: 255
}
);
let Color::Rgb { r, g, b } = at(0.5) else {
panic!("expected Color::Rgb");
};
assert!(r > 0 && r < 255, "expected a mid-gray, got {r}");
assert_eq!(r, g);
assert_eq!(g, b);
}
#[cfg(feature = "gem")]
#[test]
fn test_blend_color_non_rgb_passthrough_all_modes() {
for mode in [
BlendMode::Linear,
BlendMode::Screen,
BlendMode::Dodge,
BlendMode::Burn,
BlendMode::Overlay,
] {
assert_eq!(
blend_color(mode, Color::Default, Color::Rgb { r: 1, g: 2, b: 3 }, 0.5),
Color::Default
);
assert_eq!(
blend_color(mode, Color::BLACK, Color::WHITE, 0.5),
Color::BLACK
);
}
}
#[cfg(feature = "egc")]
#[test]
fn test_grid_clone_preserves_extra() {
let mut g = Grid::new(2, 2);
g.write_grapheme(0, 0, 0, "e\u{0301}", Style::default());
let cloned = g.clone();
assert_eq!(cloned.grapheme(0, 0, 0), Some("e\u{0301}"));
}
#[cfg(feature = "egc")]
#[test]
fn test_grid_flatten_into_carries_extra_from_higher_layer() {
let mut g = Grid::new(2, 2);
g.write_grapheme(1, 0, 0, "e\u{0301}", Style::default());
let mut flattened = Grid::new(2, 2);
g.flatten_into(&mut flattened);
assert_eq!(flattened.get(0, 0).glyph, 'e');
assert_eq!(flattened.grapheme(0, 0, 0), Some("e\u{0301}"));
}
}
#[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);
}
}
}
}