Skip to main content

louie/core/
buffer.rs

1use super::cell::Cell;
2use super::rect::{Position, Rect};
3use super::style::Style;
4use super::text::{Line, Span};
5use unicode_width::UnicodeWidthStr;
6
7/// A two-dimensional grid of terminal cells.
8///
9/// The buffer is the primary rendering target. Widgets write into a buffer,
10/// and the terminal backend diffs the current buffer against the previous
11/// frame to compute minimal screen updates.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct Buffer {
14    pub area: Rect,
15    pub content: Vec<Cell>,
16}
17
18impl Default for Buffer {
19    fn default() -> Self {
20        Self {
21            area: Rect::ZERO,
22            content: Vec::new(),
23        }
24    }
25}
26
27impl Buffer {
28    /// Create a new buffer filled with empty cells.
29    pub fn empty(area: Rect) -> Self {
30        let size = area.area() as usize;
31        Self {
32            area,
33            content: vec![Cell::default(); size],
34        }
35    }
36
37    /// Create a buffer filled with a specific string (for testing).
38    pub fn with_lines<'a>(lines: impl IntoIterator<Item = &'a str>) -> Self {
39        let lines: Vec<&str> = lines.into_iter().collect();
40        let height = lines.len() as u16;
41        let width = lines.iter().map(|l| l.width() as u16).max().unwrap_or(0);
42        let area = Rect::new(0, 0, width, height);
43        let mut buf = Self::empty(area);
44        for (y, line) in lines.iter().enumerate() {
45            buf.set_string(0, y as u16, line, Style::default());
46        }
47        buf
48    }
49
50    /// Reset all cells to empty.
51    pub fn reset(&mut self) {
52        for cell in &mut self.content {
53            cell.reset();
54        }
55    }
56
57    /// Resize the buffer (discards content).
58    pub fn resize(&mut self, area: Rect) {
59        let size = area.area() as usize;
60        self.area = area;
61        self.content.clear();
62        self.content.resize(size, Cell::default());
63    }
64
65    /// Get the cell at (x, y), if within bounds.
66    pub fn cell(&self, pos: Position) -> Option<&Cell> {
67        self.index_of(pos.x, pos.y).map(|i| &self.content[i])
68    }
69
70    /// Get a mutable reference to the cell at (x, y).
71    pub fn cell_mut(&mut self, pos: Position) -> Option<&mut Cell> {
72        self.index_of(pos.x, pos.y).map(|i| &mut self.content[i])
73    }
74
75    fn index_of(&self, x: u16, y: u16) -> Option<usize> {
76        if x >= self.area.x && x < self.area.right() && y >= self.area.y && y < self.area.bottom() {
77            Some(
78                ((y - self.area.y) as usize) * (self.area.width as usize)
79                    + ((x - self.area.x) as usize),
80            )
81        } else {
82            None
83        }
84    }
85
86    /// Set a string starting at (x, y) with the given style.
87    /// Returns the number of columns consumed.
88    pub fn set_string(&mut self, x: u16, y: u16, string: &str, style: Style) -> u16 {
89        self.set_string_truncated(x, y, string, self.area.right().saturating_sub(x), style)
90    }
91
92    /// Set a string with a maximum width, truncating if necessary.
93    pub fn set_string_truncated(
94        &mut self,
95        x: u16,
96        y: u16,
97        string: &str,
98        max_width: u16,
99        style: Style,
100    ) -> u16 {
101        let mut col = 0u16;
102        for grapheme in unicode_segmentation::UnicodeSegmentation::graphemes(string, true) {
103            let w = grapheme.width() as u16;
104            if col + w > max_width {
105                break;
106            }
107            if let Some(idx) = self.index_of(x + col, y) {
108                self.content[idx].set_symbol(grapheme).set_style(style);
109                // For wide characters, set continuation cells
110                for i in 1..w {
111                    if let Some(idx2) = self.index_of(x + col + i, y) {
112                        self.content[idx2].set_symbol("").set_style(style);
113                    }
114                }
115            }
116            col += w;
117        }
118        col
119    }
120
121    /// Set a styled line at position.
122    pub fn set_line(&mut self, x: u16, y: u16, line: &Line, max_width: u16) -> u16 {
123        let mut col = 0u16;
124        for span in &line.spans {
125            if col >= max_width {
126                break;
127            }
128            let remaining = max_width - col;
129            let written =
130                self.set_string_truncated(x + col, y, &span.content, remaining, span.style);
131            col += written;
132        }
133        col
134    }
135
136    /// Set a single span at position with a maximum width.
137    pub fn set_span(&mut self, x: u16, y: u16, span: &Span, max_width: u16) -> u16 {
138        self.set_string_truncated(x, y, &span.content, max_width, span.style)
139    }
140
141    /// Fill an area with a style (without changing symbols).
142    pub fn set_style(&mut self, area: Rect, style: Style) {
143        let area = self.area.intersection(area);
144        for y in area.y..area.bottom() {
145            for x in area.x..area.right() {
146                if let Some(idx) = self.index_of(x, y) {
147                    self.content[idx].set_style(style);
148                }
149            }
150        }
151    }
152
153    /// Fill an area with a character and style.
154    pub fn fill(&mut self, area: Rect, symbol: &str, style: Style) {
155        let area = self.area.intersection(area);
156        for y in area.y..area.bottom() {
157            for x in area.x..area.right() {
158                if let Some(idx) = self.index_of(x, y) {
159                    self.content[idx].set_symbol(symbol).set_style(style);
160                }
161            }
162        }
163    }
164
165    /// Compute the diff between this buffer and another.
166    /// Returns an iterator of (x, y, &Cell) for cells that differ.
167    pub fn diff<'a>(&'a self, other: &'a Buffer) -> Vec<(u16, u16, &'a Cell)> {
168        let mut changes = Vec::new();
169        let area = self.area.intersection(other.area);
170        for y in area.y..area.bottom() {
171            for x in area.x..area.right() {
172                if let (Some(a), Some(b)) = (self.index_of(x, y), other.index_of(x, y)) {
173                    if self.content[a] != other.content[b] {
174                        changes.push((x, y, &other.content[b]));
175                    }
176                }
177            }
178        }
179        changes
180    }
181
182    /// Merge another buffer on top of this one at its area position.
183    pub fn merge(&mut self, other: &Buffer) {
184        let area = self.area.intersection(other.area);
185        for y in area.y..area.bottom() {
186            for x in area.x..area.right() {
187                if let (Some(dst), Some(src)) = (self.index_of(x, y), other.index_of(x, y)) {
188                    self.content[dst] = other.content[src].clone();
189                }
190            }
191        }
192    }
193}
194
195impl std::ops::Index<(u16, u16)> for Buffer {
196    type Output = Cell;
197    fn index(&self, (x, y): (u16, u16)) -> &Self::Output {
198        /// Sentinel cell returned when indexing out of bounds, preventing panics (MEM-1).
199        static OOB_CELL: std::sync::LazyLock<Cell> = std::sync::LazyLock::new(Cell::default);
200        match self.index_of(x, y) {
201            Some(i) => &self.content[i],
202            None => &OOB_CELL,
203        }
204    }
205}
206
207impl std::ops::IndexMut<(u16, u16)> for Buffer {
208    fn index_mut(&mut self, (x, y): (u16, u16)) -> &mut Self::Output {
209        // Return a writable scratch cell for out-of-bounds writes instead of
210        // panicking (MEM-1 hardening).  The scratch cell is appended once
211        // and reused for subsequent OOB accesses within the same frame.
212        match self.index_of(x, y) {
213            Some(i) => &mut self.content[i],
214            None => {
215                self.content.push(Cell::default());
216                let last = self.content.len() - 1;
217                &mut self.content[last]
218            }
219        }
220    }
221}