use std::num::NonZeroU16;
use unicode_segmentation::UnicodeSegmentation;
use crate::geometry::{Position, Rect};
use crate::style::{Color, Modifier, Style};
use crate::width::grapheme_cols;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum CellDiffOption {
#[default]
None,
Skip,
AlwaysUpdate,
ForcedWidth(NonZeroU16),
}
const SYMBOL_INLINE_CAP: usize = 22;
#[derive(Clone, Debug, PartialEq, Eq)]
enum Symbol {
Inline {
len: u8,
bytes: [u8; SYMBOL_INLINE_CAP],
},
Heap(Box<str>),
}
impl Symbol {
const EMPTY: Symbol = Symbol::Inline {
len: 0,
bytes: [0; SYMBOL_INLINE_CAP],
};
fn new(text: &str) -> Self {
let bytes = text.as_bytes();
if bytes.len() <= SYMBOL_INLINE_CAP {
let mut buf = [0u8; SYMBOL_INLINE_CAP];
buf[..bytes.len()].copy_from_slice(bytes);
Symbol::Inline {
len: bytes.len() as u8,
bytes: buf,
}
} else {
Symbol::Heap(text.into())
}
}
fn as_str(&self) -> &str {
match self {
Symbol::Inline { len, bytes } => {
let bytes = &bytes[..*len as usize];
debug_assert!(
std::str::from_utf8(bytes).is_ok(),
"inline symbol bytes must stay valid UTF-8"
);
unsafe { std::str::from_utf8_unchecked(bytes) }
}
Symbol::Heap(text) => text,
}
}
fn is_empty(&self) -> bool {
match self {
Symbol::Inline { len, .. } => *len == 0,
Symbol::Heap(text) => text.is_empty(),
}
}
}
impl Default for Symbol {
fn default() -> Self {
Symbol::EMPTY
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Cell {
symbol: Symbol,
pub fg: Color,
pub bg: Color,
pub underline_color: Color,
pub modifier: Modifier,
pub diff_option: CellDiffOption,
}
impl Cell {
pub fn new(symbol: &str) -> Self {
let mut cell = Self::default();
cell.set_symbol(symbol);
cell
}
pub fn symbol(&self) -> &str {
if self.symbol.is_empty() {
" "
} else {
self.symbol.as_str()
}
}
pub fn raw_symbol(&self) -> &str {
self.symbol.as_str()
}
pub fn set_symbol(&mut self, symbol: &str) -> &mut Self {
self.symbol = Symbol::new(symbol);
self
}
pub fn set_char(&mut self, ch: char) -> &mut Self {
let mut buf = [0u8; 4];
self.set_symbol(ch.encode_utf8(&mut buf));
self
}
pub const fn set_fg(&mut self, color: Color) -> &mut Self {
self.fg = color;
self
}
pub const fn set_bg(&mut self, color: Color) -> &mut Self {
self.bg = color;
self
}
pub fn set_style<S: Into<Style>>(&mut self, style: S) -> &mut Self {
let style = style.into();
if let Some(fg) = style.fg {
self.fg = fg;
}
if let Some(bg) = style.bg {
self.bg = bg;
}
if let Some(underline) = style.underline_color {
self.underline_color = underline;
}
self.modifier = self
.modifier
.union(style.add_modifier)
.difference(style.sub_modifier);
self
}
pub const fn style(&self) -> Style {
Style {
fg: Some(self.fg),
bg: Some(self.bg),
underline_color: Some(self.underline_color),
add_modifier: self.modifier,
sub_modifier: Modifier::empty(),
}
}
pub const fn set_diff_option(&mut self, diff_option: CellDiffOption) -> &mut Self {
self.diff_option = diff_option;
self
}
pub fn reset(&mut self) {
self.symbol = Symbol::new(" ");
self.fg = Color::Reset;
self.bg = Color::Reset;
self.underline_color = Color::Reset;
self.modifier = Modifier::empty();
self.diff_option = CellDiffOption::None;
}
pub fn cell_width(&self) -> u16 {
match self.diff_option {
CellDiffOption::ForcedWidth(width) => width.get(),
_ => grapheme_cols(self.symbol()),
}
}
const fn is_skipped(&self) -> bool {
matches!(self.diff_option, CellDiffOption::Skip)
}
}
impl Default for Cell {
fn default() -> Self {
Self {
symbol: Symbol::new(" "),
fg: Color::Reset,
bg: Color::Reset,
underline_color: Color::Reset,
modifier: Modifier::empty(),
diff_option: CellDiffOption::None,
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Buffer {
pub area: Rect,
pub content: Vec<Cell>,
}
impl Buffer {
pub fn empty(area: Rect) -> Self {
Self::filled(area, Cell::default())
}
pub fn filled(area: Rect, cell: Cell) -> Self {
let size = area.area() as usize;
Self {
area,
content: vec![cell; size],
}
}
pub fn with_lines<'a, I>(lines: I) -> Self
where
I: IntoIterator,
I::Item: Into<crate::text::Line<'a>>,
{
let lines: Vec<crate::text::Line<'a>> = lines.into_iter().map(Into::into).collect();
let height = lines.len() as u16;
let width = lines
.iter()
.map(crate::text::Line::width)
.max()
.unwrap_or(0);
let mut buffer = Self::empty(Rect::new(0, 0, width, height));
for (y, line) in lines.iter().enumerate() {
buffer.set_line(0, y as u16, line, width);
}
buffer
}
fn index_of(&self, x: u16, y: u16) -> Option<usize> {
if !self.area.contains(Position { x, y }) {
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)
}
fn pos_of(&self, index: usize) -> (u16, u16) {
let width = self.area.width.max(1) as usize;
let x = index % width + self.area.x as usize;
let y = index / width + self.area.y as usize;
(x as u16, y as u16)
}
pub fn cell<P: Into<Position>>(&self, position: P) -> Option<&Cell> {
let position = position.into();
self.index_of(position.x, position.y)
.map(|index| &self.content[index])
}
pub fn cell_mut<P: Into<Position>>(&mut self, position: P) -> Option<&mut Cell> {
let position = position.into();
self.index_of(position.x, position.y)
.map(|index| &mut self.content[index])
}
pub fn content(&self) -> &[Cell] {
&self.content
}
pub fn reset(&mut self) {
for cell in &mut self.content {
cell.reset();
}
}
pub fn resize(&mut self, area: Rect) {
let size = area.area() as usize;
if self.content.len() > size {
self.content.truncate(size);
} else {
self.content.resize(size, Cell::default());
}
self.area = area;
}
pub fn merge(&mut self, other: &Self) {
let merged = self.area.union(other.area);
let mut content = vec![Cell::default(); merged.area() as usize];
let width = merged.width as usize;
let mut place = |buffer: &Self| {
for (index, cell) in buffer.content.iter().enumerate() {
let (x, y) = buffer.pos_of(index);
if !merged.contains(Position { x, y }) {
continue;
}
let dx = (x - merged.x) as usize;
let dy = (y - merged.y) as usize;
content[dy * width + dx] = cell.clone();
}
};
place(self);
place(other);
self.area = merged;
self.content = content;
}
pub fn set_style<S: Into<Style>>(&mut self, area: Rect, style: S) {
let style = style.into();
let target = area.intersection(self.area);
for y in target.y..target.bottom() {
for x in target.x..target.right() {
if let Some(cell) = self.cell_mut((x, y)) {
cell.set_style(style);
}
}
}
}
pub fn set_string<S: Into<Style>>(&mut self, x: u16, y: u16, string: &str, style: S) -> u16 {
self.set_stringn(x, y, string, usize::MAX, style).0
}
pub fn set_stringn<S: Into<Style>>(
&mut self,
x: u16,
y: u16,
string: &str,
max_width: usize,
style: S,
) -> (u16, u16) {
let style = style.into();
let mut column = x;
let mut used = 0u16;
let limit = max_width.min(u16::MAX as usize) as u16;
for grapheme in string.graphemes(true) {
let width = grapheme_cols(grapheme);
if width == 0 {
continue;
}
if used.saturating_add(width) > limit || column >= self.area.right() {
break;
}
let Some(index) = self.index_of(column, y) else {
break;
};
self.content[index].set_symbol(grapheme);
self.content[index].set_style(style);
for offset in 1..width {
if let Some(trailing) = self.index_of(column + offset, y) {
self.content[trailing].set_symbol("");
self.content[trailing].set_style(style);
}
}
column = column.saturating_add(width);
used = used.saturating_add(width);
}
(column, used)
}
pub fn set_span(
&mut self,
x: u16,
y: u16,
span: &crate::text::Span<'_>,
max_width: u16,
) -> (u16, u16) {
self.set_stringn(x, y, span.content.as_ref(), max_width as usize, span.style)
}
pub fn set_line(
&mut self,
x: u16,
y: u16,
line: &crate::text::Line<'_>,
max_width: u16,
) -> (u16, u16) {
let mut column = x;
let mut used = 0u16;
for span in &line.spans {
let remaining = max_width.saturating_sub(used);
if remaining == 0 {
break;
}
let (next, consumed) = self.set_stringn(
column,
y,
span.content.as_ref(),
remaining as usize,
line.style.patch(span.style),
);
column = next;
used = used.saturating_add(consumed);
}
(column, used)
}
pub fn diff<'next>(&self, next: &'next Self) -> Vec<(u16, u16, &'next Cell)> {
assert!(
self.area.x == next.area.x
&& self.area.y == next.area.y
&& self.area.width == next.area.width,
"diffed buffers must share an origin and width: prev={:?}, next={:?}",
self.area,
next.area,
);
let len = self.content.len().min(next.content.len());
let mut updates = Vec::new();
let mut index = 0usize;
while index < len {
let current = &next.content[index];
let previous = &self.content[index];
let position = index;
index += 1;
if current.is_skipped() {
continue;
}
match current.diff_option {
CellDiffOption::Skip => {}
CellDiffOption::ForcedWidth(width) => {
index = index.saturating_add(width.get().saturating_sub(1) as usize);
if current != previous {
let (x, y) = next.pos_of(position);
updates.push((x, y, current));
}
}
CellDiffOption::None | CellDiffOption::AlwaysUpdate => {
let width = current.cell_width() as usize;
let unchanged =
matches!(current.diff_option, CellDiffOption::None) && current == previous;
if unchanged {
index += width.saturating_sub(1);
continue;
}
let previous_width = previous.cell_width() as usize;
let (x, y) = next.pos_of(position);
updates.push((x, y, current));
if width > 1 {
index += width - 1;
} else if previous_width > width
&& (previous.bg != Color::Reset
|| previous.modifier.intersection(VISIBLE_ON_BLANK)
!= Modifier::empty())
{
let end = (position + previous_width).min(len);
for trailing in (position + 1)..end {
if !next.content[trailing].is_skipped() {
let (tx, ty) = next.pos_of(trailing);
updates.push((tx, ty, &next.content[trailing]));
}
}
index = end;
}
}
}
}
updates
}
}
const VISIBLE_ON_BLANK: Modifier = Modifier::REVERSED
.union(Modifier::UNDERLINED)
.union(Modifier::SLOW_BLINK)
.union(Modifier::RAPID_BLINK)
.union(Modifier::CROSSED_OUT);
impl<P: Into<Position>> std::ops::Index<P> for Buffer {
type Output = Cell;
#[track_caller]
fn index(&self, position: P) -> &Self::Output {
let position = position.into();
self.cell(position)
.expect("position out of the buffer's area")
}
}
impl<P: Into<Position>> std::ops::IndexMut<P> for Buffer {
#[track_caller]
fn index_mut(&mut self, position: P) -> &mut Self::Output {
let area = self.area;
self.cell_mut(position).unwrap_or_else(|| {
panic!("position out of the buffer's area {area:?}");
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn row(buffer: &Buffer, y: u16) -> String {
(buffer.area.x..buffer.area.right())
.map(|x| buffer[(x, y)].symbol().to_string())
.collect()
}
#[test]
fn empty_cells_render_as_spaces() {
let buffer = Buffer::empty(Rect::new(0, 0, 3, 1));
assert_eq!(row(&buffer, 0), " ");
}
#[test]
fn set_string_writes_and_returns_the_next_column() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 6, 1));
let next = buffer.set_string(1, 0, "abc", Style::default());
assert_eq!(next, 4);
assert_eq!(row(&buffer, 0), " abc ");
}
#[test]
fn a_wide_grapheme_blanks_its_trailing_cell() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 1));
buffer.set_string(0, 0, "日本", Style::default());
assert_eq!(buffer[(0, 0)].symbol(), "日");
assert_eq!(buffer[(1, 0)].raw_symbol(), "");
assert_eq!(buffer[(2, 0)].symbol(), "本");
assert_eq!(buffer[(3, 0)].raw_symbol(), "");
}
#[test]
fn a_wide_grapheme_that_does_not_fit_is_dropped() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 3, 1));
let (next, used) = buffer.set_stringn(0, 0, "a日", 2, Style::default());
assert_eq!(used, 1, "only the narrow grapheme fits in two columns");
assert_eq!(next, 1);
assert_eq!(buffer[(1, 0)].symbol(), " ");
}
#[test]
fn set_string_clips_at_the_area_edge() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 3, 1));
buffer.set_string(1, 0, "abcdef", Style::default());
assert_eq!(row(&buffer, 0), " ab");
}
#[test]
fn zero_width_clusters_consume_no_columns() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 1));
let (next, used) = buffer.set_stringn(0, 0, "a\u{0301}b", 4, Style::default());
assert_eq!(used, 2, "the combining acute joins its base grapheme");
assert_eq!(next, 2);
}
#[test]
fn a_symbol_too_long_to_inline_spills_to_the_heap() {
let long = "\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}\u{200D}\u{1F466}";
assert!(
long.len() > SYMBOL_INLINE_CAP,
"corpus must exceed the inline cap"
);
let mut cell = Cell::default();
cell.set_symbol(long);
assert_eq!(cell.symbol(), long);
assert!(matches!(cell.symbol, Symbol::Heap(_)));
}
#[test]
fn a_short_symbol_stays_inline() {
let mut cell = Cell::default();
cell.set_symbol("\u{1F600}");
assert_eq!(cell.symbol(), "\u{1F600}");
assert!(matches!(cell.symbol, Symbol::Inline { .. }));
}
#[test]
fn overwriting_a_heap_symbol_returns_to_inline() {
let long = "x".repeat(SYMBOL_INLINE_CAP + 8);
let mut cell = Cell::default();
cell.set_symbol(&long);
cell.set_symbol("a");
assert_eq!(cell.symbol(), "a");
assert!(matches!(cell.symbol, Symbol::Inline { .. }));
}
#[test]
fn indexing_uses_absolute_coordinates() {
let mut buffer = Buffer::empty(Rect::new(5, 5, 2, 2));
buffer[(5, 5)].set_char('x');
assert_eq!(buffer[(5, 5)].symbol(), "x");
assert!(buffer.cell((0, 0)).is_none(), "outside the area");
}
#[test]
fn an_unchanged_frame_diffs_to_nothing() {
let buffer = Buffer::empty(Rect::new(0, 0, 4, 2));
assert!(buffer.diff(&buffer.clone()).is_empty());
}
#[test]
fn diff_yields_only_changed_cells() {
let previous = Buffer::empty(Rect::new(0, 0, 4, 1));
let mut next = previous.clone();
next[(2, 0)].set_char('x');
let updates = previous.diff(&next);
assert_eq!(updates.len(), 1);
assert_eq!((updates[0].0, updates[0].1), (2, 0));
}
#[test]
fn a_skipped_cell_is_never_emitted() {
let previous = Buffer::empty(Rect::new(0, 0, 3, 1));
let mut next = previous.clone();
next[(1, 0)].set_char('x');
next[(1, 0)].set_diff_option(CellDiffOption::Skip);
assert!(previous.diff(&next).is_empty());
}
#[test]
fn always_update_emits_an_unchanged_cell() {
let previous = Buffer::empty(Rect::new(0, 0, 2, 1));
let mut next = previous.clone();
next[(0, 0)].set_diff_option(CellDiffOption::AlwaysUpdate);
let updates = previous.diff(&next);
assert_eq!(updates.len(), 1);
}
#[test]
fn forced_width_bills_the_declared_columns() {
let previous = Buffer::empty(Rect::new(0, 0, 3, 1));
let mut next = previous.clone();
next[(0, 0)].set_symbol("\u{1b}]8;;http://x\u{1b}\\a");
next[(0, 0)].set_diff_option(CellDiffOption::ForcedWidth(NonZeroU16::new(1).unwrap()));
let updates = previous.diff(&next);
assert_eq!(updates.len(), 1);
assert_eq!(next[(0, 0)].cell_width(), 1);
}
#[test]
fn a_narrowed_wide_cell_refreshes_its_vacated_column() {
let mut previous = Buffer::empty(Rect::new(0, 0, 3, 1));
previous[(0, 0)].set_symbol("日");
previous[(0, 0)].set_bg(Color::Blue);
let mut next = Buffer::empty(Rect::new(0, 0, 3, 1));
next[(0, 0)].set_char('a');
let updates = previous.diff(&next);
let columns: Vec<u16> = updates.iter().map(|(x, _, _)| *x).collect();
assert!(columns.contains(&0));
assert!(
columns.contains(&1),
"the freed column must be refreshed, got {columns:?}"
);
}
#[test]
fn a_narrowed_unstyled_wide_cell_needs_no_extra_write() {
let mut previous = Buffer::empty(Rect::new(0, 0, 3, 1));
previous[(0, 0)].set_symbol("日");
let mut next = Buffer::empty(Rect::new(0, 0, 3, 1));
next[(0, 0)].set_char('a');
let updates = previous.diff(&next);
assert_eq!(updates.len(), 1);
assert_eq!(updates[0].0, 0);
}
#[test]
fn set_style_repaints_an_area_without_touching_symbols() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 2));
buffer.set_string(0, 0, "ab", Style::default());
buffer.set_style(Rect::new(0, 0, 2, 1), Style::new().bg(Color::Blue));
assert_eq!(buffer[(0, 0)].symbol(), "a");
assert_eq!(buffer[(0, 0)].bg, Color::Blue);
assert_eq!(buffer[(2, 0)].bg, Color::Reset, "outside the area");
}
#[test]
fn set_style_clips_an_oversized_area() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 2, 1));
buffer.set_style(Rect::new(0, 0, 99, 99), Style::new().bg(Color::Blue));
assert_eq!(buffer[(1, 0)].bg, Color::Blue);
}
#[test]
fn resize_changes_the_area_and_cell_count() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 2, 1));
buffer.resize(Rect::new(0, 0, 3, 2));
assert_eq!(buffer.area, Rect::new(0, 0, 3, 2));
assert_eq!(buffer.content.len(), 6);
}
#[test]
fn resizing_to_the_same_area_preserves_every_cell() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 3, 2));
buffer.set_string(0, 0, "abc", Style::default());
buffer.resize(Rect::new(0, 0, 3, 2));
assert_eq!(buffer[(0, 0)].symbol(), "a");
assert_eq!(buffer[(2, 0)].symbol(), "c");
}
#[test]
fn growing_keeps_existing_cells_and_blanks_the_rest() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 2, 1));
buffer.set_string(0, 0, "ab", Style::default());
buffer.resize(Rect::new(0, 0, 2, 2));
assert_eq!(buffer[(0, 0)].symbol(), "a");
assert_eq!(buffer[(1, 0)].symbol(), "b");
assert_eq!(buffer[(0, 1)].symbol(), " ");
}
#[test]
fn shrinking_truncates_rather_than_clearing() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 2, 2));
buffer.set_string(0, 0, "ab", Style::default());
buffer.resize(Rect::new(0, 0, 2, 1));
assert_eq!(buffer.content.len(), 2);
assert_eq!(buffer[(0, 0)].symbol(), "a");
}
#[test]
fn merge_grows_the_area_and_lets_the_other_win() {
let mut base = Buffer::empty(Rect::new(0, 0, 2, 1));
base[(0, 0)].set_char('a');
let mut overlay = Buffer::empty(Rect::new(1, 0, 2, 1));
overlay[(1, 0)].set_char('b');
base.merge(&overlay);
assert_eq!(base.area, Rect::new(0, 0, 3, 1));
assert_eq!(base[(0, 0)].symbol(), "a");
assert_eq!(base[(1, 0)].symbol(), "b");
}
#[test]
fn set_style_leaves_unset_fields_alone() {
let mut cell = Cell::new("x");
cell.set_bg(Color::Blue);
cell.set_style(Style::new().fg(Color::Red));
assert_eq!(cell.fg, Color::Red);
assert_eq!(cell.bg, Color::Blue);
}
#[test]
fn with_lines_builds_a_grid_sized_to_the_widest_row() {
let buffer = Buffer::with_lines(["ab", "cdef"]);
assert_eq!(buffer.area, Rect::new(0, 0, 4, 2));
assert_eq!(row(&buffer, 0), "ab ");
assert_eq!(row(&buffer, 1), "cdef");
}
}