use ratatui_core::{
buffer::{Buffer, Cell},
layout::Rect,
};
pub struct Frame {
buffer: Buffer,
}
impl Frame {
pub fn new(buffer: Buffer) -> Self {
Self { buffer }
}
#[cfg(test)]
pub fn buffer(&self) -> &Buffer {
&self.buffer
}
pub fn area(&self) -> Rect {
self.buffer.area
}
pub fn diff(&self, previous: &Frame) -> Diff {
let new_area = self.buffer.area;
let prev_area = previous.buffer.area;
if new_area == prev_area {
let changes = previous
.buffer
.diff(&self.buffer)
.into_iter()
.map(|(x, y, cell)| (x, y, cell.clone()))
.collect();
return Diff {
cells: changes,
new_area,
prev_area,
};
}
let max_width = new_area.width.max(prev_area.width);
let max_height = new_area.height.max(prev_area.height);
let default_cell = Cell::default();
let mut changes = Vec::new();
for y in 0..max_height {
for x in 0..max_width {
let in_prev = x < prev_area.width && y < prev_area.height;
let in_new = x < new_area.width && y < new_area.height;
let prev_cell = if in_prev {
&previous.buffer[(x, y)]
} else {
&default_cell
};
let new_cell = if in_new {
&self.buffer[(x, y)]
} else {
&default_cell
};
if prev_cell != new_cell {
changes.push((x, y, new_cell.clone()));
}
}
}
Diff {
cells: changes,
new_area,
prev_area,
}
}
}
pub struct Diff {
pub(crate) cells: Vec<(u16, u16, Cell)>,
pub(crate) new_area: Rect,
pub(crate) prev_area: Rect,
}
impl Frame {
pub fn slice_top_rows(&self, n: u16) -> Frame {
let old_area = self.buffer.area;
let new_height = old_area.height.saturating_sub(n);
if new_height == 0 {
return Frame::new(Buffer::empty(Rect::new(0, 0, old_area.width, 0)));
}
let new_area = Rect::new(0, 0, old_area.width, new_height);
let mut new_buf = Buffer::empty(new_area);
for y in 0..new_height {
for x in 0..old_area.width {
new_buf[(x, y)] = self.buffer[(x, y + n)].clone();
}
}
Frame::new(new_buf)
}
}
impl Diff {
pub fn is_empty(&self) -> bool {
self.cells.is_empty()
}
#[cfg(test)]
pub fn len(&self) -> usize {
self.cells.len()
}
pub fn grew(&self) -> bool {
self.new_area.height > self.prev_area.height
}
#[cfg(test)]
pub fn growth(&self) -> u16 {
self.new_area.height.saturating_sub(self.prev_area.height)
}
pub fn retain_visible(&mut self, min_row: u16) {
if min_row > 0 {
self.cells.retain(|(_, y, _)| *y >= min_row);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_frame(lines: &[&str]) -> Frame {
Frame::new(Buffer::with_lines(lines.iter().map(|s| s.to_string())))
}
#[test]
fn diff_identical_frames_is_empty() {
let f1 = make_frame(&["hello", "world"]);
let f2 = make_frame(&["hello", "world"]);
let diff = f2.diff(&f1);
assert!(diff.is_empty());
}
#[test]
fn diff_single_cell_change() {
let f1 = make_frame(&["hello"]);
let f2 = make_frame(&["hallo"]);
let diff = f2.diff(&f1);
assert_eq!(diff.len(), 1);
assert_eq!(diff.cells[0].0, 1); assert_eq!(diff.cells[0].1, 0); }
#[test]
fn diff_height_growth() {
let f1 = make_frame(&["hello"]);
let f2 = make_frame(&["hello", "world"]);
let diff = f2.diff(&f1);
assert!(diff.grew());
assert_eq!(diff.growth(), 1);
let new_row_cells: Vec<_> = diff.cells.iter().filter(|(_, y, _)| *y == 1).collect();
assert!(!new_row_cells.is_empty());
}
#[test]
fn diff_no_growth_same_height() {
let f1 = make_frame(&["hello", "world"]);
let f2 = make_frame(&["hello", "earth"]);
let diff = f2.diff(&f1);
assert!(!diff.grew());
assert_eq!(diff.growth(), 0);
}
#[test]
fn diff_height_shrink() {
let f1 = make_frame(&["hello", "world"]);
let f2 = make_frame(&["hello"]);
let diff = f2.diff(&f1);
assert!(!diff.grew());
let removed_row_cells: Vec<_> = diff.cells.iter().filter(|(_, y, _)| *y == 1).collect();
assert!(!removed_row_cells.is_empty());
}
}