use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use super::{Cell, CellDiff, Color, Rect, Style};
use crate::render::compose::alpha_blend;
use rdom_style::layout::{BorderStyle, CornerStyle};
const WIDE_CLIP_PLACEHOLDER: &str = "…";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BorderSide {
Top,
Right,
Bottom,
Left,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BorderContribution {
pub style: BorderStyle,
pub fg: Color,
pub priority: u64,
pub corner_style: CornerStyle,
pub side: BorderSide,
}
impl BorderContribution {
pub fn pack_priority(depth: u16, dom_index: u32) -> u64 {
((depth as u64) << 32) | ((u32::MAX - dom_index) as u64)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct BorderDirState {
pub winner: Option<BorderContribution>,
pub killed: bool,
}
impl BorderDirState {
pub fn merge(&mut self, new: BorderContribution) {
if new.style.is_none() {
return;
}
if new.style.is_hidden() {
self.killed = true;
return;
}
let new_key = (new.style.rank(), new.priority);
let win = match self.winner {
None => true,
Some(prev) => {
let prev_key = (prev.style.rank(), prev.priority);
new_key > prev_key
}
};
if win {
self.winner = Some(new);
}
}
pub fn is_visible(&self) -> bool {
!self.killed && self.winner.is_some()
}
}
pub type BorderCell = [BorderDirState; 4];
pub const DIR_N: usize = 0;
pub const DIR_E: usize = 1;
pub const DIR_S: usize = 2;
pub const DIR_W: usize = 3;
pub const QUAD_TL: u8 = 0b0001;
pub const QUAD_TR: u8 = 0b0010;
pub const QUAD_BL: u8 = 0b0100;
pub const QUAD_BR: u8 = 0b1000;
#[derive(Debug, Clone)]
pub struct Buffer {
pub area: Rect,
pub content: Vec<Cell>,
pub border_dirs: Vec<BorderCell>,
pub half_block_quads: Vec<u8>,
compose_alpha: f32,
compose_parent_bg: Color,
}
impl PartialEq for Buffer {
fn eq(&self, other: &Self) -> bool {
self.area == other.area && self.content == other.content
}
}
impl Buffer {
pub fn empty(area: Rect) -> Self {
Self::filled(area, Cell::EMPTY)
}
pub fn filled(area: Rect, cell: Cell) -> Self {
let len = area.area() as usize;
Buffer {
area,
content: vec![cell; len],
border_dirs: vec![BorderCell::default(); len],
half_block_quads: vec![0u8; len],
compose_alpha: 1.0,
compose_parent_bg: Color::Reset,
}
}
pub fn with_cells(area: Rect, cells: Vec<Cell>) -> Self {
assert_eq!(
cells.len(),
area.area() as usize,
"Buffer::with_cells: cell count {} != area.area() {}",
cells.len(),
area.area()
);
let len = area.area() as usize;
Buffer {
area,
content: cells,
border_dirs: vec![BorderCell::default(); len],
half_block_quads: vec![0u8; len],
compose_alpha: 1.0,
compose_parent_bg: Color::Reset,
}
}
pub(crate) fn enter_compose_ctx(&mut self, alpha: f32, parent_bg: Color) -> (f32, Color) {
let saved = (self.compose_alpha, self.compose_parent_bg);
self.compose_alpha = alpha;
self.compose_parent_bg = parent_bg;
saved
}
pub(crate) fn exit_compose_ctx(&mut self, saved: (f32, Color)) {
self.compose_alpha = saved.0;
self.compose_parent_bg = saved.1;
}
fn compose_dst_bg(&self, cell_bg: Color) -> Color {
if cell_bg != Color::Reset {
cell_bg
} else if self.compose_parent_bg != Color::Reset {
self.compose_parent_bg
} else {
Color::Rgb(0, 0, 0)
}
}
fn compose_style_for_cell(&self, x: u16, y: u16, style: Style) -> Style {
if self.compose_alpha >= 1.0 {
return style;
}
let cell_bg = self.cell(x, y).map(|c| c.bg).unwrap_or(Color::Reset);
let dst = self.compose_dst_bg(cell_bg);
Style {
fg: style.fg.map(|fg| alpha_blend(fg, self.compose_alpha, dst)),
bg: style.bg.map(|bg| alpha_blend(bg, self.compose_alpha, dst)),
add_modifier: style.add_modifier,
sub_modifier: style.sub_modifier,
}
}
pub(crate) fn compose_bg_for_cell(&self, x: u16, y: u16, bg: Color) -> Color {
if self.compose_alpha >= 1.0 {
return bg;
}
let cell_bg = self.cell(x, y).map(|c| c.bg).unwrap_or(Color::Reset);
let dst = self.compose_dst_bg(cell_bg);
alpha_blend(bg, self.compose_alpha, dst)
}
pub fn index_of(&self, x: u16, y: u16) -> Option<usize> {
if x < self.area.x || y < self.area.y || x >= self.area.right() || y >= self.area.bottom() {
return None;
}
let dx = (x - self.area.x) as usize;
let dy = (y - self.area.y) as usize;
Some(dy * self.area.width as usize + dx)
}
pub fn cell(&self, x: u16, y: u16) -> Option<&Cell> {
self.index_of(x, y).map(|i| &self.content[i])
}
pub fn cell_mut(&mut self, x: u16, y: u16) -> Option<&mut Cell> {
let i = self.index_of(x, y)?;
Some(&mut self.content[i])
}
pub fn clear(&mut self) {
self.content.fill(Cell::EMPTY);
for dir in &mut self.border_dirs {
*dir = BorderCell::default();
}
self.half_block_quads.fill(0);
}
pub fn add_half_block_quads(&mut self, x: u16, y: u16, quads: u8) {
if let Some(i) = self.index_of(x, y) {
self.half_block_quads[i] |= quads;
}
}
pub fn half_block_quads_at(&self, x: u16, y: u16) -> u8 {
self.index_of(x, y)
.map(|i| self.half_block_quads[i])
.unwrap_or(0)
}
pub fn clear_half_block_quads(&mut self, x: u16, y: u16) {
if let Some(i) = self.index_of(x, y) {
self.half_block_quads[i] = 0;
}
}
pub fn clear_border_at(&mut self, x: u16, y: u16) {
if let Some(i) = self.index_of(x, y) {
self.border_dirs[i] = BorderCell::default();
self.half_block_quads[i] = 0;
}
}
pub fn add_border_dir(&mut self, x: u16, y: u16, dir: usize, contribution: BorderContribution) {
debug_assert!(dir < 4, "BorderCell index out of range");
if let Some(i) = self.index_of(x, y) {
self.border_dirs[i][dir].merge(contribution);
}
}
pub fn border_dir_at(&self, x: u16, y: u16, dir: usize) -> BorderDirState {
debug_assert!(dir < 4, "BorderCell index out of range");
self.index_of(x, y)
.map(|i| self.border_dirs[i][dir])
.unwrap_or_default()
}
pub fn set_border_dir(&mut self, x: u16, y: u16, dir: usize, state: BorderDirState) {
debug_assert!(dir < 4, "BorderCell index out of range");
if let Some(i) = self.index_of(x, y) {
self.border_dirs[i][dir] = state;
}
}
pub fn border_mask_at(&self, x: u16, y: u16) -> u8 {
let Some(i) = self.index_of(x, y) else {
return 0;
};
let cell = &self.border_dirs[i];
let mut mask = 0u8;
if cell[DIR_N].is_visible() {
mask |= 0b0001;
}
if cell[DIR_E].is_visible() {
mask |= 0b0010;
}
if cell[DIR_S].is_visible() {
mask |= 0b0100;
}
if cell[DIR_W].is_visible() {
mask |= 0b1000;
}
mask
}
pub fn fill(&mut self, area: Rect, cell: Cell) {
let clip = self.area.intersection(area);
if clip.is_empty() {
return;
}
for y in clip.y..clip.bottom() {
for x in clip.x..clip.right() {
if let Some(i) = self.index_of(x, y) {
self.content[i] = cell.clone();
}
}
}
}
pub fn resize(&mut self, new_area: Rect) -> bool {
if new_area == self.area {
return false;
}
let mut next = Self::empty(new_area);
let overlap = self.area.intersection(new_area);
if !overlap.is_empty() {
for y in overlap.y..overlap.bottom() {
for x in overlap.x..overlap.right() {
if let (Some(src_i), Some(dst_i)) = (self.index_of(x, y), next.index_of(x, y)) {
next.content[dst_i] = self.content[src_i].clone();
}
}
}
}
*self = next;
true
}
pub fn merge(&mut self, other: &Buffer) {
let overlap = self.area.intersection(other.area);
if overlap.is_empty() {
return;
}
for y in overlap.y..overlap.bottom() {
for x in overlap.x..overlap.right() {
if let (Some(src_i), Some(dst_i)) = (other.index_of(x, y), self.index_of(x, y)) {
self.content[dst_i] = other.content[src_i].clone();
}
}
}
}
pub fn set_symbol(&mut self, x: u16, y: u16, symbol: &str, style: Style) {
let composed = self.compose_style_for_cell(x, y, style);
if let Some(c) = self.cell_mut(x, y) {
c.set_symbol(symbol);
c.apply_style(composed);
}
}
pub fn set_char(&mut self, x: u16, y: u16, ch: char, style: Style) {
let mut buf = [0u8; 4];
let s: &str = ch.encode_utf8(&mut buf);
self.set_symbol(x, y, s, style);
}
pub fn set_style(&mut self, x: u16, y: u16, style: Style) {
let composed = self.compose_style_for_cell(x, y, style);
if let Some(c) = self.cell_mut(x, y) {
c.apply_style(composed);
}
}
pub fn set_link_range(&mut self, x: u16, y: u16, width: u16, link: Option<&str>) {
for dx in 0..width {
let cx = x.saturating_add(dx);
if let Some(cell) = self.cell_mut(cx, y) {
cell.set_link(link);
}
}
}
pub fn set_string(&mut self, x: u16, y: u16, s: &str, style: Style) -> (u16, u16) {
self.set_stringn(x, y, s, self.area.width, style)
}
pub fn set_stringn(
&mut self,
x: u16,
y: u16,
s: &str,
max_width: u16,
style: Style,
) -> (u16, u16) {
if y < self.area.y || y >= self.area.bottom() {
return (x, y);
}
if x >= self.area.right() {
return (x, y);
}
let buffer_room = self.area.right().saturating_sub(x);
let budget = max_width.min(buffer_room);
if budget == 0 {
return (x, y);
}
let mut cursor_x = x;
let mut cells_written: u16 = 0;
for grapheme in s.graphemes(true) {
let first = grapheme.chars().next().unwrap_or(' ');
if first.is_control() {
continue;
}
let w = UnicodeWidthStr::width(grapheme) as u16;
if w == 0 {
continue;
}
if cells_written + w > budget {
if w == 2 && budget - cells_written == 1 {
let composed = self.compose_style_for_cell(cursor_x, y, style);
if let Some(c) = self.cell_mut(cursor_x, y) {
c.set_symbol(WIDE_CLIP_PLACEHOLDER);
c.apply_style(composed);
}
cursor_x = cursor_x.saturating_add(1);
}
break;
}
let composed = self.compose_style_for_cell(cursor_x, y, style);
if let Some(c) = self.cell_mut(cursor_x, y) {
c.set_symbol(grapheme);
c.apply_style(composed);
}
if w == 2 {
let spacer_x = cursor_x.saturating_add(1);
let spacer_composed = self.compose_style_for_cell(spacer_x, y, style);
if let Some(c) = self.cell_mut(spacer_x, y) {
c.set_spacer();
c.apply_style(spacer_composed);
}
}
cursor_x = cursor_x.saturating_add(w);
cells_written += w;
}
(cursor_x, y)
}
pub fn diff_iter<'a>(
&'a self,
previous: &'a Buffer,
) -> impl Iterator<Item = (u16, u16, &'a Cell)> + 'a {
assert_eq!(
self.area, previous.area,
"Buffer::diff_iter: area mismatch (self {:?} vs previous {:?})",
self.area, previous.area
);
let area = self.area;
self.content
.iter()
.zip(previous.content.iter())
.enumerate()
.filter_map(move |(i, (new, old))| {
if new.is_spacer() {
return None;
}
match new.diff {
CellDiff::Skip => None,
CellDiff::AlwaysUpdate => {
let (x, y) = Self::xy_at(area, i);
Some((x, y, new))
}
CellDiff::Normal => {
if new == old {
None
} else {
let (x, y) = Self::xy_at(area, i);
Some((x, y, new))
}
}
}
})
}
fn xy_at(area: Rect, i: usize) -> (u16, u16) {
let w = area.width as usize;
let dy = (i / w) as u16;
let dx = (i % w) as u16;
(area.x + dx, area.y + dy)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render::{Color, Modifier};
use proptest::prelude::*;
fn ascii_style() -> Style {
Style::new()
.fg(Color::Rgb(255, 0, 0))
.add_modifier(Modifier::BOLD)
}
#[test]
fn empty_has_correct_cell_count() {
let b = Buffer::empty(Rect::new(0, 0, 10, 5));
assert_eq!(b.content.len(), 50);
assert!(b.content.iter().all(|c| *c == Cell::EMPTY));
}
#[test]
fn filled_has_expected_cell() {
let mut c = Cell::new("X");
c.fg = Color::Rgb(0, 0, 255);
let b = Buffer::filled(Rect::new(0, 0, 3, 2), c.clone());
for cell in &b.content {
assert_eq!(*cell, c);
}
}
#[test]
fn index_of_inside_and_outside() {
let b = Buffer::empty(Rect::new(10, 20, 5, 4));
assert_eq!(b.index_of(10, 20), Some(0));
assert_eq!(b.index_of(14, 23), Some(19)); assert_eq!(b.index_of(9, 20), None); assert_eq!(b.index_of(15, 20), None); assert_eq!(b.index_of(10, 24), None); }
#[test]
fn cell_returns_copy_of_initial_empty() {
let b = Buffer::empty(Rect::new(0, 0, 3, 3));
assert_eq!(b.cell(0, 0), Some(&Cell::EMPTY));
assert_eq!(b.cell(99, 99), None);
}
#[test]
fn ascii_set_string() {
let mut b = Buffer::empty(Rect::new(0, 0, 10, 1));
let end = b.set_string(0, 0, "hello", ascii_style());
assert_eq!(end, (5, 0));
assert_eq!(b.cell(0, 0).unwrap().symbol(), "h");
assert_eq!(b.cell(4, 0).unwrap().symbol(), "o");
assert_eq!(b.cell(0, 0).unwrap().fg, Color::Rgb(255, 0, 0));
}
#[test]
fn control_chars_are_skipped_not_written_as_cells() {
let mut b = Buffer::empty(Rect::new(0, 0, 10, 1));
let end = b.set_string(0, 0, "\n \n\t\r AB", ascii_style());
assert_eq!(end, (6, 0));
assert_eq!(b.cell(0, 0).unwrap().symbol(), " ");
assert_eq!(b.cell(1, 0).unwrap().symbol(), " ");
assert_eq!(b.cell(2, 0).unwrap().symbol(), " ");
assert_eq!(b.cell(3, 0).unwrap().symbol(), " ");
assert_eq!(b.cell(4, 0).unwrap().symbol(), "A");
assert_eq!(b.cell(5, 0).unwrap().symbol(), "B");
}
#[test]
fn cjk_advances_by_two_cells_not_three_bytes() {
let mut b = Buffer::empty(Rect::new(0, 0, 10, 1));
let end = b.set_string(0, 0, "中", ascii_style());
assert_eq!(end, (2, 0), "CJK must advance by visible cell width");
assert_eq!(b.cell(0, 0).unwrap().symbol(), "中");
assert!(b.cell(1, 0).unwrap().is_spacer());
}
#[test]
fn emoji_cursor_advance() {
let mut b = Buffer::empty(Rect::new(0, 0, 10, 1));
let end = b.set_string(0, 0, "🦀A", ascii_style());
assert_eq!(end, (3, 0));
assert_eq!(b.cell(0, 0).unwrap().symbol(), "🦀");
assert!(b.cell(1, 0).unwrap().is_spacer());
assert_eq!(b.cell(2, 0).unwrap().symbol(), "A");
}
#[test]
fn zwj_family_is_one_grapheme() {
let mut b = Buffer::empty(Rect::new(0, 0, 10, 1));
let fam = "👨\u{200D}👩\u{200D}👧"; let end = b.set_string(0, 0, fam, ascii_style());
assert_eq!(end, (2, 0), "ZWJ family must be one wide glyph");
assert_eq!(b.cell(0, 0).unwrap().symbol(), fam);
assert!(b.cell(1, 0).unwrap().is_spacer());
}
#[test]
fn combining_mark_folds_into_grapheme() {
let mut b = Buffer::empty(Rect::new(0, 0, 10, 1));
let end = b.set_string(0, 0, "e\u{0301}f", ascii_style());
assert_eq!(end, (2, 0));
assert_eq!(b.cell(0, 0).unwrap().symbol(), "e\u{0301}");
assert_eq!(b.cell(1, 0).unwrap().symbol(), "f");
}
#[test]
fn regional_indicator_flag_is_one_grapheme_width_2() {
let mut b = Buffer::empty(Rect::new(0, 0, 10, 1));
let end = b.set_string(0, 0, "🇺🇸", ascii_style());
assert_eq!(end, (2, 0));
assert_eq!(b.cell(0, 0).unwrap().symbol(), "🇺🇸");
assert!(b.cell(1, 0).unwrap().is_spacer());
}
#[test]
fn mixed_ascii_cjk_emoji() {
let mut b = Buffer::empty(Rect::new(0, 0, 20, 1));
let end = b.set_string(0, 0, "A中🦀B", ascii_style());
assert_eq!(end, (6, 0));
assert_eq!(b.cell(0, 0).unwrap().symbol(), "A");
assert_eq!(b.cell(1, 0).unwrap().symbol(), "中");
assert!(b.cell(2, 0).unwrap().is_spacer());
assert_eq!(b.cell(3, 0).unwrap().symbol(), "🦀");
assert!(b.cell(4, 0).unwrap().is_spacer());
assert_eq!(b.cell(5, 0).unwrap().symbol(), "B");
}
#[test]
fn clip_past_right_edge_stops() {
let mut b = Buffer::empty(Rect::new(0, 0, 5, 1));
let end = b.set_string(0, 0, "0123456789", ascii_style());
assert_eq!(end, (5, 0));
assert_eq!(b.cell(4, 0).unwrap().symbol(), "4");
assert_eq!(b.cell(5, 0), None);
}
#[test]
fn wide_glyph_clipped_at_right_edge_emits_ellipsis() {
let mut b = Buffer::empty(Rect::new(0, 0, 5, 1));
let end = b.set_string(0, 0, "中中中", ascii_style());
assert_eq!(end, (5, 0));
assert_eq!(b.cell(0, 0).unwrap().symbol(), "中");
assert_eq!(b.cell(2, 0).unwrap().symbol(), "中");
assert_eq!(b.cell(4, 0).unwrap().symbol(), "…");
}
#[test]
fn set_stringn_enforces_max_width() {
let mut b = Buffer::empty(Rect::new(0, 0, 20, 1));
let end = b.set_stringn(0, 0, "hello world", 5, ascii_style());
assert_eq!(end, (5, 0));
assert_eq!(b.cell(4, 0).unwrap().symbol(), "o");
assert_eq!(b.cell(5, 0).unwrap().symbol(), " "); }
#[test]
fn row_out_of_range_is_noop() {
let mut b = Buffer::empty(Rect::new(0, 0, 5, 1));
let end = b.set_string(0, 5, "hello", ascii_style());
assert_eq!(end, (0, 5));
assert_eq!(b.cell(0, 0).unwrap().symbol(), " "); }
#[test]
fn x_past_right_edge_is_noop() {
let mut b = Buffer::empty(Rect::new(0, 0, 5, 1));
let end = b.set_string(99, 0, "hello", ascii_style());
assert_eq!(end, (99, 0));
}
#[test]
fn set_style_preserves_symbol() {
let mut b = Buffer::empty(Rect::new(0, 0, 5, 1));
b.set_string(0, 0, "hi", Style::new());
b.set_style(0, 0, Style::new().fg(Color::Rgb(255, 0, 0)));
assert_eq!(b.cell(0, 0).unwrap().symbol(), "h");
assert_eq!(b.cell(0, 0).unwrap().fg, Color::Rgb(255, 0, 0));
}
#[test]
fn set_char_writes_single_codepoint() {
let mut b = Buffer::empty(Rect::new(0, 0, 3, 1));
b.set_char(1, 0, 'X', Style::new().fg(Color::Rgb(0, 128, 0)));
assert_eq!(b.cell(1, 0).unwrap().symbol(), "X");
assert_eq!(b.cell(1, 0).unwrap().fg, Color::Rgb(0, 128, 0));
}
#[test]
fn fill_overwrites_region() {
let mut b = Buffer::empty(Rect::new(0, 0, 5, 3));
b.fill(Rect::new(1, 1, 3, 1), Cell::new("#"));
assert_eq!(b.cell(0, 0).unwrap().symbol(), " ");
assert_eq!(b.cell(1, 1).unwrap().symbol(), "#");
assert_eq!(b.cell(2, 1).unwrap().symbol(), "#");
assert_eq!(b.cell(3, 1).unwrap().symbol(), "#");
assert_eq!(b.cell(4, 1).unwrap().symbol(), " ");
assert_eq!(b.cell(1, 0).unwrap().symbol(), " ");
}
#[test]
fn fill_clips_to_buffer() {
let mut b = Buffer::empty(Rect::new(0, 0, 3, 3));
b.fill(Rect::new(0, 0, 100, 100), Cell::new("#"));
for x in 0..3 {
for y in 0..3 {
assert_eq!(b.cell(x, y).unwrap().symbol(), "#");
}
}
}
#[test]
fn clear_restores_all_empty() {
let mut b = Buffer::empty(Rect::new(0, 0, 3, 3));
b.set_string(0, 0, "hello", Style::new().fg(Color::Rgb(255, 0, 0)));
b.clear();
for c in &b.content {
assert_eq!(*c, Cell::EMPTY);
}
}
#[test]
fn merge_copies_overlapping_cells() {
let mut dst = Buffer::empty(Rect::new(0, 0, 5, 3));
let mut src = Buffer::empty(Rect::new(1, 1, 3, 1));
src.set_string(1, 1, "foo", Style::new());
dst.merge(&src);
assert_eq!(dst.cell(1, 1).unwrap().symbol(), "f");
assert_eq!(dst.cell(2, 1).unwrap().symbol(), "o");
assert_eq!(dst.cell(3, 1).unwrap().symbol(), "o");
assert_eq!(dst.cell(0, 0).unwrap().symbol(), " "); }
#[test]
fn merge_non_overlapping_is_noop() {
let before = Buffer::empty(Rect::new(0, 0, 3, 3));
let mut dst = before.clone();
let src = Buffer::filled(Rect::new(100, 100, 3, 3), Cell::new("X"));
dst.merge(&src);
assert_eq!(dst, before);
}
#[test]
fn resize_preserves_intersection() {
let mut b = Buffer::empty(Rect::new(0, 0, 5, 3));
b.set_string(0, 0, "hello", Style::new().fg(Color::Rgb(255, 0, 0)));
let changed = b.resize(Rect::new(0, 0, 3, 5));
assert!(changed);
assert_eq!(b.cell(0, 0).unwrap().symbol(), "h");
assert_eq!(b.cell(2, 0).unwrap().symbol(), "l");
assert_eq!(b.cell(2, 4).unwrap().symbol(), " "); }
#[test]
fn resize_identity_returns_false() {
let mut b = Buffer::empty(Rect::new(0, 0, 5, 3));
assert!(!b.resize(Rect::new(0, 0, 5, 3)));
}
#[test]
fn diff_empty_on_identical() {
let a = Buffer::empty(Rect::new(0, 0, 10, 5));
let b = Buffer::empty(Rect::new(0, 0, 10, 5));
assert_eq!(a.diff_iter(&b).count(), 0);
}
#[test]
fn diff_yields_changed_cells() {
let a = Buffer::empty(Rect::new(0, 0, 5, 1));
let mut b = a.clone();
b.set_string(2, 0, "ab", Style::new());
let diffs: Vec<_> = b
.diff_iter(&a)
.map(|(x, y, c)| (x, y, c.symbol().to_string()))
.collect();
assert_eq!(diffs, vec![(2, 0, "a".into()), (3, 0, "b".into())]);
}
#[test]
fn diff_skips_spacer_cells() {
let a = Buffer::empty(Rect::new(0, 0, 5, 1));
let mut b = a.clone();
b.set_string(0, 0, "中", Style::new());
let diffs: Vec<_> = b.diff_iter(&a).collect();
assert_eq!(diffs.len(), 1);
assert_eq!(diffs[0].0, 0);
assert_eq!(diffs[0].2.symbol(), "中");
}
#[test]
fn diff_honors_skip_flag() {
let a = Buffer::empty(Rect::new(0, 0, 3, 1));
let mut b = a.clone();
b.set_string(0, 0, "abc", Style::new());
b.cell_mut(1, 0).unwrap().diff = CellDiff::Skip;
let positions: Vec<_> = b.diff_iter(&a).map(|(x, _, _)| x).collect();
assert_eq!(positions, vec![0, 2]);
}
#[test]
fn diff_honors_always_update() {
let a = Buffer::empty(Rect::new(0, 0, 3, 1));
let mut b = a.clone();
b.cell_mut(1, 0).unwrap().diff = CellDiff::AlwaysUpdate;
let positions: Vec<_> = b.diff_iter(&a).map(|(x, _, _)| x).collect();
assert_eq!(positions, vec![1]);
}
proptest! {
#[test]
fn set_string_cursor_matches_unicode_width(s in r"[A-Za-z0-9\u{4e00}-\u{9fff}\u{1f300}-\u{1f5ff} ]{0,20}") {
let mut b = Buffer::empty(Rect::new(0, 0, 100, 1));
let expected = unicode_width::UnicodeWidthStr::width(s.as_str()) as u16;
let (end_x, _) = b.set_string(0, 0, &s, Style::new());
prop_assert_eq!(
end_x,
expected.min(100),
"cursor advance must equal UnicodeWidthStr::width, not s.len() or s.chars().count()"
);
}
#[test]
fn spacer_after_every_wide_glyph(s in r"[\u{4e00}-\u{9fff}A-Za-z]{0,15}") {
let mut b = Buffer::empty(Rect::new(0, 0, 50, 1));
b.set_string(0, 0, &s, Style::new());
for x in 0..49 {
let c = b.cell(x, 0).unwrap();
if c.cell_width() == 2 {
let next = b.cell(x + 1, 0).unwrap();
prop_assert!(next.is_spacer(), "cell at x={} is wide but x+1 is not a spacer", x);
}
}
}
#[test]
fn resize_preserves_overlapping_symbols(
w1 in 1u16..30, h1 in 1u16..10,
w2 in 1u16..30, h2 in 1u16..10
) {
let mut b = Buffer::empty(Rect::new(0, 0, w1, h1));
for y in 0..h1 {
for x in 0..w1 {
let ch = char::from_u32(33 + ((x as u32 + y as u32 * 31) % 94)).unwrap();
b.set_char(x, y, ch, Style::new());
}
}
let snapshot = b.clone();
b.resize(Rect::new(0, 0, w2, h2));
for y in 0..h1.min(h2) {
for x in 0..w1.min(w2) {
prop_assert_eq!(
b.cell(x, y).unwrap().symbol(),
snapshot.cell(x, y).unwrap().symbol()
);
}
}
}
#[test]
fn diff_self_vs_self_is_empty(w in 1u16..20, h in 1u16..5) {
let a = Buffer::empty(Rect::new(0, 0, w, h));
prop_assert_eq!(a.diff_iter(&a).count(), 0);
}
}
}