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.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,
}
}
#[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
.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_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, 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.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 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) {
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, Arc<str>)> = 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);
dst_lb.buf.as_mut()[dst_idx] = out_tile;
if tile.flags.contains(TileFlags::HAS_EXTRA) {
if let Some(extra) = src_lb.extra_arc_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 = "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,
) {
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, Arc<str>)> = 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);
dst_lb.buf.as_mut()[dst_idx] = blended;
if tile.flags.contains(TileFlags::HAS_EXTRA) {
if let Some(extra) = src_lb.extra_arc_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 = (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 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;
}
if tile.flags.contains(TileFlags::HAS_EXTRA) {
if let Some(extra) = lb.extra_arc_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 = (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_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.get_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.get_tile(20, 1, 1).unwrap().glyph, 'H');
assert_eq!(g.get_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].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}"));
}
#[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(0, 0, Tile::new('x', Style::default()));
dst.blit(0, &src, Rect::new(0, 0, 0, 0), 0, 0);
assert_eq!(dst.get(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(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.get(1, 1).glyph(), '#');
assert_eq!(dst.get(0, 0).glyph(), ' ');
assert_eq!(dst.get(0, 1).glyph(), ' ');
assert_eq!(dst.get(1, 0).glyph(), ' ');
}
#[test]
fn test_grid_blit_sub_cell_offset_and_transparency() {
let mut src = Grid::new(2, 2);
src.put(0, 0, Tile::new('A', Style::default()));
src.put(0, 1, Tile::new('B', Style::default()));
let mut dst = Grid::new(3, 3);
dst.put(2, 2, Tile::new('Z', Style::default()));
dst.blit(0, &src, Rect::new(0, 0, 2, 2), 1, 1);
assert_eq!(dst.get(1, 1).glyph(), 'A');
assert_eq!(dst.get(1, 2).glyph(), 'B');
assert_eq!(dst.get(2, 1).glyph(), ' ');
assert_eq!(dst.get(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.get_tile(0, 0, 0).map(Tile::glyph), Some('a'));
assert_eq!(dst.get_tile(2, 0, 0).map(Tile::glyph), Some('b'));
assert!(dst.get_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(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.get(1, 0).glyph(), ' ');
for x in 0..4 {
assert_eq!(
dst.get(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(0, 0, Tile::new('A', Style::default()));
src.put(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.get(1, 1).glyph(), 'A');
assert_eq!(dst.get(2, 2).glyph(), 'B');
}
#[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_dest_origin_near_u16_max_does_not_wrap() {
let mut src = Grid::new(4, 1);
src.put(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.get(x, 0).glyph(),
' ',
"cell ({x}, 0) unexpectedly written"
);
}
}
#[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}"));
}
#[test]
fn test_grid_flatten_into_single_layer_is_a_plain_copy() {
let mut g = Grid::new(2, 2);
g.put(0, 0, Tile::new('a', Style::default()));
g.put(1, 1, Tile::new('b', Style::default()));
let mut flattened = Grid::new(2, 2);
g.flatten_into(&mut flattened);
assert_eq!(flattened.get(0, 0).glyph(), 'a');
assert_eq!(flattened.get(1, 1).glyph(), 'b');
assert_eq!(flattened.get(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(
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.get(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(0, 0, Tile::new('a', Style::default()));
g.put(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.get(0, 0).glyph(), 'c');
assert_eq!(flattened.get(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(0, 0, Tile::new('z', Style::default()));
let g = Grid::new(1, 1);
g.flatten_into(&mut flattened);
assert_eq!(flattened.get(0, 0).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);
}
}
}
}