Skip to main content

katui_present/surface/
buffer.rs

1//! The diffed cell-buffer strategy: write a whole cell buffer once,
2//! then only the cells that changed, frame over frame.
3
4use crate::Sink;
5use std::io::{self, Write as _};
6
7use katui_scene::cell::{Buffer, all_cells, diff};
8
9/// The stateful cell-buffer presenter: writes a buffer's cells once,
10/// then per frame only the cells that changed, inside synchronized
11/// output. Indexed and truecolor cell colors travel through verbatim,
12/// which makes this the presenter cell-plane demos drive directly and
13/// the engine behind the blocks path. A fresh or resized buffer clears
14/// and repaints everything; an unchanged frame writes nothing.
15#[derive(Debug, Default)]
16pub struct BufferSurface {
17    prev: Option<Buffer>,
18}
19
20impl BufferSurface {
21    /// A fresh presenter with no previous frame.
22    #[must_use]
23    pub fn new() -> Self {
24        Self::default()
25    }
26
27    /// Write one frame of `next` and return the bytes written. A fresh
28    /// or resized frame clears the screen and repaints every cell; a
29    /// later frame repaints only the changed cells, inside synchronized
30    /// output, with no clear that would erase the rest.
31    pub fn render_frame<W: Sink>(&mut self, out: &mut W, next: Buffer) -> io::Result<usize> {
32        let full = self
33            .prev
34            .as_ref()
35            .is_none_or(|prev| prev.size() != next.size());
36        let changed = match self.prev.as_ref() {
37            Some(prev) if !full => diff(prev, &next),
38            _ => all_cells(&next),
39        };
40        let mut bytes = Vec::new();
41        if !changed.is_empty() {
42            bytes.write_all(b"\x1b[?2026h")?;
43            if full {
44                bytes.write_all(b"\x1b[H\x1b[2J")?;
45            }
46            for (position, cell) in changed {
47                crate::cells::render_cell(&mut bytes, position, cell)?;
48            }
49            bytes.write_all(b"\x1b[0m\x1b[?2026l")?;
50        }
51        self.prev = Some(next);
52        out.write_all(&bytes)?;
53        Ok(bytes.len())
54    }
55}
56
57#[cfg(test)]
58mod tests {
59    use super::BufferSurface;
60    use katui_scene::CellSize;
61    use katui_scene::cell::{Buffer, Cell, Color, Position};
62
63    fn grid(columns: u16, rows: u16, alive: &[(u16, u16)]) -> Buffer {
64        let mut buffer = Buffer::blank(CellSize { columns, rows });
65        for &(x, y) in alive {
66            buffer.set(
67                Position { x, y },
68                Cell {
69                    symbol: "█".to_owned(),
70                    fg: Color::Indexed(9),
71                    bg: Color::Reset,
72                    attrs: katui_scene::cell::Attrs::EMPTY,
73                },
74            );
75        }
76        buffer
77    }
78
79    #[test]
80    fn an_identical_frame_writes_nothing() {
81        let mut presenter = BufferSurface::new();
82        let mut first = Vec::new();
83        presenter
84            .render_frame(&mut first, grid(8, 4, &[(1, 1), (2, 2)]))
85            .expect("writing to a Vec cannot fail");
86        let mut second = Vec::new();
87        let written = presenter
88            .render_frame(&mut second, grid(8, 4, &[(1, 1), (2, 2)]))
89            .expect("writing to a Vec cannot fail");
90        assert_eq!(written, 0, "no cell change means no bytes");
91        assert!(second.is_empty());
92    }
93
94    #[test]
95    fn a_fresh_frame_clears_and_a_later_change_does_not() {
96        let mut presenter = BufferSurface::new();
97        let mut first = Vec::new();
98        presenter
99            .render_frame(&mut first, grid(8, 4, &[(1, 1)]))
100            .expect("writing to a Vec cannot fail");
101        assert!(
102            String::from_utf8_lossy(&first).contains("\x1b[H\x1b[2J"),
103            "the first frame clears"
104        );
105        let mut second = Vec::new();
106        presenter
107            .render_frame(&mut second, grid(8, 4, &[(2, 2)]))
108            .expect("writing to a Vec cannot fail");
109        let text = String::from_utf8_lossy(&second);
110        assert!(!text.contains("\x1b[2J"), "a diffed frame never erases");
111        assert!(
112            text.contains("\x1b[38;5;9m"),
113            "indexed colors emit the 256-color SGR"
114        );
115    }
116
117    #[test]
118    fn a_resize_repaints_everything() {
119        let mut presenter = BufferSurface::new();
120        let mut first = Vec::new();
121        presenter
122            .render_frame(&mut first, grid(8, 4, &[(1, 1)]))
123            .expect("writing to a Vec cannot fail");
124        let mut second = Vec::new();
125        presenter
126            .render_frame(&mut second, grid(6, 4, &[(1, 1)]))
127            .expect("writing to a Vec cannot fail");
128        assert!(
129            String::from_utf8_lossy(&second).contains("\x1b[2J"),
130            "a resize clears and repaints whole"
131        );
132    }
133}