Skip to main content

slt/
buffer.rs

1use crate::cell::Cell;
2use crate::rect::Rect;
3use crate::style::Style;
4use unicode_width::UnicodeWidthChar;
5
6/// A 2D grid of [`Cell`]s backing the terminal display.
7///
8/// Two buffers are kept (current + previous); only the diff is flushed to the
9/// terminal, giving immediate-mode ergonomics with retained-mode efficiency.
10///
11/// The buffer also maintains a clip stack. Push a [`Rect`] with
12/// [`Buffer::push_clip`] to restrict writes to that region, and pop it with
13/// [`Buffer::pop_clip`] when done.
14pub struct Buffer {
15    /// The area this buffer covers, in terminal coordinates.
16    pub area: Rect,
17    /// Flat row-major storage of all cells. Length equals `area.width * area.height`.
18    pub content: Vec<Cell>,
19    clip_stack: Vec<Rect>,
20}
21
22impl Buffer {
23    /// Create a buffer filled with blank cells covering `area`.
24    pub fn empty(area: Rect) -> Self {
25        let size = area.area() as usize;
26        Self {
27            area,
28            content: vec![Cell::default(); size],
29            clip_stack: Vec::new(),
30        }
31    }
32
33    /// Push a clipping rectangle onto the clip stack.
34    ///
35    /// Subsequent writes are restricted to the intersection of all active clip
36    /// regions. Nested calls intersect with the current clip, so the effective
37    /// clip can only shrink, never grow.
38    pub fn push_clip(&mut self, rect: Rect) {
39        let effective = if let Some(current) = self.clip_stack.last() {
40            intersect_rects(*current, rect)
41        } else {
42            rect
43        };
44        self.clip_stack.push(effective);
45    }
46
47    /// Pop the most recently pushed clipping rectangle.
48    ///
49    /// After this call, writes are clipped to the previous region (or
50    /// unclipped if the stack is now empty).
51    pub fn pop_clip(&mut self) {
52        self.clip_stack.pop();
53    }
54
55    fn effective_clip(&self) -> Option<&Rect> {
56        self.clip_stack.last()
57    }
58
59    #[inline]
60    fn index_of(&self, x: u32, y: u32) -> usize {
61        ((y - self.area.y) * self.area.width + (x - self.area.x)) as usize
62    }
63
64    /// Returns `true` if `(x, y)` is within the buffer's area.
65    #[inline]
66    pub fn in_bounds(&self, x: u32, y: u32) -> bool {
67        x >= self.area.x && x < self.area.right() && y >= self.area.y && y < self.area.bottom()
68    }
69
70    /// Return a reference to the cell at `(x, y)`.
71    ///
72    /// Panics if `(x, y)` is out of bounds.
73    #[inline]
74    pub fn get(&self, x: u32, y: u32) -> &Cell {
75        &self.content[self.index_of(x, y)]
76    }
77
78    /// Return a mutable reference to the cell at `(x, y)`.
79    ///
80    /// Panics if `(x, y)` is out of bounds.
81    #[inline]
82    pub fn get_mut(&mut self, x: u32, y: u32) -> &mut Cell {
83        let idx = self.index_of(x, y);
84        &mut self.content[idx]
85    }
86
87    /// Write a string into the buffer starting at `(x, y)`.
88    ///
89    /// Respects cell boundaries and Unicode character widths. Wide characters
90    /// (e.g., CJK) occupy two columns; the trailing cell is blanked. Writes
91    /// that fall outside the current clip region are skipped but still advance
92    /// the cursor position.
93    pub fn set_string(&mut self, mut x: u32, y: u32, s: &str, style: Style) {
94        if y >= self.area.bottom() {
95            return;
96        }
97        let clip = self.effective_clip().copied();
98        for ch in s.chars() {
99            if x >= self.area.right() {
100                break;
101            }
102            let char_width = UnicodeWidthChar::width(ch).unwrap_or(0) as u32;
103            if char_width == 0 {
104                continue;
105            }
106
107            let in_clip = clip.map_or(true, |clip| {
108                x >= clip.x && x < clip.right() && y >= clip.y && y < clip.bottom()
109            });
110
111            if !in_clip {
112                x = x.saturating_add(char_width);
113                continue;
114            }
115
116            let cell = self.get_mut(x, y);
117            cell.set_char(ch);
118            cell.set_style(style);
119
120            // Wide characters occupy two cells; blank the trailing cell.
121            if char_width > 1 {
122                let next_x = x + 1;
123                if next_x < self.area.right() {
124                    let next = self.get_mut(next_x, y);
125                    next.symbol.clear();
126                    next.style = style;
127                }
128            }
129
130            x = x.saturating_add(char_width);
131        }
132    }
133
134    /// Write a single character at `(x, y)` with the given style.
135    ///
136    /// No-ops if `(x, y)` is out of bounds or outside the current clip region.
137    pub fn set_char(&mut self, x: u32, y: u32, ch: char, style: Style) {
138        let in_clip = self.effective_clip().map_or(true, |clip| {
139            x >= clip.x && x < clip.right() && y >= clip.y && y < clip.bottom()
140        });
141        if !self.in_bounds(x, y) || !in_clip {
142            return;
143        }
144        let cell = self.get_mut(x, y);
145        cell.set_char(ch);
146        cell.set_style(style);
147    }
148
149    /// Compute the diff between `self` (current) and `other` (previous).
150    ///
151    /// Returns `(x, y, cell)` tuples for every cell that changed. The run loop
152    /// uses this to emit only the minimal set of terminal escape sequences
153    /// needed to update the display.
154    pub fn diff<'a>(&'a self, other: &'a Buffer) -> Vec<(u32, u32, &'a Cell)> {
155        let mut updates = Vec::new();
156        for y in self.area.y..self.area.bottom() {
157            for x in self.area.x..self.area.right() {
158                let cur = self.get(x, y);
159                let prev = other.get(x, y);
160                if cur != prev {
161                    updates.push((x, y, cur));
162                }
163            }
164        }
165        updates
166    }
167
168    /// Reset every cell to a blank space with default style, and clear the clip stack.
169    pub fn reset(&mut self) {
170        for cell in &mut self.content {
171            cell.reset();
172        }
173        self.clip_stack.clear();
174    }
175
176    /// Resize the buffer to fit a new area, resetting all cells.
177    ///
178    /// If the new area is larger, new cells are initialized to blank. All
179    /// existing content is discarded.
180    pub fn resize(&mut self, area: Rect) {
181        self.area = area;
182        let size = area.area() as usize;
183        self.content.resize(size, Cell::default());
184        self.reset();
185    }
186}
187
188fn intersect_rects(a: Rect, b: Rect) -> Rect {
189    let x = a.x.max(b.x);
190    let y = a.y.max(b.y);
191    let right = a.right().min(b.right());
192    let bottom = a.bottom().min(b.bottom());
193    let width = right.saturating_sub(x);
194    let height = bottom.saturating_sub(y);
195    Rect::new(x, y, width, height)
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    #[test]
203    fn clip_stack_intersects_nested_regions() {
204        let mut buf = Buffer::empty(Rect::new(0, 0, 10, 5));
205        buf.push_clip(Rect::new(1, 1, 6, 3));
206        buf.push_clip(Rect::new(4, 0, 6, 4));
207
208        buf.set_char(3, 2, 'x', Style::new());
209        buf.set_char(4, 2, 'y', Style::new());
210
211        assert_eq!(buf.get(3, 2).symbol, " ");
212        assert_eq!(buf.get(4, 2).symbol, "y");
213    }
214
215    #[test]
216    fn set_string_advances_even_when_clipped() {
217        let mut buf = Buffer::empty(Rect::new(0, 0, 8, 1));
218        buf.push_clip(Rect::new(2, 0, 6, 1));
219
220        buf.set_string(0, 0, "abcd", Style::new());
221
222        assert_eq!(buf.get(2, 0).symbol, "c");
223        assert_eq!(buf.get(3, 0).symbol, "d");
224    }
225
226    #[test]
227    fn pop_clip_restores_previous_clip() {
228        let mut buf = Buffer::empty(Rect::new(0, 0, 6, 1));
229        buf.push_clip(Rect::new(0, 0, 2, 1));
230        buf.push_clip(Rect::new(4, 0, 2, 1));
231
232        buf.set_char(1, 0, 'a', Style::new());
233        buf.pop_clip();
234        buf.set_char(1, 0, 'b', Style::new());
235
236        assert_eq!(buf.get(1, 0).symbol, "b");
237    }
238
239    #[test]
240    fn reset_clears_clip_stack() {
241        let mut buf = Buffer::empty(Rect::new(0, 0, 4, 1));
242        buf.push_clip(Rect::new(0, 0, 0, 0));
243        buf.reset();
244        buf.set_char(0, 0, 'z', Style::new());
245
246        assert_eq!(buf.get(0, 0).symbol, "z");
247    }
248}