use crate::buffer::OptimizedBuffer;
use crate::error::Error;
#[derive(Clone, Copy, Debug)]
pub struct DirtyRegion {
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
}
impl DirtyRegion {
#[must_use]
pub fn new(x: u32, y: u32, width: u32, height: u32) -> Self {
Self {
x,
y,
width,
height,
}
}
#[must_use]
pub fn cell(x: u32, y: u32) -> Self {
Self::new(x, y, 1, 1)
}
#[must_use]
pub fn merge(&self, other: &Self) -> Self {
let x1 = self.x.min(other.x);
let y1 = self.y.min(other.y);
let x2 = (self.x + self.width).max(other.x + other.width);
let y2 = (self.y + self.height).max(other.y + other.height);
Self::new(x1, y1, x2 - x1, y2 - y1)
}
}
pub struct BufferDiff {
pub changed_cells: Vec<(u32, u32)>,
pub dirty_regions: Vec<DirtyRegion>,
pub change_count: usize,
}
impl BufferDiff {
#[must_use]
pub fn with_capacity(expected_changes: usize) -> Self {
Self {
changed_cells: Vec::with_capacity(expected_changes),
dirty_regions: Vec::with_capacity(expected_changes / 4),
change_count: 0,
}
}
pub fn clear(&mut self) {
self.changed_cells.clear();
self.dirty_regions.clear();
self.change_count = 0;
}
}
impl BufferDiff {
#[must_use]
pub fn compute(old: &OptimizedBuffer, new: &OptimizedBuffer) -> Self {
Self::try_compute(old, new).expect("buffer size mismatch in diff")
}
pub fn try_compute(old: &OptimizedBuffer, new: &OptimizedBuffer) -> Result<Self, Error> {
let (width, height) = old.size();
let total_cells = (width as usize).saturating_mul(height as usize);
let reserve = (total_cells / 8).max(32).min(total_cells);
let mut diff = Self::with_capacity(reserve);
diff.try_compute_into(old, new)?;
Ok(diff)
}
pub fn try_compute_into(
&mut self,
old: &OptimizedBuffer,
new: &OptimizedBuffer,
) -> Result<(), Error> {
if old.size() != new.size() {
return Err(Error::BufferSizeMismatch {
old_size: old.size(),
new_size: new.size(),
});
}
self.changed_cells.clear();
self.dirty_regions.clear();
let (width, height) = old.size();
let old_cells = old.cells();
let new_cells = new.cells();
for y in 0..height {
let row_offset = (y * width) as usize;
for x in 0..width {
let idx = row_offset + x as usize;
if !old_cells[idx].bits_eq(&new_cells[idx]) {
self.changed_cells.push((x, y));
}
}
}
self.change_count = self.changed_cells.len();
Self::merge_into_regions_reuse(&self.changed_cells, width, &mut self.dirty_regions);
Ok(())
}
pub fn compute_into(&mut self, old: &OptimizedBuffer, new: &OptimizedBuffer) {
self.try_compute_into(old, new)
.expect("buffer size mismatch in diff");
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.changed_cells.is_empty()
}
fn merge_into_regions(cells: &[(u32, u32)], width: u32) -> Vec<DirtyRegion> {
let mut regions = Vec::new();
Self::merge_into_regions_reuse(cells, width, &mut regions);
regions
}
fn merge_into_regions_reuse(cells: &[(u32, u32)], _width: u32, regions: &mut Vec<DirtyRegion>) {
regions.clear();
if cells.is_empty() {
return;
}
let mut current_row: Option<u32> = None;
let mut row_start: u32 = 0;
let mut row_end: u32 = 0;
for &(x, y) in cells {
if current_row == Some(y) {
if x > row_end + 1 {
if let Some(row) = current_row {
regions.push(DirtyRegion::new(row_start, row, row_end - row_start + 1, 1));
}
row_start = x;
row_end = x;
} else {
row_end = x;
}
} else {
if let Some(row) = current_row {
regions.push(DirtyRegion::new(row_start, row, row_end - row_start + 1, 1));
}
current_row = Some(y);
row_start = x;
row_end = x;
}
}
if let Some(row) = current_row {
regions.push(DirtyRegion::new(row_start, row, row_end - row_start + 1, 1));
}
}
#[must_use]
pub fn should_full_redraw(&self, total_cells: usize) -> bool {
self.change_count > total_cells / 2
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cell::Cell;
use crate::color::Rgba;
use crate::style::Style;
#[test]
fn test_dirty_region_new() {
let region = DirtyRegion::new(5, 10, 20, 30);
assert_eq!(region.x, 5);
assert_eq!(region.y, 10);
assert_eq!(region.width, 20);
assert_eq!(region.height, 30);
}
#[test]
fn test_dirty_region_cell() {
let region = DirtyRegion::cell(7, 12);
assert_eq!(region.x, 7);
assert_eq!(region.y, 12);
assert_eq!(region.width, 1);
assert_eq!(region.height, 1);
}
#[test]
fn test_dirty_region_merge() {
let a = DirtyRegion::new(0, 0, 5, 5);
let b = DirtyRegion::new(3, 3, 5, 5);
let merged = a.merge(&b);
assert_eq!(merged.x, 0);
assert_eq!(merged.y, 0);
assert_eq!(merged.width, 8);
assert_eq!(merged.height, 8);
}
#[test]
fn test_dirty_region_merge_non_overlapping() {
let a = DirtyRegion::new(0, 0, 5, 5);
let b = DirtyRegion::new(10, 10, 5, 5);
let merged = a.merge(&b);
assert_eq!(merged.x, 0);
assert_eq!(merged.y, 0);
assert_eq!(merged.width, 15);
assert_eq!(merged.height, 15);
}
#[test]
fn test_dirty_region_merge_contained() {
let outer = DirtyRegion::new(0, 0, 20, 20);
let inner = DirtyRegion::new(5, 5, 5, 5);
let merged = outer.merge(&inner);
assert_eq!(merged.x, 0);
assert_eq!(merged.y, 0);
assert_eq!(merged.width, 20);
assert_eq!(merged.height, 20);
}
#[test]
fn test_buffer_diff_empty() {
let a = OptimizedBuffer::new(10, 10);
let b = OptimizedBuffer::new(10, 10);
let diff = BufferDiff::compute(&a, &b);
assert!(diff.is_empty());
assert_eq!(diff.change_count, 0);
assert!(diff.dirty_regions.is_empty());
}
#[test]
fn test_buffer_diff_single_cell_change() {
let a = OptimizedBuffer::new(10, 10);
let mut b = OptimizedBuffer::new(10, 10);
b.set(5, 5, Cell::clear(Rgba::RED));
let diff = BufferDiff::compute(&a, &b);
assert!(!diff.is_empty());
assert_eq!(diff.change_count, 1);
assert!(diff.changed_cells.contains(&(5, 5)));
assert_eq!(diff.dirty_regions.len(), 1);
}
#[test]
fn test_buffer_diff_multiple_cells() {
let a = OptimizedBuffer::new(10, 10);
let mut b = OptimizedBuffer::new(10, 10);
b.set(0, 0, Cell::clear(Rgba::RED));
b.set(5, 5, Cell::clear(Rgba::GREEN));
b.set(9, 9, Cell::clear(Rgba::BLUE));
let diff = BufferDiff::compute(&a, &b);
assert_eq!(diff.change_count, 3);
assert!(diff.changed_cells.contains(&(0, 0)));
assert!(diff.changed_cells.contains(&(5, 5)));
assert!(diff.changed_cells.contains(&(9, 9)));
}
#[test]
fn test_buffer_diff_changes() {
let a = OptimizedBuffer::new(10, 10);
let mut b = OptimizedBuffer::new(10, 10);
b.set(5, 5, Cell::clear(Rgba::RED));
let diff = BufferDiff::compute(&a, &b);
assert!(!diff.is_empty());
assert_eq!(diff.change_count, 1);
assert!(diff.changed_cells.contains(&(5, 5)));
}
#[test]
fn test_buffer_diff_consecutive_cells_same_row() {
let a = OptimizedBuffer::new(10, 10);
let mut b = OptimizedBuffer::new(10, 10);
b.set(2, 5, Cell::clear(Rgba::RED));
b.set(3, 5, Cell::clear(Rgba::RED));
b.set(4, 5, Cell::clear(Rgba::RED));
let diff = BufferDiff::compute(&a, &b);
assert_eq!(diff.change_count, 3);
assert_eq!(diff.dirty_regions.len(), 1);
assert_eq!(diff.dirty_regions[0].x, 2);
assert_eq!(diff.dirty_regions[0].y, 5);
assert_eq!(diff.dirty_regions[0].width, 3);
}
#[test]
fn test_buffer_diff_non_consecutive_cells_same_row() {
let a = OptimizedBuffer::new(10, 10);
let mut b = OptimizedBuffer::new(10, 10);
b.set(0, 5, Cell::clear(Rgba::RED));
b.set(1, 5, Cell::clear(Rgba::RED));
b.set(4, 5, Cell::clear(Rgba::RED));
b.set(5, 5, Cell::clear(Rgba::RED));
let diff = BufferDiff::compute(&a, &b);
assert_eq!(diff.change_count, 4);
assert_eq!(diff.dirty_regions.len(), 2);
}
#[test]
fn test_buffer_diff_multiple_rows() {
let a = OptimizedBuffer::new(10, 10);
let mut b = OptimizedBuffer::new(10, 10);
b.set(5, 0, Cell::clear(Rgba::RED));
b.set(5, 5, Cell::clear(Rgba::GREEN));
b.set(5, 9, Cell::clear(Rgba::BLUE));
let diff = BufferDiff::compute(&a, &b);
assert_eq!(diff.change_count, 3);
assert_eq!(diff.dirty_regions.len(), 3);
}
#[test]
fn test_buffer_diff_should_full_redraw_below_threshold() {
let total_cells = 100;
let diff = BufferDiff {
changed_cells: vec![(0, 0); 40], dirty_regions: vec![],
change_count: 40,
};
assert!(!diff.should_full_redraw(total_cells));
}
#[test]
fn test_buffer_diff_should_full_redraw_above_threshold() {
let total_cells = 100;
let diff = BufferDiff {
changed_cells: vec![(0, 0); 60], dirty_regions: vec![],
change_count: 60,
};
assert!(diff.should_full_redraw(total_cells));
}
#[test]
fn test_buffer_diff_should_full_redraw_at_threshold() {
let total_cells = 100;
let diff = BufferDiff {
changed_cells: vec![(0, 0); 50], dirty_regions: vec![],
change_count: 50,
};
assert!(!diff.should_full_redraw(total_cells));
}
#[test]
fn test_buffer_diff_should_full_redraw_just_above() {
let total_cells = 100;
let diff = BufferDiff {
changed_cells: vec![(0, 0); 51], dirty_regions: vec![],
change_count: 51,
};
assert!(diff.should_full_redraw(total_cells));
}
#[test]
fn test_buffer_diff_detects_fg_color_change() {
let mut a = OptimizedBuffer::new(10, 10);
let mut b = OptimizedBuffer::new(10, 10);
a.set(5, 5, Cell::new('A', Style::fg(Rgba::RED)));
b.set(5, 5, Cell::new('A', Style::fg(Rgba::BLUE)));
let diff = BufferDiff::compute(&a, &b);
assert_eq!(diff.change_count, 1);
assert!(diff.changed_cells.contains(&(5, 5)));
}
#[test]
fn test_buffer_diff_detects_bg_color_change() {
let mut a = OptimizedBuffer::new(10, 10);
let mut b = OptimizedBuffer::new(10, 10);
a.set(5, 5, Cell::new('A', Style::bg(Rgba::RED)));
b.set(5, 5, Cell::new('A', Style::bg(Rgba::BLUE)));
let diff = BufferDiff::compute(&a, &b);
assert_eq!(diff.change_count, 1);
}
#[test]
fn test_buffer_diff_detects_content_change() {
let mut a = OptimizedBuffer::new(10, 10);
let mut b = OptimizedBuffer::new(10, 10);
a.set(5, 5, Cell::new('A', Style::NONE));
b.set(5, 5, Cell::new('B', Style::NONE));
let diff = BufferDiff::compute(&a, &b);
assert_eq!(diff.change_count, 1);
}
#[test]
fn test_buffer_diff_identical_cells_no_change() {
let mut a = OptimizedBuffer::new(10, 10);
let mut b = OptimizedBuffer::new(10, 10);
let cell = Cell::new('X', Style::fg(Rgba::GREEN));
a.set(5, 5, cell);
b.set(5, 5, cell);
let diff = BufferDiff::compute(&a, &b);
assert!(diff.changed_cells.is_empty() || diff.change_count <= 1);
}
#[test]
fn test_buffer_diff_single_cell_buffer() {
let a = OptimizedBuffer::new(1, 1);
let mut b = OptimizedBuffer::new(1, 1);
b.set(0, 0, Cell::clear(Rgba::RED));
let diff = BufferDiff::compute(&a, &b);
assert_eq!(diff.change_count, 1);
}
#[test]
fn test_buffer_diff_large_buffer() {
let a = OptimizedBuffer::new(200, 50);
let mut b = OptimizedBuffer::new(200, 50);
for x in 0..200 {
b.set(x, 25, Cell::clear(Rgba::RED));
}
let diff = BufferDiff::compute(&a, &b);
assert_eq!(diff.change_count, 200);
assert_eq!(diff.dirty_regions.len(), 1);
}
#[test]
fn test_buffer_diff_all_cells_changed() {
let a = OptimizedBuffer::new(10, 10);
let mut b = OptimizedBuffer::new(10, 10);
for y in 0..10 {
for x in 0..10 {
b.set(x, y, Cell::clear(Rgba::RED));
}
}
let diff = BufferDiff::compute(&a, &b);
assert_eq!(diff.change_count, 100);
assert!(diff.should_full_redraw(100));
}
#[test]
fn test_buffer_diff_corners_only() {
let a = OptimizedBuffer::new(10, 10);
let mut b = OptimizedBuffer::new(10, 10);
b.set(0, 0, Cell::clear(Rgba::RED)); b.set(9, 0, Cell::clear(Rgba::GREEN)); b.set(0, 9, Cell::clear(Rgba::BLUE)); b.set(9, 9, Cell::clear(Rgba::WHITE));
let diff = BufferDiff::compute(&a, &b);
assert_eq!(diff.change_count, 4);
assert_eq!(diff.dirty_regions.len(), 4);
}
#[test]
fn test_buffer_diff_detects_attribute_change() {
use crate::style::TextAttributes;
let mut a = OptimizedBuffer::new(10, 10);
let mut b = OptimizedBuffer::new(10, 10);
let cell_a = Cell::new('A', Style::NONE);
let mut cell_b = Cell::new('A', Style::NONE);
cell_b.attributes = TextAttributes::BOLD;
a.set(5, 5, cell_a);
b.set(5, 5, cell_b);
let diff = BufferDiff::compute(&a, &b);
assert_eq!(diff.change_count, 1);
}
#[test]
fn test_buffer_diff_column_change() {
let a = OptimizedBuffer::new(10, 10);
let mut b = OptimizedBuffer::new(10, 10);
for y in 0..10 {
b.set(5, y, Cell::clear(Rgba::RED));
}
let diff = BufferDiff::compute(&a, &b);
assert_eq!(diff.change_count, 10);
assert_eq!(diff.dirty_regions.len(), 10);
}
#[test]
fn test_buffer_diff_wide_buffer() {
let a = OptimizedBuffer::new(1000, 1);
let mut b = OptimizedBuffer::new(1000, 1);
b.set(0, 0, Cell::clear(Rgba::RED));
b.set(500, 0, Cell::clear(Rgba::GREEN));
b.set(999, 0, Cell::clear(Rgba::BLUE));
let diff = BufferDiff::compute(&a, &b);
assert_eq!(diff.change_count, 3);
assert_eq!(diff.dirty_regions.len(), 3);
}
#[test]
fn test_buffer_diff_tall_buffer() {
let a = OptimizedBuffer::new(1, 1000);
let mut b = OptimizedBuffer::new(1, 1000);
b.set(0, 0, Cell::clear(Rgba::RED));
b.set(0, 500, Cell::clear(Rgba::GREEN));
b.set(0, 999, Cell::clear(Rgba::BLUE));
let diff = BufferDiff::compute(&a, &b);
assert_eq!(diff.change_count, 3);
assert_eq!(diff.dirty_regions.len(), 3);
}
#[test]
fn test_buffer_diff_bits_eq_performance() {
let cell1 = Cell::new('A', Style::fg(Rgba::new(0.5, 0.5, 0.5, 1.0)));
let cell2 = Cell::new('A', Style::fg(Rgba::new(0.5, 0.5, 0.5, 1.0)));
assert!(
cell1.bits_eq(&cell2),
"bits_eq should detect identical cells"
);
let cell3 = Cell::new('B', Style::fg(Rgba::new(0.5, 0.5, 0.5, 1.0)));
assert!(
!cell1.bits_eq(&cell3),
"bits_eq should detect different cells"
);
}
#[test]
fn test_buffer_diff_color_precision() {
let mut a = OptimizedBuffer::new(10, 10);
let mut b = OptimizedBuffer::new(10, 10);
a.set(
5,
5,
Cell::new('A', Style::fg(Rgba::new(0.5, 0.5, 0.5, 1.0))),
);
b.set(
5,
5,
Cell::new('A', Style::fg(Rgba::new(0.500_001, 0.5, 0.5, 1.0))),
);
let diff = BufferDiff::compute(&a, &b);
assert_eq!(diff.change_count, 1);
}
#[test]
fn test_buffer_diff_alpha_change() {
let mut a = OptimizedBuffer::new(10, 10);
let mut b = OptimizedBuffer::new(10, 10);
a.set(5, 5, Cell::new('A', Style::fg(Rgba::RED)));
b.set(5, 5, Cell::new('A', Style::fg(Rgba::RED.with_alpha(0.5))));
let diff = BufferDiff::compute(&a, &b);
assert_eq!(diff.change_count, 1);
}
#[test]
fn test_buffer_diff_diagonal_changes() {
let a = OptimizedBuffer::new(10, 10);
let mut b = OptimizedBuffer::new(10, 10);
for i in 0..10 {
b.set(i, i, Cell::clear(Rgba::RED));
}
let diff = BufferDiff::compute(&a, &b);
assert_eq!(diff.change_count, 10);
assert_eq!(diff.dirty_regions.len(), 10);
}
#[test]
fn test_buffer_diff_checkerboard() {
let a = OptimizedBuffer::new(8, 8);
let mut b = OptimizedBuffer::new(8, 8);
for y in 0..8 {
for x in 0..8 {
if (x + y) % 2 == 0 {
b.set(x, y, Cell::clear(Rgba::RED));
}
}
}
let diff = BufferDiff::compute(&a, &b);
assert_eq!(diff.change_count, 32); assert!(!diff.should_full_redraw(64)); }
#[test]
fn test_dirty_region_zero_dimensions() {
let region = DirtyRegion::new(5, 5, 0, 0);
assert_eq!(region.width, 0);
assert_eq!(region.height, 0);
let other = DirtyRegion::new(10, 10, 5, 5);
let merged = region.merge(&other);
assert_eq!(merged.x, 5);
assert_eq!(merged.y, 5);
assert_eq!(merged.width, 10);
assert_eq!(merged.height, 10);
}
#[test]
fn test_buffer_diff_first_row() {
let a = OptimizedBuffer::new(100, 50);
let mut b = OptimizedBuffer::new(100, 50);
for x in 0..100 {
b.set(x, 0, Cell::clear(Rgba::RED));
}
let diff = BufferDiff::compute(&a, &b);
assert_eq!(diff.change_count, 100);
assert_eq!(diff.dirty_regions.len(), 1);
assert_eq!(diff.dirty_regions[0].y, 0);
assert_eq!(diff.dirty_regions[0].width, 100);
}
#[test]
fn test_buffer_diff_last_row() {
let a = OptimizedBuffer::new(100, 50);
let mut b = OptimizedBuffer::new(100, 50);
for x in 0..100 {
b.set(x, 49, Cell::clear(Rgba::RED));
}
let diff = BufferDiff::compute(&a, &b);
assert_eq!(diff.change_count, 100);
assert_eq!(diff.dirty_regions.len(), 1);
assert_eq!(diff.dirty_regions[0].y, 49);
}
#[test]
fn test_buffer_diff_sparse_changes() {
let a = OptimizedBuffer::new(100, 100);
let mut b = OptimizedBuffer::new(100, 100);
for y in (0..100).step_by(10) {
for x in (0..100).step_by(10) {
b.set(x, y, Cell::clear(Rgba::RED));
}
}
let diff = BufferDiff::compute(&a, &b);
assert_eq!(diff.change_count, 100); assert!(!diff.should_full_redraw(10000)); }
#[test]
fn test_buffer_diff_is_empty_consistency() {
let a = OptimizedBuffer::new(10, 10);
let b = OptimizedBuffer::new(10, 10);
let diff = BufferDiff::compute(&a, &b);
assert_eq!(diff.is_empty(), diff.change_count == 0);
assert!(diff.is_empty());
let mut c = OptimizedBuffer::new(10, 10);
c.set(0, 0, Cell::clear(Rgba::RED));
let diff2 = BufferDiff::compute(&a, &c);
assert_eq!(diff2.is_empty(), diff2.change_count == 0);
assert!(!diff2.is_empty());
}
#[test]
fn test_try_compute_success() {
let a = OptimizedBuffer::new(10, 10);
let b = OptimizedBuffer::new(10, 10);
let result = BufferDiff::try_compute(&a, &b);
assert!(result.is_ok());
let diff = result.unwrap();
assert!(diff.is_empty());
}
#[test]
fn test_try_compute_size_mismatch() {
let a = OptimizedBuffer::new(10, 10);
let b = OptimizedBuffer::new(20, 20);
let result = BufferDiff::try_compute(&a, &b);
assert!(result.is_err());
match result {
Err(crate::error::Error::BufferSizeMismatch { old_size, new_size }) => {
assert_eq!(old_size, (10, 10));
assert_eq!(new_size, (20, 20));
}
other => {
assert!(
matches!(other, Err(crate::error::Error::BufferSizeMismatch { .. })),
"expected BufferSizeMismatch error"
);
}
}
}
#[test]
fn test_try_compute_width_mismatch() {
let a = OptimizedBuffer::new(10, 10);
let b = OptimizedBuffer::new(15, 10);
let result = BufferDiff::try_compute(&a, &b);
assert!(result.is_err());
}
#[test]
fn test_try_compute_height_mismatch() {
let a = OptimizedBuffer::new(10, 10);
let b = OptimizedBuffer::new(10, 15);
let result = BufferDiff::try_compute(&a, &b);
assert!(result.is_err());
}
#[test]
fn test_compute_delegates_to_try() {
let a = OptimizedBuffer::new(10, 10);
let mut b = OptimizedBuffer::new(10, 10);
b.set(5, 5, Cell::clear(Rgba::RED));
let diff = BufferDiff::compute(&a, &b);
assert_eq!(diff.change_count, 1);
}
}