Skip to main content

smelt_term/
compositor.rs

1use super::flush::flush_diff;
2use super::grid::Grid;
3use super::Theme;
4use crossterm::terminal::{BeginSynchronizedUpdate, EndSynchronizedUpdate};
5use crossterm::QueueableCommand;
6use std::io::Write;
7
8/// Double-buffered terminal renderer. Diffs `current` against `previous`
9/// and flushes only changed cells; `force_redraw` triggers a full repaint.
10pub struct Compositor {
11    current: Grid,
12    previous: Grid,
13    width: u16,
14    height: u16,
15    force_redraw: bool,
16}
17
18impl Compositor {
19    pub fn new(width: u16, height: u16) -> Self {
20        Self {
21            current: Grid::new(width, height),
22            previous: Grid::new(width, height),
23            width,
24            height,
25            force_redraw: true,
26        }
27    }
28
29    pub fn resize(&mut self, width: u16, height: u16) {
30        self.width = width;
31        self.height = height;
32        self.current.resize(width, height);
33        self.previous.resize(width, height);
34        self.force_redraw = true;
35    }
36
37    /// Render one frame. `paint` writes into `current`. The hardware caret
38    /// stays hidden for the lifetime of the app - any visible cursor is
39    /// painted into the grid as a styled cell, so it rides the diff atomically
40    /// with the rest of the frame and can never flicker through the
41    /// intermediate `MoveTo`s that `flush_diff` emits between cell runs.
42    pub fn render_with<W: Write, F: FnOnce(&mut Grid, &Theme)>(
43        &mut self,
44        theme: &Theme,
45        w: &mut W,
46        paint: F,
47    ) -> std::io::Result<()> {
48        self.current.clear_all();
49        paint(&mut self.current, theme);
50
51        w.queue(BeginSynchronizedUpdate)?;
52
53        if self.force_redraw {
54            flush_full(&self.current, w)?;
55        } else {
56            flush_diff(w, self.current.diff(&self.previous))?;
57        }
58
59        w.queue(EndSynchronizedUpdate)?;
60        w.flush()?;
61
62        self.current.swap_with(&mut self.previous);
63        self.force_redraw = false;
64
65        Ok(())
66    }
67
68    pub fn force_redraw(&mut self) {
69        self.force_redraw = true;
70    }
71
72    /// The most recently flushed grid (snapshot harnesses read this after a discard-writer render).
73    pub fn previous(&self) -> &Grid {
74        &self.previous
75    }
76}
77
78fn flush_full<W: Write>(grid: &Grid, w: &mut W) -> std::io::Result<()> {
79    use super::grid::Style;
80    use crossterm::cursor::MoveTo;
81    use crossterm::style::{
82        Attribute, ResetColor, SetAttribute, SetBackgroundColor, SetForegroundColor,
83    };
84
85    use crate::grid::char_width;
86
87    let mut current_style = Style::default();
88    for y in 0..grid.height() {
89        w.queue(MoveTo(0, y))?;
90        let mut terminal_col: u16 = 0;
91        let mut x = 0u16;
92        while x < grid.width() {
93            let cell = grid.cell(x, y);
94            // `\0` is a wide-char continuation slot - paint a space to
95            // keep the cursor in sync rather than emitting a literal NUL.
96            let symbol = if cell.symbol == '\0' {
97                ' '
98            } else {
99                cell.symbol
100            };
101            let cw = char_width(symbol);
102
103            // Wide char overflowing the right edge: emit a space to prevent wrapping.
104            let (sym, emit_w) = if terminal_col + cw > grid.width() {
105                (' ', 1u16)
106            } else {
107                (symbol, cw)
108            };
109
110            if cell.style != current_style {
111                w.queue(SetAttribute(Attribute::Reset))?;
112                w.queue(ResetColor)?;
113                if let Some(fg) = cell.style.fg {
114                    w.queue(SetForegroundColor(super::grid::to_crossterm_color(fg)))?;
115                }
116                if let Some(bg) = cell.style.bg {
117                    w.queue(SetBackgroundColor(super::grid::to_crossterm_color(bg)))?;
118                }
119                if cell.style.bold {
120                    w.queue(SetAttribute(Attribute::Bold))?;
121                }
122                if cell.style.dim {
123                    w.queue(SetAttribute(Attribute::Dim))?;
124                }
125                if cell.style.italic {
126                    w.queue(SetAttribute(Attribute::Italic))?;
127                }
128                if cell.style.underline {
129                    w.queue(SetAttribute(Attribute::Underlined))?;
130                }
131                if cell.style.crossedout {
132                    w.queue(SetAttribute(Attribute::CrossedOut))?;
133                }
134                if cell.style.reverse {
135                    w.queue(SetAttribute(Attribute::Reverse))?;
136                }
137                current_style = cell.style;
138            }
139            let mut buf = [0u8; 4];
140            let s = sym.encode_utf8(&mut buf);
141            w.write_all(s.as_bytes())?;
142
143            terminal_col += emit_w;
144            // Skip the continuation cell so the grid cursor matches the terminal's visual width.
145            x += emit_w;
146        }
147    }
148    w.queue(SetAttribute(Attribute::Reset))?;
149    w.queue(ResetColor)?;
150    Ok(())
151}