Skip to main content

a3s_tui/
diff.rs

1//! Incremental diff renderer — only redraws cells that changed between frames.
2
3use crate::grid::{CellChange, Grid};
4use crate::terminal::Terminal;
5use crossterm::{cursor, queue};
6use std::io::{self, Write};
7
8pub struct DiffRenderer {
9    previous: Option<Grid>,
10}
11
12impl DiffRenderer {
13    pub fn new() -> Self {
14        Self { previous: None }
15    }
16
17    pub fn render(&mut self, terminal: &mut Terminal, grid: Grid) -> io::Result<()> {
18        if self.needs_full_paint(&grid) {
19            self.full_paint(terminal, &grid)?;
20        } else if let Some(prev) = &self.previous {
21            let changes = prev.diff(&grid);
22            if !changes.is_empty() {
23                self.apply_changes(terminal, &changes)?;
24            }
25        }
26        self.previous = Some(grid);
27        Ok(())
28    }
29
30    fn needs_full_paint(&self, grid: &Grid) -> bool {
31        match &self.previous {
32            None => true,
33            Some(prev) => prev.width != grid.width || prev.height != grid.height,
34        }
35    }
36
37    fn full_paint(&self, terminal: &mut Terminal, grid: &Grid) -> io::Result<()> {
38        let mut output = String::with_capacity((grid.width as usize * grid.height as usize) * 4);
39        output.push_str("\x1b[H\x1b[2J");
40
41        for row in 0..grid.height {
42            if row > 0 {
43                output.push_str("\r\n");
44            }
45            for col in 0..grid.width {
46                grid.cells[row as usize][col as usize].write_ansi(&mut output);
47            }
48        }
49
50        terminal.write_raw(&output)?;
51        terminal.flush()
52    }
53
54    fn apply_changes(&self, terminal: &mut Terminal, changes: &[CellChange]) -> io::Result<()> {
55        let stdout = terminal.stdout_mut();
56        let mut last_x: i32 = -1;
57        let mut last_y: i32 = -1;
58
59        for change in changes {
60            if change.y as i32 != last_y || change.x as i32 != last_x + 1 {
61                queue!(stdout, cursor::MoveTo(change.x, change.y))?;
62            }
63            write!(stdout, "{}", change.cell.to_ansi())?;
64            last_x = change.x as i32;
65            last_y = change.y as i32;
66        }
67
68        stdout.flush()
69    }
70}
71
72impl Default for DiffRenderer {
73    fn default() -> Self {
74        Self::new()
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn diff_renderer_full_paints_after_grid_size_changes() {
84        let mut renderer = DiffRenderer::new();
85        let initial = Grid::new(2, 1);
86
87        assert!(renderer.needs_full_paint(&initial));
88
89        renderer.previous = Some(initial);
90
91        assert!(!renderer.needs_full_paint(&Grid::new(2, 1)));
92        assert!(renderer.needs_full_paint(&Grid::new(3, 1)));
93        assert!(renderer.needs_full_paint(&Grid::new(2, 2)));
94    }
95}