use crate::{ColorPair, Result};
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct Cell {
pub(crate) ch: char,
pub(crate) colors: Option<ColorPair>,
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct BufferChange {
pub(crate) y: u16,
pub(crate) x: u16,
pub(crate) start_idx: usize,
pub(crate) len: usize,
pub(crate) colors: Option<ColorPair>,
}
#[derive(Clone, Copy, Debug)]
struct DirtyRange {
min_x: u16,
max_x: u16,
}
impl Cell {
pub fn empty() -> Self {
Self {
ch: ' ',
colors: None,
}
}
}
pub struct Buffer {
width: u16,
height: u16,
current: Vec<Cell>, previous: Vec<Cell>, dirty_rows: Vec<Option<DirtyRange>>,
changes: Vec<BufferChange>,
}
impl Buffer {
pub(crate) fn new(width: u16, height: u16) -> Self {
let size = width as usize * height as usize;
let current = vec![Cell::empty(); size];
let previous = vec![Cell::empty(); size];
Self {
width,
height,
current,
previous,
dirty_rows: vec![None; height as usize],
changes: Vec::new(),
}
}
fn coords_to_index(&self, x: u16, y: u16) -> usize {
(y as usize * self.width as usize) + x as usize
}
fn mark_dirty_span(&mut self, y: u16, min_x: u16, max_x: u16) {
let Some(row) = self.dirty_rows.get_mut(y as usize) else {
return;
};
match row {
Some(range) => {
range.min_x = range.min_x.min(min_x);
range.max_x = range.max_x.max(max_x);
}
None => {
*row = Some(DirtyRange { min_x, max_x });
}
}
}
#[allow(dead_code)]
pub(crate) fn write_char(
&mut self,
y: u16,
x: u16,
ch: char,
colors: Option<ColorPair>,
) -> Result<()> {
if x >= self.width || y >= self.height {
return Err(crate::Error::BufferSizeError {
x,
y,
width: self.width,
height: self.height,
});
}
let idx = self.coords_to_index(x, y);
let cell = &mut self.current[idx];
if cell.ch != ch || cell.colors != colors {
cell.ch = ch;
cell.colors = colors;
self.mark_dirty_span(y, x, x);
}
Ok(())
}
pub(crate) fn write_str(
&mut self,
y: u16,
x: u16,
s: &str,
colors: Option<ColorPair>,
) -> Result<()> {
if x >= self.width || y >= self.height {
return Err(crate::Error::BufferSizeError {
x,
y,
width: self.width,
height: self.height,
});
}
let row_start = self.coords_to_index(0, y);
let mut x_pos = x;
let mut min_changed: Option<u16> = None;
let mut max_changed: u16 = x;
for ch in s.chars() {
if x_pos >= self.width {
break; }
let idx = row_start + x_pos as usize;
let cell = &mut self.current[idx];
if cell.ch != ch || cell.colors != colors {
cell.ch = ch;
cell.colors = colors;
if min_changed.is_none() {
min_changed = Some(x_pos);
}
max_changed = x_pos;
}
x_pos = x_pos.saturating_add(1);
}
if let Some(min_x) = min_changed {
self.mark_dirty_span(y, min_x, max_changed);
}
Ok(())
}
pub(crate) fn clear(&mut self) {
for y in 0..self.height {
let row_start = self.coords_to_index(0, y);
let mut min_changed = None;
let mut max_changed = 0;
for x in 0..self.width {
let cell = &mut self.current[row_start + x as usize];
if cell.ch != ' ' || cell.colors.is_some() {
*cell = Cell::empty();
min_changed.get_or_insert(x);
max_changed = x;
}
}
if let Some(min_x) = min_changed {
self.mark_dirty_span(y, min_x, max_changed);
}
}
}
pub(crate) fn clear_line(&mut self, y: u16) -> Result<()> {
if y >= self.height {
return Err(crate::Error::LineOutOfBoundsError {
y,
height: self.height,
});
}
let start_idx = self.coords_to_index(0, y);
let mut min_changed = None;
let mut max_changed = 0;
for x in 0..self.width {
let cell = &mut self.current[start_idx + x as usize];
if cell.ch != ' ' || cell.colors.is_some() {
*cell = Cell::empty();
min_changed.get_or_insert(x);
max_changed = x;
}
}
if let Some(min_x) = min_changed {
self.mark_dirty_span(y, min_x, max_changed);
}
Ok(())
}
pub(crate) fn clear_area(&mut self, start_y: u16, start_x: u16, end_y: u16, end_x: u16) {
for y in start_y..=end_y {
let row_start = self.coords_to_index(0, y);
let mut min_changed = None;
let mut max_changed = 0;
for x in start_x..=end_x {
let cell = &mut self.current[row_start + x as usize];
if cell.ch != ' ' || cell.colors.is_some() {
*cell = Cell::empty();
min_changed.get_or_insert(x);
max_changed = x;
}
}
if let Some(min_x) = min_changed {
self.mark_dirty_span(y, min_x, max_changed);
}
}
}
pub(crate) fn process_changes(&mut self) -> usize {
self.changes.clear();
for y in 0..self.height {
let Some(range) = self.dirty_rows[y as usize] else {
continue;
};
let row_start = self.coords_to_index(0, y);
let mut x = range.min_x as usize;
let max_x = range.max_x as usize;
while x <= max_x {
let idx = row_start + x;
let current = &self.current[idx];
let previous = &self.previous[idx];
if current == previous {
x += 1;
continue;
}
let mut run_length = 1usize;
while x + run_length <= max_x {
let next_idx = idx + run_length;
let next_cell = &self.current[next_idx];
let next_prev = &self.previous[next_idx];
if next_cell.colors != current.colors || next_cell == next_prev {
break;
}
run_length += 1;
}
self.changes.push(BufferChange {
y,
x: x as u16,
start_idx: idx,
len: run_length,
colors: current.colors,
});
x += run_length;
}
}
self.changes.len()
}
pub(crate) fn commit_changes(&mut self) {
for (y, row) in self.dirty_rows.iter_mut().enumerate() {
if let Some(range) = row.take() {
let row_start = y * self.width as usize;
let start_idx = row_start + range.min_x as usize;
let end_idx = row_start + range.max_x as usize + 1;
self.previous[start_idx..end_idx]
.copy_from_slice(&self.current[start_idx..end_idx]);
}
}
self.changes.clear();
}
pub(crate) fn change(&self, index: usize) -> BufferChange {
self.changes[index]
}
pub(crate) fn change_text(&self, change: BufferChange, output: &mut String) {
output.clear();
output.reserve(change.len);
output.extend(
self.current[change.start_idx..change.start_idx + change.len]
.iter()
.map(|cell| cell.ch),
);
}
#[allow(dead_code)]
pub(crate) fn get_stats(&self) -> BufferStats {
let dirty_rows = self.dirty_rows.iter().filter(|row| row.is_some()).count();
let dirty_cols = self
.dirty_rows
.iter()
.filter_map(|row| row.map(|range| (range.max_x - range.min_x + 1) as usize))
.sum();
let modified_cells = self
.dirty_rows
.iter()
.enumerate()
.map(|(y, row)| {
let Some(range) = row else {
return 0;
};
let row_start = y * self.width as usize;
let start_idx = row_start + range.min_x as usize;
let end_idx = row_start + range.max_x as usize + 1;
self.current[start_idx..end_idx]
.iter()
.zip(&self.previous[start_idx..end_idx])
.filter(|(current, previous)| current != previous)
.count()
})
.sum();
BufferStats {
width: self.width,
height: self.height,
dirty_rows,
dirty_cols,
modified_cells,
}
}
}
#[derive(Debug)]
#[allow(dead_code)]
pub struct BufferStats {
pub width: u16,
pub height: u16,
pub dirty_rows: usize,
pub dirty_cols: usize,
pub modified_cells: usize,
}
#[cfg(test)]
mod tests {
use super::Buffer;
#[test]
fn clear_area_marks_only_changed_cells() {
let mut buffer = Buffer::new(5, 2);
buffer.write_str(0, 0, "abcde", None).unwrap();
buffer.commit_changes();
buffer.clear_area(0, 1, 0, 3);
let stats = buffer.get_stats();
assert_eq!(stats.dirty_rows, 1);
assert_eq!(stats.dirty_cols, 3);
assert_eq!(stats.modified_cells, 3);
assert_eq!(buffer.process_changes(), 1);
let mut output = String::new();
buffer.change_text(buffer.change(0), &mut output);
assert_eq!(output, " ");
}
#[test]
fn clear_area_ignores_cells_that_are_already_clear() {
let mut buffer = Buffer::new(5, 2);
buffer.clear_area(0, 1, 1, 3);
let stats = buffer.get_stats();
assert_eq!(stats.dirty_rows, 0);
assert_eq!(stats.dirty_cols, 0);
assert_eq!(stats.modified_cells, 0);
assert_eq!(buffer.process_changes(), 0);
}
#[test]
fn stats_count_dirty_cells_that_still_differ_from_previous_frame() {
let mut buffer = Buffer::new(5, 2);
buffer.write_str(0, 1, "ab", None).unwrap();
let stats = buffer.get_stats();
assert_eq!(stats.dirty_rows, 1);
assert_eq!(stats.dirty_cols, 2);
assert_eq!(stats.modified_cells, 2);
}
#[test]
fn reverted_dirty_cells_produce_no_terminal_changes() {
let mut buffer = Buffer::new(5, 2);
buffer.write_str(0, 1, "a", None).unwrap();
buffer.write_str(0, 1, " ", None).unwrap();
let stats = buffer.get_stats();
assert_eq!(stats.dirty_rows, 1);
assert_eq!(stats.dirty_cols, 1);
assert_eq!(stats.modified_cells, 0);
assert_eq!(buffer.process_changes(), 0);
buffer.commit_changes();
let stats = buffer.get_stats();
assert_eq!(stats.dirty_rows, 0);
assert_eq!(stats.dirty_cols, 0);
assert_eq!(stats.modified_cells, 0);
}
#[test]
fn change_text_reuses_output_storage() {
let mut buffer = Buffer::new(5, 1);
let mut output = String::from("stale text");
buffer.write_str(0, 0, "hey", None).unwrap();
assert_eq!(buffer.process_changes(), 1);
buffer.change_text(buffer.change(0), &mut output);
assert_eq!(output, "hey");
}
}