use crate::Sink;
use std::io::{self, Write as _};
use katui_scene::cell::{Buffer, all_cells, diff};
#[derive(Debug, Default)]
pub struct BufferSurface {
prev: Option<Buffer>,
}
impl BufferSurface {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn render_frame<W: Sink>(&mut self, out: &mut W, next: Buffer) -> io::Result<usize> {
let full = self
.prev
.as_ref()
.is_none_or(|prev| prev.size() != next.size());
let changed = match self.prev.as_ref() {
Some(prev) if !full => diff(prev, &next),
_ => all_cells(&next),
};
let mut bytes = Vec::new();
if !changed.is_empty() {
bytes.write_all(b"\x1b[?2026h")?;
if full {
bytes.write_all(b"\x1b[H\x1b[2J")?;
}
for (position, cell) in changed {
crate::cells::render_cell(&mut bytes, position, cell)?;
}
bytes.write_all(b"\x1b[0m\x1b[?2026l")?;
}
self.prev = Some(next);
out.write_all(&bytes)?;
Ok(bytes.len())
}
}
#[cfg(test)]
mod tests {
use super::BufferSurface;
use katui_scene::CellSize;
use katui_scene::cell::{Buffer, Cell, Color, Position};
fn grid(columns: u16, rows: u16, alive: &[(u16, u16)]) -> Buffer {
let mut buffer = Buffer::blank(CellSize { columns, rows });
for &(x, y) in alive {
buffer.set(
Position { x, y },
Cell {
symbol: "█".to_owned(),
fg: Color::Indexed(9),
bg: Color::Reset,
attrs: katui_scene::cell::Attrs::EMPTY,
},
);
}
buffer
}
#[test]
fn an_identical_frame_writes_nothing() {
let mut presenter = BufferSurface::new();
let mut first = Vec::new();
presenter
.render_frame(&mut first, grid(8, 4, &[(1, 1), (2, 2)]))
.expect("writing to a Vec cannot fail");
let mut second = Vec::new();
let written = presenter
.render_frame(&mut second, grid(8, 4, &[(1, 1), (2, 2)]))
.expect("writing to a Vec cannot fail");
assert_eq!(written, 0, "no cell change means no bytes");
assert!(second.is_empty());
}
#[test]
fn a_fresh_frame_clears_and_a_later_change_does_not() {
let mut presenter = BufferSurface::new();
let mut first = Vec::new();
presenter
.render_frame(&mut first, grid(8, 4, &[(1, 1)]))
.expect("writing to a Vec cannot fail");
assert!(
String::from_utf8_lossy(&first).contains("\x1b[H\x1b[2J"),
"the first frame clears"
);
let mut second = Vec::new();
presenter
.render_frame(&mut second, grid(8, 4, &[(2, 2)]))
.expect("writing to a Vec cannot fail");
let text = String::from_utf8_lossy(&second);
assert!(!text.contains("\x1b[2J"), "a diffed frame never erases");
assert!(
text.contains("\x1b[38;5;9m"),
"indexed colors emit the 256-color SGR"
);
}
#[test]
fn a_resize_repaints_everything() {
let mut presenter = BufferSurface::new();
let mut first = Vec::new();
presenter
.render_frame(&mut first, grid(8, 4, &[(1, 1)]))
.expect("writing to a Vec cannot fail");
let mut second = Vec::new();
presenter
.render_frame(&mut second, grid(6, 4, &[(1, 1)]))
.expect("writing to a Vec cannot fail");
assert!(
String::from_utf8_lossy(&second).contains("\x1b[2J"),
"a resize clears and repaints whole"
);
}
}