Skip to main content

katui_present/surface/
blocks.rs

1//! The blocks strategy: quarter-block cells for terminals without a
2//! graphics protocol, damage-tracked so a moving sprite costs a handful
3//! of cells per frame.
4
5use crate::Sink;
6use std::io;
7
8use katui_scene::cell::{Attrs, Buffer, Color, Position};
9use katui_scene::surface::Surface;
10
11use super::BufferSurface;
12use crate::blocks::{Rgb, quarter};
13
14/// The stateful blocks presenter: renders a pixel surface as
15/// quarter-block cells once, then repaints only the cells whose pixels
16/// changed.
17#[derive(Debug, Default)]
18pub struct BlocksSurface {
19    cells: BufferSurface,
20}
21
22impl BlocksSurface {
23    /// A fresh presenter with no previous frame.
24    #[must_use]
25    pub fn new() -> Self {
26        Self::default()
27    }
28
29    /// Write one frame's cells and return the bytes written. A fresh or
30    /// resized frame clears the screen and repaints every cell; a later
31    /// frame repaints only the cells whose pixels changed, inside
32    /// synchronized output, with no clear that would erase the rest.
33    /// An unchanged frame writes nothing.
34    pub fn render_frame<W: Sink>(&mut self, out: &mut W, surface: &Surface) -> io::Result<usize> {
35        self.cells.render_frame(out, cells_for_surface(surface))
36    }
37}
38
39/// Classify every cell of a surface into the quarter-block grid.
40fn cells_for_surface(surface: &Surface) -> Buffer {
41    let columns = u16::try_from(surface.width() / 2).unwrap_or(u16::MAX);
42    let rows = u16::try_from(surface.height() / 2).unwrap_or(u16::MAX);
43    let mut buffer = Buffer::blank(katui_scene::CellSize { columns, rows });
44    for row in 0..rows {
45        for column in 0..columns {
46            let pixel = |dx: u32, dy: u32| -> Rgb {
47                let x = u32::from(column) * 2 + dx;
48                let y = u32::from(row) * 2 + dy;
49                surface
50                    .get(katui_scene::surface::PixelPos::new(x, y))
51                    .map_or(Rgb { r: 0, g: 0, b: 0 }, |color| Rgb {
52                        r: color.r,
53                        g: color.g,
54                        b: color.b,
55                    })
56            };
57            let block = quarter([[pixel(0, 0), pixel(1, 0)], [pixel(0, 1), pixel(1, 1)]]);
58            let cell = katui_scene::cell::Cell {
59                symbol: block.glyph.to_string(),
60                fg: Color::Rgb(block.fg.r, block.fg.g, block.fg.b),
61                bg: Color::Rgb(block.bg.r, block.bg.g, block.bg.b),
62                attrs: Attrs::EMPTY,
63            };
64            buffer.set(Position { x: column, y: row }, cell);
65        }
66    }
67    buffer
68}
69
70#[cfg(test)]
71mod tests {
72    use crate::surface::test_support::solid;
73
74    mod diffed_blocks {
75        use super::super::BlocksSurface;
76        use super::solid;
77        use katui_scene::surface::Rgba;
78
79        #[test]
80        fn an_identical_frame_writes_nothing() {
81            let mut presenter = BlocksSurface::new();
82            let surface = solid(16, 16, Rgba::solid(30, 40, 50));
83            let mut first = Vec::new();
84            presenter
85                .render_frame(&mut first, &surface)
86                .expect("writing to a Vec cannot fail");
87            let mut second = Vec::new();
88            let written = presenter
89                .render_frame(&mut second, &surface)
90                .expect("writing to a Vec cannot fail");
91            assert_eq!(written, 0, "no pixel change means no bytes");
92            assert!(second.is_empty());
93        }
94
95        #[test]
96        fn a_small_change_costs_a_handful_of_cells() {
97            let mut presenter = BlocksSurface::new();
98            let mut surface = solid(32, 32, Rgba::solid(10, 10, 10));
99            let mut first = Vec::new();
100            presenter
101                .render_frame(&mut first, &surface)
102                .expect("writing to a Vec cannot fail");
103            surface.set(
104                katui_scene::surface::PixelPos::new(20, 20),
105                Rgba::solid(255, 0, 0),
106            );
107            let mut second = Vec::new();
108            let written = presenter
109                .render_frame(&mut second, &surface)
110                .expect("writing to a Vec cannot fail");
111            assert!(
112                written < first.len() / 8,
113                "one pixel change is far cheaper than a full frame"
114            );
115            let text = String::from_utf8_lossy(&second);
116            assert!(
117                !text.contains("\x1b[2J"),
118                "a diffed frame never erases the screen"
119            );
120            assert!(
121                text.matches("\x1b[").count() < 10,
122                "a couple of cells repaint, not the screen"
123            );
124        }
125
126        #[test]
127        fn a_size_change_repaints_everything() {
128            let mut presenter = BlocksSurface::new();
129            let mut first = Vec::new();
130            presenter
131                .render_frame(&mut first, &solid(16, 16, Rgba::solid(9, 9, 9)))
132                .expect("writing to a Vec cannot fail");
133            let mut second = Vec::new();
134            let written = presenter
135                .render_frame(&mut second, &solid(32, 32, Rgba::solid(9, 9, 9)))
136                .expect("writing to a Vec cannot fail");
137            assert!(written > 0, "a resize repaints the whole grid");
138        }
139    }
140}