use crate::backend::DrawCell;
use crate::color::Color;
use crate::style::Style;
use crate::tile::Tile;
use crate::tile::TileFlags;
#[cfg(feature = "egc")]
use crate::tile::cap_grapheme;
use crate::tint::Tint;
use alloc::collections::BTreeMap;
use alloc::sync::Arc;
use alloc::vec::Vec;
#[cfg(feature = "color-space")]
use alpha_blend::BlendMode as 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 = "color-space")]
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum BlendMode {
#[default]
Linear,
Screen,
Dodge,
Burn,
Overlay,
Multiply,
}
#[cfg(feature = "color-space")]
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),
Self::Multiply => Some(SeparableBlendMode::Multiply),
}
}
}
#[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>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct Offset {
pub dx: i16,
pub dy: i16,
}
impl Offset {
#[must_use]
pub const fn new(dx: i16, dy: i16) -> Self {
Self { dx, dy }
}
}
impl From<(i16, i16)> for Offset {
fn from((dx, dy): (i16, i16)) -> Self {
Self { dx, dy }
}
}
impl From<Offset> for (i16, i16) {
fn from(offset: Offset) -> Self {
(offset.dx, offset.dy)
}
}
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, TileExtra>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) struct TileExtra {
pub(crate) grapheme: Option<Arc<str>>,
pub(crate) tint: Tint,
}
impl TileExtra {
fn is_empty(&self) -> bool {
self.grapheme.is_none() && self.tint == Tint::None
}
}
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 entry_for(&self, idx: usize, tile: &Tile) -> Option<&TileExtra> {
if tile.flags.contains(TileFlags::HAS_EXTRA) {
self.extras.get(&idx)
} else {
None
}
}
fn extra_for(&self, idx: usize, tile: &Tile) -> Option<&str> {
self.entry_for(idx, tile)?.grapheme.as_deref()
}
fn tint_for(&self, idx: usize, tile: &Tile) -> Tint {
self.entry_for(idx, tile).map_or(Tint::None, |e| e.tint)
}
fn extra_entry_for(&self, idx: usize, tile: &Tile) -> Option<TileExtra> {
self.entry_for(idx, tile).cloned()
}
}
#[derive(Clone)]
pub struct Grid {
width: u16,
height: u16,
layers: Vec<Option<LayerBuf>>,
max_layer: u8,
has_spans: bool,
}
impl Grid {
fn layer(&self, id: u8) -> Option<&LayerBuf> {
self.layers.get(usize::from(id))?.as_ref()
}
fn layer_or_alloc(&mut self, id: u8) -> &mut LayerBuf {
let idx = usize::from(id);
if idx >= self.layers.len() {
self.layers.resize_with(idx + 1, || None);
}
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 {
Self {
width,
height,
layers: alloc::vec![Some(LayerBuf::new(width, height))],
max_layer: 0,
has_spans: false,
}
}
#[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, Pos::new(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
}
#[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)
}
#[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) -> Option<CellsMut<'_>> {
let width = usize::from(self.width);
let lb = self.layers.get_mut(usize::from(layer))?.as_mut()?;
Some(CellsMut {
iter: lb.buf.as_mut().iter_mut().enumerate(),
width,
})
}
pub fn cells_mut_or_alloc(&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
.get_mut(usize::from(layer))
.and_then(Option::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_span_overlap(layer, x, y, width);
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;
#[allow(clippy::cast_possible_truncation)]
{
lb.buf.as_mut()[idx].width = width as u8;
}
if has_extra {
lb.extras.insert(
idx,
TileExtra {
grapheme: Some(Arc::from(cap_grapheme(grapheme))),
tint: Tint::None,
},
);
} 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.width = 0;
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 write_span<S: AsRef<str>>(
&mut self,
layer: u8,
x: u16,
y: u16,
rows: &[S],
style: Style,
) -> Option<()> {
let cols = rows.first()?.as_ref().chars().count();
if cols == 0 || rows.iter().any(|r| r.as_ref().chars().count() != cols) {
return None;
}
let footprint = (u8::try_from(cols).ok()?, u8::try_from(rows.len()).ok()?);
self.write_span_cells(
layer,
Pos::new(x, y),
footprint,
style,
rows.iter().map(|row| row.as_ref().chars()),
)
}
pub fn write_span_uniform(
&mut self,
layer: u8,
pos: impl Into<Pos>,
size: impl Into<Size>,
anchor: char,
fill: char,
style: Style,
) -> Option<()> {
let size = size.into();
let footprint = (
u8::try_from(size.width).ok()?,
u8::try_from(size.height).ok()?,
);
if footprint.0 == 0 || footprint.1 == 0 {
return None;
}
let rows = (0..footprint.1).map(move |row| {
(0..footprint.0).map(move |col| if (row, col) == (0, 0) { anchor } else { fill })
});
self.write_span_cells(layer, pos.into(), footprint, style, rows)
}
fn write_span_cells<R: Iterator<Item = char>>(
&mut self,
layer: u8,
pos: Pos,
footprint: (u8, u8),
style: Style,
rows: impl Iterator<Item = R>,
) -> Option<()> {
let (footprint_w, footprint_h) = footprint;
let (x, y) = (pos.x, pos.y);
let grid_w = usize::from(self.width);
if usize::from(x) + usize::from(footprint_w) > grid_w
|| usize::from(y) + usize::from(footprint_h) > usize::from(self.height)
{
return None;
}
for row in 0..footprint_h {
let cy = y + u16::from(row);
self.clear_span_overlap(layer, x, cy, u16::from(footprint_w));
#[cfg(feature = "egc")]
self.clear_overlap(layer, x, cy, u16::from(footprint_w));
}
self.has_spans = true;
let lb = self.layer_or_alloc(layer);
for (row, line) in rows.enumerate() {
for (col, ch) in line.enumerate() {
let idx = (usize::from(y) + row) * grid_w + usize::from(x) + col;
let mut tile = Tile::new(ch, style);
if row == 0 && col == 0 {
tile.flags = TileFlags::SPAN_ANCHOR;
tile.span_w = footprint_w;
tile.span_h = footprint_h;
} else {
#[allow(clippy::cast_possible_truncation)]
{
tile.flags = TileFlags::SPAN_COVERED;
tile.span_w = col as u8;
tile.span_h = row as u8;
}
}
lb.buf.as_mut()[idx] = tile;
lb.extras.remove(&idx);
}
}
Some(())
}
#[must_use]
pub fn span_owner(&self, layer: u8, x: u16, y: u16) -> Option<Pos> {
self.span_anchor_at(layer, x, y)
}
pub fn clear_span(&mut self, layer: u8, x: u16, y: u16) {
if let Some(anchor) = self.span_anchor_at(layer, x, y) {
self.reset_span_at(layer, anchor);
}
}
fn span_anchor_at(&self, layer: u8, x: u16, y: u16) -> Option<Pos> {
let tile = self.layer(layer)?.buf.get(to_grixy_pos(Pos::new(x, y)))?;
if tile.flags.contains(TileFlags::SPAN_ANCHOR) {
return Some(Pos::new(x, y));
}
let (dx, dy) = tile.span_offset()?;
Some(Pos::new(x.checked_sub(dx)?, y.checked_sub(dy)?))
}
fn reset_span_at(&mut self, layer: u8, anchor: Pos) {
let w = usize::from(self.width);
let h = usize::from(self.height);
let Some(lb) = self
.layers
.get_mut(usize::from(layer))
.and_then(Option::as_mut)
else {
return;
};
let anchor_idx = usize::from(anchor.y) * w + usize::from(anchor.x);
let Some(anchor_tile) = lb.buf.as_ref().get(anchor_idx).copied() else {
return;
};
if !anchor_tile.flags.contains(TileFlags::SPAN_ANCHOR) {
return;
}
for row in 0..usize::from(anchor_tile.span_h) {
let cy = usize::from(anchor.y) + row;
if cy >= h {
break;
}
for col in 0..usize::from(anchor_tile.span_w) {
let cx = usize::from(anchor.x) + col;
if cx >= w {
break;
}
let idx = cy * w + cx;
lb.buf.as_mut()[idx].reset();
lb.extras.remove(&idx);
}
}
}
fn clear_span_overlap(&mut self, layer: u8, x: u16, y: u16, width: u16) {
if !self.has_spans {
return;
}
let mut anchors: Vec<Pos> = Vec::new();
let Some(lb) = self.layer(layer) else {
return;
};
for cx in x..x.saturating_add(width) {
let Some(tile) = lb.buf.get(to_grixy_pos(Pos::new(cx, y))) else {
continue;
};
let anchor = if tile.flags.contains(TileFlags::SPAN_ANCHOR) {
Pos::new(cx, y)
} else if let Some((dx, dy)) = tile.span_offset() {
match (cx.checked_sub(dx), y.checked_sub(dy)) {
(Some(ax), Some(ay)) => Pos::new(ax, ay),
_ => continue,
}
} else {
continue;
};
if !anchors.contains(&anchor) {
anchors.push(anchor);
}
}
for anchor in anchors {
self.reset_span_at(layer, anchor);
}
}
}
impl Grid {
pub fn put_tile(&mut self, layer: u8, pos: impl Into<Pos>, mut tile: Tile) -> Option<()> {
let pos = pos.into();
self.clear_span_overlap(layer, pos.x, pos.y, 1);
let gpos = to_grixy_pos(pos);
let idx = usize::from(pos.y) * usize::from(self.width) + usize::from(pos.x);
let lb = self.layer_or_alloc(layer);
if !lb.buf.contains(gpos) {
return None;
}
lb.extras.remove(&idx);
tile.flags.remove(TileFlags::HAS_EXTRA);
lb.buf[gpos] = tile;
Some(())
}
pub(crate) fn set_extra(&mut self, layer: u8, x: u16, y: u16, extra: TileExtra) {
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) {
if extra.is_empty() {
lb.buf[pos].flags.remove(TileFlags::HAS_EXTRA);
lb.extras.remove(&idx);
} else {
lb.buf[pos].flags.insert(TileFlags::HAS_EXTRA);
lb.extras.insert(idx, extra);
}
}
}
#[must_use]
pub fn tint(&self, layer: u8, x: u16, y: u16) -> Tint {
let Some(lb) = self.layer(layer) else {
return Tint::None;
};
let Some(tile) = lb.buf.get(to_grixy_pos(Pos::new(x, y))) else {
return Tint::None;
};
let idx = usize::from(y) * usize::from(self.width) + usize::from(x);
lb.tint_for(idx, tile)
}
pub fn set_tint(&mut self, layer: u8, x: u16, y: u16, tint: Tint) {
let idx = usize::from(y) * usize::from(self.width) + usize::from(x);
let pos = to_grixy_pos(Pos::new(x, y));
let lb = self.layer_or_alloc(layer);
if !lb.buf.contains(pos) {
return;
}
let grapheme = if lb.buf[pos].flags.contains(TileFlags::HAS_EXTRA) {
lb.extras.get(&idx).and_then(|e| e.grapheme.clone())
} else {
None
};
let entry = TileExtra { grapheme, tint };
if entry.is_empty() {
lb.buf[pos].flags.remove(TileFlags::HAS_EXTRA);
lb.extras.remove(&idx);
} else {
lb.buf[pos].flags.insert(TileFlags::HAS_EXTRA);
lb.extras.insert(idx, entry);
}
}
#[must_use]
pub fn tile(&self, layer: u8, pos: impl Into<Pos>) -> Option<&Tile> {
let pos = to_grixy_pos(pos.into());
self.layer(layer)?.buf.get(pos)
}
pub fn tile_mut(&mut self, layer: u8, pos: impl Into<Pos>) -> Option<&mut Tile> {
let pos = to_grixy_pos(pos.into());
self.layers
.get_mut(usize::from(layer))?
.as_mut()?
.buf
.get_mut(pos)
}
pub fn blit(&mut self, layer: u8, src: &Self, src_rect: Rect, dst_x: u16, dst_y: u16) {
let Some(src_lb) = src.layer(layer) else {
return;
};
let src_width = usize::from(src.width);
let sx0 = src_rect.left().min(src.width);
let sx1 = src_rect.right().min(src.width);
let sy0 = src_rect.top().min(src.height);
let sy1 = src_rect.bottom().min(src.height);
if sx0 >= sx1 || sy0 >= sy1 {
return;
}
let has_visible = (sy0..sy1).any(|sy| {
let start = usize::from(sy) * src_width + usize::from(sx0);
let end = usize::from(sy) * src_width + usize::from(sx1);
src_lb.buf.as_ref()[start..end]
.iter()
.any(|t| !t.flags.contains(TileFlags::EMPTY))
});
if !has_visible {
return;
}
let dst_width = usize::from(self.width);
let dst_height = usize::from(self.height);
let dst_lb = self.layer_or_alloc(layer);
let mut pending_extras: Vec<(usize, TileExtra)> = Vec::new();
for sy in sy0..sy1 {
let dy = dst_y.saturating_add(sy - src_rect.top());
if usize::from(dy) >= dst_height {
continue;
}
for sx in sx0..sx1 {
let dx = dst_x.saturating_add(sx - src_rect.left());
if usize::from(dx) >= dst_width {
continue;
}
let src_idx = usize::from(sy) * src_width + usize::from(sx);
let tile = &src_lb.buf.as_ref()[src_idx];
if tile.flags.contains(TileFlags::EMPTY) {
continue;
}
let dst_idx = usize::from(dy) * dst_width + usize::from(dx);
let mut out_tile = *tile;
out_tile.flags.remove(TileFlags::HAS_EXTRA);
out_tile.clear_span();
dst_lb.buf.as_mut()[dst_idx] = out_tile;
if tile.flags.contains(TileFlags::HAS_EXTRA) {
if let Some(extra) = src_lb.extra_entry_for(src_idx, tile) {
pending_extras.push((dst_idx, extra));
}
} else {
dst_lb.extras.remove(&dst_idx);
}
}
}
for (idx, extra) in pending_extras {
dst_lb.buf.as_mut()[idx].flags.insert(TileFlags::HAS_EXTRA);
dst_lb.extras.insert(idx, extra);
}
}
#[cfg(feature = "color-space")]
#[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,
) {
let Some(src_lb) = src.layer(layer) else {
return;
};
let src_width = usize::from(src.width);
let sx0 = src_rect.left().min(src.width);
let sx1 = src_rect.right().min(src.width);
let sy0 = src_rect.top().min(src.height);
let sy1 = src_rect.bottom().min(src.height);
if sx0 >= sx1 || sy0 >= sy1 {
return;
}
let has_visible = (sy0..sy1).any(|sy| {
let start = usize::from(sy) * src_width + usize::from(sx0);
let end = usize::from(sy) * src_width + usize::from(sx1);
src_lb.buf.as_ref()[start..end]
.iter()
.any(|t| !t.flags.contains(TileFlags::EMPTY))
});
if !has_visible {
return;
}
let dst_width = usize::from(self.width);
let dst_height = usize::from(self.height);
let dst_lb = self.layer_or_alloc(layer);
let mut pending_extras: Vec<(usize, TileExtra)> = Vec::new();
for sy in sy0..sy1 {
let dy = dst_y.saturating_add(sy - src_rect.top());
if usize::from(dy) >= dst_height {
continue;
}
for sx in sx0..sx1 {
let dx = dst_x.saturating_add(sx - src_rect.left());
if usize::from(dx) >= dst_width {
continue;
}
let src_idx = usize::from(sy) * src_width + usize::from(sx);
let tile = &src_lb.buf.as_ref()[src_idx];
if tile.flags.contains(TileFlags::EMPTY) {
continue;
}
let dst_idx = usize::from(dy) * dst_width + usize::from(dx);
let mut blended = *tile;
{
let dst_tile = &dst_lb.buf.as_ref()[dst_idx];
if mode != BlendMode::Linear || fg_alpha != 1.0 {
blended.style.fg =
blend_fg(mode, tile.style.fg, dst_tile.style.fg, fg_alpha);
}
if mode != BlendMode::Linear || bg_alpha != 1.0 {
blended.style.bg =
blend_bg(mode, tile.style.bg, dst_tile.style.bg, bg_alpha);
}
}
blended.flags.remove(TileFlags::HAS_EXTRA);
blended.clear_span();
dst_lb.buf.as_mut()[dst_idx] = blended;
if tile.flags.contains(TileFlags::HAS_EXTRA) {
if let Some(extra) = src_lb.extra_entry_for(src_idx, tile) {
pending_extras.push((dst_idx, extra));
}
} else {
dst_lb.extras.remove(&dst_idx);
}
}
}
for (idx, extra) in pending_extras {
dst_lb.buf.as_mut()[idx].flags.insert(TileFlags::HAS_EXTRA);
dst_lb.extras.insert(idx, extra);
}
}
pub fn layers(&self) -> impl Iterator<Item = DrawCell<'_>> + '_ {
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;
DrawCell {
layer: id,
pos: Pos::new(x, y),
tile,
grapheme: lb.extra_for(i, tile),
tint: lb.tint_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) {
dst.has_spans |= self.has_spans;
let layer0 = self.layer0();
let cell_count = layer0.buf.as_ref().len();
let dst_layer0 = dst.layer0_mut();
dst_layer0.buf.as_mut().copy_from_slice(layer0.buf.as_ref());
dst_layer0.extras.clear();
for (&idx, extra) in &layer0.extras {
if layer0.buf.as_ref()[idx]
.flags
.contains(TileFlags::HAS_EXTRA)
{
dst_layer0.extras.insert(idx, extra.clone());
}
}
for id in 1..=self.max_layer {
let Some(lb) = self.layer(id) else {
continue;
};
let src_buf = lb.buf.as_ref();
debug_assert_eq!(src_buf.len(), cell_count);
let dst_layer0 = dst.layer0_mut();
for (idx, tile) in src_buf.iter().enumerate() {
if !tile.flags.contains(TileFlags::EMPTY) {
{
let out = &mut dst_layer0.buf.as_mut()[idx];
out.glyph = tile.glyph;
out.width = tile.width;
out.style.fg = tile.style.fg;
out.dx = tile.dx;
out.dy = tile.dy;
out.flags = tile.flags;
out.span_w = tile.span_w;
out.span_h = tile.span_h;
}
if tile.flags.contains(TileFlags::HAS_EXTRA) {
if let Some(extra) = lb.extra_entry_for(idx, tile) {
dst_layer0.extras.insert(idx, extra);
}
} else {
dst_layer0.extras.remove(&idx);
}
}
if tile.style.bg != Color::Default {
dst_layer0.buf.as_mut()[idx].style.bg = tile.style.bg;
}
}
}
}
pub fn diff<'a>(&'a self, other: &'a Self) -> impl Iterator<Item = DrawCell<'a>> + '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;
DrawCell {
layer: id,
pos: Pos::new(x, y),
tile,
grapheme: cur_lb.extra_for(i, tile),
tint: cur_lb.tint_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.entry_for(i, tile);
let prev_extra = prev_lb.entry_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(DrawCell {
layer: id,
pos: Pos::new(x, y),
tile,
grapheme: cur_extra.and_then(|e| e.grapheme.as_deref()),
tint: cur_extra.map_or(Tint::None, |e| e.tint),
})
},
))
}
}
})
}
}
enum LayerDiff<F, D> {
Empty,
Full(F),
Diff(D),
}
impl<'a, F, D> Iterator for LayerDiff<F, D>
where
F: Iterator<Item = DrawCell<'a>>,
D: Iterator<Item = DrawCell<'a>>,
{
type Item = DrawCell<'a>;
fn next(&mut self) -> Option<Self::Item> {
match self {
Self::Empty => None,
Self::Full(iter) => iter.next(),
Self::Diff(iter) => iter.next(),
}
}
}
#[cfg(feature = "color-space")]
#[allow(clippy::float_cmp)]
fn blend_color(mode: BlendMode, src: Color, dst: Color, t: f32) -> Color {
use gem::Mix as _;
use gem::rgb::{HasBlue as _, HasGreen as _, HasRed 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).mix(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 = "color-space")]
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 = "color-space")]
fn blend_fg(mode: BlendMode, src: Color, dst: Color, t: f32) -> Color {
blend_color(mode, src, dst, t)
}
#[cfg(feature = "color-space")]
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[Pos::new(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_tile(0, (5, 5), tile);
assert_eq!(grid[Pos::new(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.put_tile(0, (5, 5), tile).is_some());
assert_eq!(grid.tile(0, (5, 5)).unwrap().glyph(), 'Y');
assert!(grid.tile(0, (10, 0)).is_none());
assert!(grid.put_tile(0, (0, 10), Tile::default()).is_none());
}
#[test]
fn test_grid_put_tile_out_of_bounds_returns_none() {
let mut grid = Grid::new(10, 10);
assert!(grid.put_tile(0, (10, 0), Tile::default()).is_none());
}
#[test]
#[should_panic(expected = "index out of bounds")]
fn test_grid_index_panics_out_of_bounds() {
let grid = Grid::new(10, 10);
let _ = &grid[Pos::new(0, 10)];
}
#[test]
fn test_grid_diff() {
let mut g1 = Grid::new(2, 2);
let g2 = Grid::new(2, 2);
g1.put_tile(0, (0, 0), Tile::default().with_glyph('A'));
let diffs: Vec<_> = g1.diff(&g2).collect();
assert_eq!(diffs.len(), 1);
assert_eq!(
diffs[0],
DrawCell::on_layer(0, Pos::new(0, 0), &g1[Pos::new(0, 0)])
);
}
#[test]
fn test_grid_resize_expand() {
let mut grid = Grid::new(3, 3);
grid.put_tile(0, (1, 1), Tile::default().with_glyph('X'));
grid.resize(6, 6);
assert_eq!(grid.width(), 6);
assert_eq!(grid.height(), 6);
assert_eq!(grid[Pos::new(1, 1)].glyph(), 'X'); assert_eq!(grid[Pos::new(5, 5)].glyph(), ' '); }
#[test]
fn test_grid_resize_shrink() {
let mut grid = Grid::new(10, 10);
grid.put_tile(0, (1, 1), Tile::default().with_glyph('A'));
grid.resize(5, 5);
assert_eq!(grid.width(), 5);
assert_eq!(grid.height(), 5);
assert_eq!(grid[Pos::new(1, 1)].glyph(), 'A'); }
#[test]
fn test_grid_resize_preserves_overlap() {
let mut grid = Grid::new(4, 4);
grid.put_tile(0, (0, 0), Tile::default().with_glyph('@'));
grid.put_tile(0, (3, 3), Tile::default().with_glyph('X'));
grid.resize(3, 3); assert_eq!(grid[Pos::new(0, 0)].glyph(), '@');
assert_eq!(grid[Pos::new(2, 2)].glyph(), ' '); }
#[test]
fn test_grid_display() {
let mut grid = Grid::new(3, 2);
grid.put_tile(0, (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).unwrap() {
#[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[Pos::new(0, 0)].glyph(), 'A');
assert_eq!(grid[Pos::new(1, 0)].glyph(), 'B');
assert_eq!(grid[Pos::new(0, 1)].glyph(), 'C');
assert_eq!(grid[Pos::new(1, 1)].glyph(), 'D');
}
#[test]
fn test_grid_cells_mut_unallocated_layer_is_none() {
let mut grid = Grid::new(2, 2);
assert!(grid.cells_mut(3).is_none());
assert!(grid.tile(3, (0, 0)).is_none());
}
#[test]
fn test_grid_cells_mut_or_alloc_allocates_an_unwritten_layer() {
use crate::style::Style;
let mut grid = Grid::new(2, 2);
assert!(grid.cells_mut(2).is_none());
for (_, _, tile) in grid.cells_mut_or_alloc(2) {
*tile = Tile::new('x', Style::default());
}
assert!(grid.cells_mut(2).is_some());
assert_eq!(grid.tile(2, (0, 0)).unwrap().glyph(), 'x');
}
#[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_offset_from_tuple() {
let o: Offset = (-3i16, 7i16).into();
assert_eq!(o, Offset::new(-3, 7));
let t: (i16, i16) = o.into();
assert_eq!(t, (-3, 7));
}
#[test]
fn test_offset_default_is_zero() {
assert_eq!(Offset::default(), Offset::new(0, 0));
}
#[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_new_layer_table_starts_at_a_single_slot() {
let g = Grid::new(5, 5);
assert_eq!(g.layers.len(), 1);
assert_eq!(g.max_layer(), 0);
}
#[test]
fn test_grid_layer_or_alloc_grows_table_lazily_to_the_written_id() {
let mut g = Grid::new(5, 5);
g.put_tile(10, (0, 0), Tile::new('@', Style::default()));
assert_eq!(g.layers.len(), 11);
assert_eq!(g.max_layer(), 10);
assert!(g.layer(10).is_some());
for id in 1u8..10 {
assert!(g.layer(id).is_none(), "layer {id} should be None");
}
}
#[test]
fn test_grid_layer_beyond_table_length_reads_as_none() {
let g = Grid::new(5, 5);
assert_eq!(g.layers.len(), 1);
assert!(g.layer(255).is_none());
assert!(g.tile(255, (0, 0)).is_none());
assert!(g.grapheme(255, 0, 0).is_none());
}
#[test]
fn test_grid_clear_beyond_table_length_is_a_no_op() {
let mut g = Grid::new(5, 5);
g.clear(255);
assert_eq!(g.layers.len(), 1);
}
#[test]
fn test_grid_layer_table_growth_is_monotonic_across_writes() {
let mut g = Grid::new(5, 5);
g.put_tile(20, (1, 1), Tile::new('H', Style::default()));
assert_eq!(g.layers.len(), 21);
g.put_tile(2, (0, 0), Tile::new('L', Style::default()));
assert_eq!(
g.layers.len(),
21,
"writing a lower id must not shrink the table"
);
assert_eq!(g.max_layer(), 20);
assert_eq!(g.tile(20, (1, 1)).unwrap().glyph, 'H');
assert_eq!(g.tile(2, (0, 0)).unwrap().glyph, 'L');
}
#[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].layer, 0);
assert_eq!(diffs[0].pos, Pos::new(2, 3));
assert_eq!(diffs[0].tile.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(|c| c.layer == 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(|c| c.layer).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.tile(2, (1, 1)).unwrap().glyph, 'Z');
assert_eq!(g[Pos::new(1, 1)].glyph, ' ');
assert!(g.tile(3, (0, 0)).is_none());
}
#[test]
fn test_grid_tile_mut_writes_in_place_without_clearing_spans() {
let mut g = Grid::new(4, 4);
g.write_span(0, 0, 0, &["C=", "[]"], Style::default())
.unwrap();
g.tile_mut(0, (0, 0)).unwrap().glyph = 'x';
assert_eq!(g[Pos::new(0, 0)].glyph(), 'x');
assert!(g.tile_mut(1, (0, 0)).is_none());
assert!(g.tile_mut(0, (10, 10)).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.tile(0, (0, 0)).unwrap().glyph, 'A');
assert!(g.tile(1, (0, 0)).is_some());
assert_eq!(g.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[Pos::new(0, 0)].glyph, ' ');
assert_eq!(g.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[Pos::new(0, 0)].glyph, 'A');
assert_eq!(cloned.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[Pos::new(0, 0)].glyph, 'Z');
assert_eq!(g[Pos::new(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[Pos::new(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_tile(0, (0, 0), Tile::new('X', Style::default()));
assert_eq!(g.grapheme(0, 0, 0), None);
assert!(!g[Pos::new(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[Pos::new(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);
}
#[test]
fn tint_round_trips_and_defaults_to_none() {
let mut g = Grid::new(4, 4);
assert_eq!(g.tint(0, 1, 1), Tint::None);
g.write_grapheme(0, 1, 1, "@", Style::default());
g.set_tint(0, 1, 1, Tint::multiply(128, 64, 32));
assert_eq!(g.tint(0, 1, 1), Tint::multiply(128, 64, 32));
g.set_tint(0, 1, 1, Tint::None);
assert_eq!(g.tint(0, 1, 1), Tint::None);
}
#[test]
fn tint_is_per_layer_and_per_cell() {
let mut g = Grid::new(4, 4);
g.set_tint(0, 1, 1, Tint::multiply(10, 20, 30));
g.set_tint(3, 1, 1, Tint::mix(1, 2, 3, 4));
assert_eq!(g.tint(0, 1, 1), Tint::multiply(10, 20, 30));
assert_eq!(g.tint(3, 1, 1), Tint::mix(1, 2, 3, 4));
assert_eq!(g.tint(0, 1, 2), Tint::None);
assert_eq!(g.tint(1, 1, 1), Tint::None);
}
#[test]
fn tint_out_of_bounds_reads_none_and_writes_nothing() {
let mut g = Grid::new(2, 2);
g.set_tint(0, 9, 9, Tint::multiply(1, 2, 3));
assert_eq!(g.tint(0, 9, 9), Tint::None);
assert_eq!(g.tint(0, 0, 0), Tint::None);
}
#[test]
fn writing_a_glyph_over_a_tinted_cell_drops_the_tint() {
let mut g = Grid::new(4, 4);
g.write_grapheme(0, 1, 1, "@", Style::default());
g.set_tint(0, 1, 1, Tint::multiply(128, 128, 128));
g.write_grapheme(0, 1, 1, "#", Style::default());
assert_eq!(g.tint(0, 1, 1), Tint::None);
}
#[test]
fn put_tile_drops_the_tint() {
let mut g = Grid::new(4, 4);
g.set_tint(0, 1, 1, Tint::multiply(128, 128, 128));
g.put_tile(0, Pos::new(1, 1), Tile::new('x', Style::default()));
assert_eq!(g.tint(0, 1, 1), Tint::None);
}
#[test]
fn clear_drops_every_tint_on_the_layer() {
let mut g = Grid::new(4, 4);
g.set_tint(0, 1, 1, Tint::multiply(1, 2, 3));
g.set_tint(1, 1, 1, Tint::multiply(4, 5, 6));
g.clear(0);
assert_eq!(g.tint(0, 1, 1), Tint::None);
assert_eq!(g.tint(1, 1, 1), Tint::multiply(4, 5, 6));
}
#[test]
fn resize_remaps_a_tint_to_the_new_stride() {
let mut g = Grid::new(4, 4);
g.write_grapheme(0, 3, 1, "@", Style::default());
g.set_tint(0, 3, 1, Tint::mix(200, 100, 50, 128));
g.resize(8, 4);
assert_eq!(g.tint(0, 3, 1), Tint::mix(200, 100, 50, 128));
assert_eq!(g.tint(0, 7, 0), Tint::None);
g.resize(2, 4);
assert_eq!(g.tint(0, 3, 1), Tint::None);
}
#[test]
fn blit_carries_a_tint_across_grids() {
let mut src = Grid::new(4, 4);
src.write_grapheme(0, 1, 1, "@", Style::default());
src.set_tint(0, 1, 1, Tint::multiply(64, 128, 192));
let mut dst = Grid::new(4, 4);
dst.set_tint(0, 1, 1, Tint::mix(9, 9, 9, 9));
dst.blit(0, &src, Rect::new(0, 0, 4, 4), 0, 0);
assert_eq!(dst.tint(0, 1, 1), Tint::multiply(64, 128, 192));
assert_eq!(dst.tint(0, 0, 0), Tint::None);
}
#[test]
fn blit_clears_a_destination_tint_where_the_source_has_none() {
let mut src = Grid::new(2, 2);
src.write_grapheme(0, 0, 0, "@", Style::default());
let mut dst = Grid::new(2, 2);
dst.set_tint(0, 0, 0, Tint::multiply(1, 2, 3));
dst.blit(0, &src, Rect::new(0, 0, 2, 2), 0, 0);
assert_eq!(dst.tint(0, 0, 0), Tint::None);
}
#[cfg(feature = "egc")]
#[test]
fn a_tint_and_a_grapheme_share_one_entry_without_clobbering_each_other() {
let mut g = Grid::new(4, 4);
g.write_grapheme(0, 1, 1, "e\u{0301}", Style::default());
g.set_tint(0, 1, 1, Tint::multiply(128, 128, 128));
assert_eq!(g.grapheme(0, 1, 1), Some("e\u{0301}"));
assert_eq!(g.tint(0, 1, 1), Tint::multiply(128, 128, 128));
g.set_tint(0, 1, 1, Tint::None);
assert_eq!(g.grapheme(0, 1, 1), Some("e\u{0301}"));
assert_eq!(g.tint(0, 1, 1), Tint::None);
}
#[cfg(feature = "egc")]
#[test]
fn a_tint_alone_keeps_grapheme_reads_answering_none() {
let mut g = Grid::new(4, 4);
g.write_grapheme(0, 1, 1, "@", Style::default());
g.set_tint(0, 1, 1, Tint::multiply(128, 128, 128));
assert_eq!(g.grapheme(0, 1, 1), None);
assert_eq!(g.tint(0, 1, 1), Tint::multiply(128, 128, 128));
}
#[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].pos, Pos::new(0, 0));
assert_eq!(diffs[0].grapheme, 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[Pos::new(0, 0)].glyph, 'e');
assert_eq!(dst.grapheme(0, 0, 0), Some("e\u{0301}"));
}
#[test]
fn test_grid_blit_empty_rect_is_a_no_op() {
let src = Grid::new(2, 2);
let mut dst = Grid::new(2, 2);
dst.put_tile(0, (0, 0), Tile::new('x', Style::default()));
dst.blit(0, &src, Rect::new(0, 0, 0, 0), 0, 0);
assert_eq!(dst[Pos::new(0, 0)].glyph(), 'x');
assert_eq!(dst.max_layer(), 0);
}
#[test]
fn test_grid_blit_fully_transparent_source_does_not_allocate_dst_layer() {
let src = Grid::new(2, 2);
let mut dst = Grid::new(2, 2);
dst.blit(3, &src, Rect::new(0, 0, 2, 2), 0, 0);
assert_eq!(dst.max_layer(), 0);
}
#[test]
fn test_grid_blit_skips_out_of_bounds_source_and_dest_regions() {
let mut src = Grid::new(4, 4);
for y in 0..4 {
for x in 0..4 {
src.put_tile(0, (x, y), Tile::new('#', Style::default()));
}
}
let mut dst = Grid::new(2, 2);
dst.blit(0, &src, Rect::new(2, 2, 10, 10), 1, 1);
assert_eq!(dst[Pos::new(1, 1)].glyph(), '#');
assert_eq!(dst[Pos::new(0, 0)].glyph(), ' ');
assert_eq!(dst[Pos::new(0, 1)].glyph(), ' ');
assert_eq!(dst[Pos::new(1, 0)].glyph(), ' ');
}
#[test]
fn test_grid_blit_sub_cell_offset_and_transparency() {
let mut src = Grid::new(2, 2);
src.put_tile(0, (0, 0), Tile::new('A', Style::default()));
src.put_tile(0, (0, 1), Tile::new('B', Style::default()));
let mut dst = Grid::new(3, 3);
dst.put_tile(0, (2, 2), Tile::new('Z', Style::default()));
dst.blit(0, &src, Rect::new(0, 0, 2, 2), 1, 1);
assert_eq!(dst[Pos::new(1, 1)].glyph(), 'A');
assert_eq!(dst[Pos::new(1, 2)].glyph(), 'B');
assert_eq!(dst[Pos::new(2, 1)].glyph(), ' ');
assert_eq!(dst[Pos::new(2, 2)].glyph(), 'Z');
}
#[test]
fn test_grid_blit_multi_layer_independent() {
let mut src = Grid::new(2, 2);
src.put_tile(0, (0, 0), Tile::new('a', Style::default()));
src.put_tile(2, (0, 0), Tile::new('b', Style::default()));
let mut dst = Grid::new(2, 2);
dst.blit(0, &src, Rect::new(0, 0, 2, 2), 0, 0);
dst.blit(2, &src, Rect::new(0, 0, 2, 2), 0, 0);
assert_eq!(dst.tile(0, (0, 0)).map(Tile::glyph), Some('a'));
assert_eq!(dst.tile(2, (0, 0)).map(Tile::glyph), Some('b'));
assert!(dst.tile(1, (0, 0)).is_none());
}
#[test]
fn test_grid_blit_dest_origin_near_u16_max_does_not_wrap() {
let mut src = Grid::new(4, 1);
src.put_tile(0, (3, 0), Tile::new('Q', Style::default()));
let mut dst = Grid::new(4, 1);
dst.blit(0, &src, Rect::new(0, 0, 4, 1), u16::MAX - 1, 0);
assert_eq!(dst[Pos::new(1, 0)].glyph(), ' ');
for x in 0..4 {
assert_eq!(
dst[Pos::new(x, 0)].glyph(),
' ',
"cell ({x}, 0) unexpectedly written"
);
}
}
#[test]
fn test_grid_blit_normal_offset_unaffected_by_overflow_fix() {
let mut src = Grid::new(2, 2);
src.put_tile(0, (0, 0), Tile::new('A', Style::default()));
src.put_tile(0, (1, 1), Tile::new('B', Style::default()));
let mut dst = Grid::new(4, 4);
dst.blit(0, &src, Rect::new(0, 0, 2, 2), 1, 1);
assert_eq!(dst[Pos::new(1, 1)].glyph(), 'A');
assert_eq!(dst[Pos::new(2, 2)].glyph(), 'B');
}
#[cfg(feature = "color-space")]
#[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 = "color-space")]
#[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 = "color-space")]
#[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 = "color-space")]
#[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 = "color-space")]
#[test]
fn test_grid_blit_alpha_screen_blends_fg() {
let mut src = Grid::new(1, 1);
src.put_tile(
0,
(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_tile(
0,
(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[Pos::new(0, 0)].style.fg,
Color::Rgb {
r: 224,
g: 224,
b: 224
}
);
}
#[cfg(feature = "color-space")]
#[test]
fn test_grid_blit_alpha_dest_origin_near_u16_max_does_not_wrap() {
let mut src = Grid::new(4, 1);
src.put_tile(0, (3, 0), Tile::new('Q', Style::default()));
let mut dst = Grid::new(4, 1);
dst.blit_alpha(
0,
&src,
Rect::new(0, 0, 4, 1),
u16::MAX - 1,
0,
BlendMode::Linear,
1.0,
1.0,
);
for x in 0..4 {
assert_eq!(
dst[Pos::new(x, 0)].glyph(),
' ',
"cell ({x}, 0) unexpectedly written"
);
}
}
#[cfg(feature = "color-space")]
#[test]
fn test_grid_blit_alpha_linear_direction() {
let mut src = Grid::new(1, 1);
src.put_tile(
0,
(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_tile(
0,
(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[Pos::new(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 = "color-space")]
#[test]
fn test_blend_color_non_rgb_passthrough_all_modes() {
for mode in [
BlendMode::Linear,
BlendMode::Screen,
BlendMode::Dodge,
BlendMode::Burn,
BlendMode::Overlay,
BlendMode::Multiply,
] {
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[Pos::new(0, 0)].glyph, 'e');
assert_eq!(flattened.grapheme(0, 0, 0), Some("e\u{0301}"));
}
#[test]
fn test_grid_flatten_into_single_layer_is_a_plain_copy() {
let mut g = Grid::new(2, 2);
g.put_tile(0, (0, 0), Tile::new('a', Style::default()));
g.put_tile(0, (1, 1), Tile::new('b', Style::default()));
let mut flattened = Grid::new(2, 2);
g.flatten_into(&mut flattened);
assert_eq!(flattened[Pos::new(0, 0)].glyph(), 'a');
assert_eq!(flattened[Pos::new(1, 1)].glyph(), 'b');
assert_eq!(flattened[Pos::new(1, 0)].glyph(), ' ');
}
#[test]
fn test_grid_flatten_into_higher_layer_overwrites_glyph_and_fg_but_not_default_bg() {
let mut g = Grid::new(1, 1);
g.put_tile(
0,
(0, 0),
Tile::new('a', Style::new().fg(Color::BLACK).bg(Color::WHITE)),
);
g.put_tile(1, (0, 0), Tile::new('b', Style::new().fg(Color::WHITE)));
let mut flattened = Grid::new(1, 1);
g.flatten_into(&mut flattened);
let out = flattened[Pos::new(0, 0)];
assert_eq!(out.glyph(), 'b');
assert_eq!(out.style().fg, Color::WHITE);
assert_eq!(out.style().bg, Color::WHITE);
}
#[test]
fn test_grid_flatten_into_empty_higher_layer_cell_is_transparent() {
let mut g = Grid::new(2, 1);
g.put_tile(0, (0, 0), Tile::new('a', Style::default()));
g.put_tile(0, (1, 0), Tile::new('b', Style::default()));
g.put_tile(1, (0, 0), Tile::new('c', Style::default()));
let mut flattened = Grid::new(2, 1);
g.flatten_into(&mut flattened);
assert_eq!(flattened[Pos::new(0, 0)].glyph(), 'c');
assert_eq!(flattened[Pos::new(1, 0)].glyph(), 'b');
}
#[test]
fn test_grid_flatten_into_multi_layer_stale_dst_extra_is_cleared() {
let mut flattened = Grid::new(1, 1);
flattened.put_tile(0, (0, 0), Tile::new('z', Style::default()));
let g = Grid::new(1, 1);
g.flatten_into(&mut flattened);
assert_eq!(flattened[Pos::new(0, 0)].glyph(), ' ');
}
#[test]
fn write_span_marks_anchor_and_covered_cells() {
let mut grid = Grid::new(4, 4);
grid.write_span(0, 1, 1, &["C=", "[]"], Style::default())
.expect("2x2 span fits in a 4x4 grid");
let anchor = grid.tile(0, (1, 1)).unwrap();
assert!(anchor.flags().contains(TileFlags::SPAN_ANCHOR));
assert_eq!(anchor.span(), (2, 2));
assert_eq!(anchor.span_offset(), None);
assert_eq!(anchor.glyph(), 'C');
for (x, y, glyph, offset) in [
(2, 1, '=', (1, 0)),
(1, 2, '[', (0, 1)),
(2, 2, ']', (1, 1)),
] {
let tile = grid.tile(0, (x, y)).unwrap();
assert!(
tile.flags().contains(TileFlags::SPAN_COVERED),
"({x}, {y}) should be covered"
);
assert_eq!(tile.glyph(), glyph, "({x}, {y}) keeps its fallback glyph");
assert_eq!(tile.span_offset(), Some(offset));
assert_eq!(tile.span(), (1, 1));
}
}
#[test]
fn write_span_keeps_the_fallback_glyphs_readable() {
let mut grid = Grid::new(4, 4);
grid.write_span(0, 0, 0, &["C=", "[]"], Style::default())
.unwrap();
let read = |x, y| grid.tile(0, (x, y)).unwrap().glyph();
assert_eq!(
[read(0, 0), read(1, 0), read(0, 1), read(1, 1)],
['C', '=', '[', ']']
);
}
#[test]
fn span_owner_reports_the_anchor_from_every_cell_of_the_span() {
let mut grid = Grid::new(6, 6);
grid.write_span(0, 2, 3, &["AB", "CD", "EF"], Style::default())
.unwrap();
for (x, y) in [(2, 3), (3, 3), (2, 4), (3, 4), (2, 5), (3, 5)] {
assert_eq!(grid.span_owner(0, x, y), Some(Pos::new(2, 3)), "({x}, {y})");
}
assert_eq!(grid.span_owner(0, 0, 0), None);
assert_eq!(grid.span_owner(0, 99, 99), None);
assert_eq!(grid.span_owner(3, 3, 3), None);
}
#[test]
fn write_span_rejects_malformed_input_without_writing() {
let mut grid = Grid::new(4, 4);
assert_eq!(
grid.write_span(0, 0, 0, &[] as &[&str], Style::default()),
None
);
assert_eq!(grid.write_span(0, 0, 0, &[""], Style::default()), None);
assert_eq!(
grid.write_span(0, 0, 0, &["ab", "c"], Style::default()),
None
);
assert_eq!(grid.write_span(0, 3, 0, &["ab"], Style::default()), None);
assert_eq!(
grid.write_span(0, 0, 3, &["a", "b"], Style::default()),
None
);
for y in 0..4 {
for x in 0..4 {
assert!(
grid[Pos::new(x, y)].is_empty(),
"({x}, {y}) should be untouched"
);
}
}
}
#[test]
fn write_span_takes_any_as_ref_str_row() {
let mut grid = Grid::new(4, 4);
let rows: Vec<String> = (0..2)
.map(|row| {
(0..2)
.map(|col| if (row, col) == (0, 0) { 'C' } else { ' ' })
.collect()
})
.collect();
assert_eq!(grid.write_span(0, 0, 0, &rows, Style::default()), Some(()));
assert_eq!(grid[Pos::new(0, 0)].glyph(), 'C');
assert_eq!(grid[Pos::new(0, 0)].span(), (2, 2));
}
#[test]
fn write_span_uniform_writes_the_anchor_once_and_fills_the_rest() {
let mut grid = Grid::new(4, 4);
assert_eq!(
grid.write_span_uniform(0, (1, 1), (2, 2), 'C', '.', Style::default()),
Some(())
);
assert_eq!(grid[Pos::new(1, 1)].glyph(), 'C');
assert_eq!(grid[Pos::new(1, 1)].span(), (2, 2));
for (x, y) in [(2, 1), (1, 2), (2, 2)] {
assert_eq!(grid[Pos::new(x, y)].glyph(), '.', "({x}, {y})");
assert_eq!(grid.span_owner(0, x, y), Some(Pos::new(1, 1)));
}
}
#[test]
fn write_span_uniform_matches_the_equivalent_write_span() {
let mut uniform = Grid::new(4, 4);
uniform
.write_span_uniform(0, (0, 0), (3, 2), 'C', ' ', Style::default())
.unwrap();
let mut rows = Grid::new(4, 4);
rows.write_span(0, 0, 0, &["C ", " "], Style::default())
.unwrap();
for y in 0..4 {
for x in 0..4 {
assert_eq!(
uniform[Pos::new(x, y)],
rows[Pos::new(x, y)],
"({x}, {y}) differs"
);
}
}
}
#[test]
fn write_span_uniform_rejects_a_degenerate_or_oversized_footprint() {
let mut grid = Grid::new(4, 4);
let style = Style::default();
assert_eq!(
grid.write_span_uniform(0, (0, 0), (0, 2), 'C', ' ', style),
None
);
assert_eq!(
grid.write_span_uniform(0, (0, 0), (2, 0), 'C', ' ', style),
None
);
assert_eq!(
grid.write_span_uniform(0, (0, 0), (256, 1), 'C', ' ', style),
None
);
assert_eq!(
grid.write_span_uniform(0, (3, 0), (2, 1), 'C', ' ', style),
None
);
for y in 0..4 {
for x in 0..4 {
assert!(
grid[Pos::new(x, y)].is_empty(),
"({x}, {y}) should be untouched"
);
}
}
}
#[test]
fn writing_into_a_covered_cell_clears_the_whole_span() {
let mut grid = Grid::new(4, 4);
grid.write_span(0, 0, 0, &["C=", "[]"], Style::default())
.unwrap();
grid.put_tile(0, (1, 1), Tile::new('x', Style::default()));
assert_eq!(grid[Pos::new(1, 1)].glyph(), 'x');
for (x, y) in [(0, 0), (1, 0), (0, 1)] {
let tile = grid[Pos::new(x, y)];
assert!(tile.is_empty(), "({x}, {y}) should have been cleared");
assert_eq!(tile.flags(), TileFlags::EMPTY);
}
}
#[test]
fn writing_over_the_anchor_clears_the_whole_span() {
let mut grid = Grid::new(4, 4);
grid.write_span(0, 0, 0, &["C=", "[]"], Style::default())
.unwrap();
grid.put_tile(0, (0, 0), Tile::new('x', Style::default()));
assert_eq!(grid[Pos::new(0, 0)].glyph(), 'x');
assert_eq!(grid[Pos::new(0, 0)].span(), (1, 1));
for (x, y) in [(1, 0), (0, 1), (1, 1)] {
assert!(
grid[Pos::new(x, y)].is_empty(),
"({x}, {y}) should be cleared"
);
}
}
#[test]
fn overlapping_spans_erase_the_old_one_entirely() {
let mut grid = Grid::new(4, 4);
grid.write_span(0, 0, 0, &["AB", "CD"], Style::default())
.unwrap();
grid.write_span(0, 1, 1, &["EF", "GH"], Style::default())
.unwrap();
assert!(grid[Pos::new(0, 0)].is_empty());
assert!(grid[Pos::new(1, 0)].is_empty());
assert!(grid[Pos::new(0, 1)].is_empty());
assert_eq!(grid[Pos::new(1, 1)].glyph(), 'E');
assert_eq!(grid.span_owner(0, 2, 2), Some(Pos::new(1, 1)));
}
#[test]
fn clear_span_works_from_any_cell_of_the_span() {
let mut grid = Grid::new(4, 4);
for from in [(0, 0), (1, 0), (0, 1), (1, 1)] {
grid.write_span(0, 0, 0, &["C=", "[]"], Style::default())
.unwrap();
grid.clear_span(0, from.0, from.1);
for y in 0..2 {
for x in 0..2 {
assert!(
grid[Pos::new(x, y)].is_empty(),
"clearing from {from:?}: ({x}, {y})"
);
}
}
}
grid.put_tile(0, (3, 3), Tile::new('z', Style::default()));
grid.clear_span(0, 3, 3);
assert_eq!(grid[Pos::new(3, 3)].glyph(), 'z');
}
#[test]
fn spans_are_layer_scoped() {
let mut grid = Grid::new(4, 4);
grid.write_span(1, 0, 0, &["C=", "[]"], Style::default())
.unwrap();
assert_eq!(grid.span_owner(1, 1, 1), Some(Pos::new(0, 0)));
assert_eq!(grid.span_owner(0, 1, 1), None);
assert_eq!(grid.span_owner(0, 0, 0), None);
grid.put_tile(0, (1, 1), Tile::new('x', Style::default()));
assert_eq!(grid.span_owner(1, 1, 1), Some(Pos::new(0, 0)));
}
#[test]
fn flatten_into_carries_span() {
let mut grid = Grid::new(4, 4);
grid.write_span(2, 1, 1, &["C=", "[]"], Style::default())
.unwrap();
let mut flat = Grid::new(4, 4);
grid.flatten_into(&mut flat);
assert_eq!(flat[Pos::new(1, 1)].span(), (2, 2));
assert!(
flat[Pos::new(1, 1)]
.flags()
.contains(TileFlags::SPAN_ANCHOR)
);
assert_eq!(flat.span_owner(0, 2, 2), Some(Pos::new(1, 1)));
assert_eq!(flat[Pos::new(2, 2)].glyph(), ']');
}
#[test]
fn blit_degrades_a_span_to_its_fallback_glyphs() {
let mut src = Grid::new(4, 4);
src.write_span(0, 0, 0, &["C=", "[]"], Style::default())
.unwrap();
let mut dst = Grid::new(4, 4);
dst.blit(0, &src, Rect::new(0, 0, 2, 2), 0, 0);
assert_eq!(dst[Pos::new(0, 0)].glyph(), 'C');
assert_eq!(dst[Pos::new(1, 1)].glyph(), ']');
assert_eq!(dst[Pos::new(0, 0)].span(), (1, 1));
assert_eq!(dst.span_owner(0, 1, 1), None);
for (x, y) in [(0, 0), (1, 0), (0, 1), (1, 1)] {
let flags = dst[Pos::new(x, y)].flags();
assert!(!flags.contains(TileFlags::SPAN_ANCHOR), "({x}, {y})");
assert!(!flags.contains(TileFlags::SPAN_COVERED), "({x}, {y})");
}
}
#[test]
fn clear_region_clears_a_span_it_only_partly_covers() {
let mut grid = Grid::new(4, 4);
grid.write_span(0, 0, 0, &["C=", "[]"], Style::default())
.unwrap();
grid.put_tile(0, (0, 0), Tile::default());
for y in 0..2 {
for x in 0..2 {
assert!(grid[Pos::new(x, y)].is_empty(), "({x}, {y})");
}
}
}
}
#[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[Pos::new(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[Pos::new(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[Pos::new(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);
}
}
}
}