tuxtui_core/
buffer.rs

1//! Double-buffered terminal cell storage with efficient diffing.
2
3use crate::geometry::Rect;
4use crate::style::Style;
5use alloc::string::String;
6use alloc::vec::Vec;
7use core::fmt;
8use unicode_width::UnicodeWidthStr;
9
10#[cfg(feature = "serde")]
11use serde::{Deserialize, Serialize};
12
13/// A single cell in the terminal buffer.
14///
15/// Each cell stores a grapheme cluster, style, and skip flag for wide characters.
16#[derive(Debug, Clone, PartialEq, Eq)]
17#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
18pub struct Cell {
19    /// The symbol (grapheme cluster) to display
20    pub symbol: String,
21    /// The style for this cell
22    pub style: Style,
23    /// Skip rendering flag (for wide character continuations)
24    pub skip: bool,
25}
26
27impl Default for Cell {
28    fn default() -> Self {
29        Self {
30            symbol: String::from(" "),
31            style: Style::default(),
32            skip: false,
33        }
34    }
35}
36
37impl Cell {
38    /// Create a new cell with the given symbol and style.
39    ///
40    /// # Example
41    ///
42    /// ```
43    /// use tuxtui_core::buffer::Cell;
44    /// use tuxtui_core::style::Style;
45    ///
46    /// let cell = Cell::new("x", Style::default());
47    /// ```
48    #[must_use]
49    pub fn new(symbol: impl Into<String>, style: Style) -> Self {
50        Self {
51            symbol: symbol.into(),
52            style,
53            skip: false,
54        }
55    }
56
57    /// Reset the cell to a space with default style.
58    pub fn reset(&mut self) {
59        self.symbol.clear();
60        self.symbol.push(' ');
61        self.style = Style::default();
62        self.skip = false;
63    }
64
65    /// Set the symbol for this cell.
66    pub fn set_symbol(&mut self, symbol: impl Into<String>) {
67        self.symbol = symbol.into();
68    }
69
70    /// Set the style for this cell.
71    pub fn set_style(&mut self, style: Style) {
72        self.style = style;
73    }
74
75    /// Get the display width of the symbol (1 or 2 for wide characters).
76    #[must_use]
77    pub fn width(&self) -> usize {
78        self.symbol.width()
79    }
80}
81
82/// A buffer representing the terminal screen.
83///
84/// The buffer is a rectangular grid of [`Cell`]s that can be efficiently
85/// diffed to minimize terminal updates.
86///
87/// # Example
88///
89/// ```
90/// use tuxtui_core::buffer::Buffer;
91/// use tuxtui_core::geometry::Rect;
92/// use tuxtui_core::style::{Color, Style};
93///
94/// let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 5));
95/// buffer.set_string(0, 0, "Hello", Style::default().fg(Color::Green));
96/// ```
97#[derive(Debug, Clone, PartialEq, Eq)]
98#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
99pub struct Buffer {
100    /// The area covered by this buffer
101    pub area: Rect,
102    /// The cells in this buffer (row-major order)
103    pub content: Vec<Cell>,
104}
105
106impl Buffer {
107    /// Create an empty buffer with the given area.
108    ///
109    /// All cells are initialized to spaces with default style.
110    #[must_use]
111    pub fn empty(area: Rect) -> Self {
112        let cell_count = area.area() as usize;
113        Self {
114            area,
115            content: vec![Cell::default(); cell_count],
116        }
117    }
118
119    /// Create a buffer filled with a specific cell.
120    #[must_use]
121    pub fn filled(area: Rect, cell: &Cell) -> Self {
122        let cell_count = area.area() as usize;
123        Self {
124            area,
125            content: vec![cell.clone(); cell_count],
126        }
127    }
128
129    /// Get the index into the content vector for the given coordinates.
130    ///
131    /// Returns `None` if the coordinates are out of bounds.
132    #[must_use]
133    pub const fn index_of(&self, x: u16, y: u16) -> Option<usize> {
134        if x >= self.area.x
135            && x < self.area.x + self.area.width
136            && y >= self.area.y
137            && y < self.area.y + self.area.height
138        {
139            let row = (y - self.area.y) as usize;
140            let col = (x - self.area.x) as usize;
141            Some(row * self.area.width as usize + col)
142        } else {
143            None
144        }
145    }
146
147    /// Get a reference to the cell at the given coordinates.
148    #[must_use]
149    pub fn get(&self, x: u16, y: u16) -> Option<&Cell> {
150        self.index_of(x, y).and_then(|i| self.content.get(i))
151    }
152
153    /// Get a mutable reference to the cell at the given coordinates.
154    pub fn get_mut(&mut self, x: u16, y: u16) -> Option<&mut Cell> {
155        if let Some(i) = self.index_of(x, y) {
156            self.content.get_mut(i)
157        } else {
158            None
159        }
160    }
161
162    /// Set the symbol and style of a cell at the given coordinates.
163    ///
164    /// Returns `true` if the cell was updated, `false` if out of bounds.
165    ///
166    /// # Example
167    ///
168    /// ```
169    /// use tuxtui_core::buffer::Buffer;
170    /// use tuxtui_core::geometry::Rect;
171    /// use tuxtui_core::style::Style;
172    ///
173    /// let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 10));
174    /// buffer.set(5, 5, "X", Style::default());
175    /// ```
176    pub fn set(&mut self, x: u16, y: u16, symbol: impl Into<String>, style: Style) -> bool {
177        if let Some(cell) = self.get_mut(x, y) {
178            let symbol = symbol.into();
179            let width = symbol.width();
180            cell.symbol = symbol;
181            cell.style = style;
182            cell.skip = false;
183
184            // Mark continuation cells for wide characters
185            if width > 1 {
186                for i in 1..width {
187                    if let Some(next_cell) = self.get_mut(x + i as u16, y) {
188                        next_cell.reset();
189                        next_cell.skip = true;
190                    }
191                }
192            }
193            true
194        } else {
195            false
196        }
197    }
198
199    /// Set a string at the given position with a style.
200    ///
201    /// Returns the x-coordinate after the last written character.
202    ///
203    /// # Example
204    ///
205    /// ```
206    /// use tuxtui_core::buffer::Buffer;
207    /// use tuxtui_core::geometry::Rect;
208    /// use tuxtui_core::style::{Color, Style};
209    ///
210    /// let mut buffer = Buffer::empty(Rect::new(0, 0, 20, 5));
211    /// let style = Style::default().fg(Color::Blue);
212    /// let end_x = buffer.set_string(0, 0, "Hello, world!", style);
213    /// ```
214    pub fn set_string(&mut self, x: u16, y: u16, string: &str, style: Style) -> u16 {
215        let mut x = x;
216        for grapheme in unicode_segmentation::UnicodeSegmentation::graphemes(string, true) {
217            if x >= self.area.right() {
218                break;
219            }
220            self.set(x, y, grapheme, style);
221            x += grapheme.width() as u16;
222        }
223        x
224    }
225
226    /// Set a styled string with mixed styles (via spans).
227    ///
228    /// This is used internally by text rendering.
229    pub fn set_styled_string(&mut self, x: u16, y: u16, string: &str, style: Style) -> u16 {
230        self.set_string(x, y, string, style)
231    }
232
233    /// Clear the entire buffer.
234    pub fn clear(&mut self) {
235        for cell in &mut self.content {
236            cell.reset();
237        }
238    }
239
240    /// Clear a specific rectangular region.
241    pub fn clear_region(&mut self, region: Rect) {
242        let region = self.area.intersection(region);
243        for y in region.top()..region.bottom() {
244            for x in region.left()..region.right() {
245                if let Some(cell) = self.get_mut(x, y) {
246                    cell.reset();
247                }
248            }
249        }
250    }
251
252    /// Set the style for subsequent operations (no-op for buffer).
253    pub fn set_style(&mut self, _style: Style) {
254        // Buffer doesn't have a global style, this is a no-op
255    }
256
257    /// Resize the buffer to a new area.
258    ///
259    /// Content is preserved where it overlaps; new areas are filled with default cells.
260    pub fn resize(&mut self, area: Rect) {
261        if area == self.area {
262            return;
263        }
264
265        let mut new_buffer = Self::empty(area);
266        let intersection = self.area.intersection(area);
267
268        // Copy overlapping content
269        for y in intersection.top()..intersection.bottom() {
270            for x in intersection.left()..intersection.right() {
271                if let Some(cell) = self.get(x, y) {
272                    if let Some(idx) = new_buffer.index_of(x, y) {
273                        new_buffer.content[idx] = cell.clone();
274                    }
275                }
276            }
277        }
278
279        *self = new_buffer;
280    }
281
282    /// Merge another buffer into this one at the specified position.
283    pub fn merge(&mut self, other: &Self) {
284        let area = self.area.intersection(other.area);
285        for y in area.top()..area.bottom() {
286            for x in area.left()..area.right() {
287                if let Some(cell) = other.get(x, y) {
288                    if !cell.skip {
289                        self.set(x, y, cell.symbol.as_str(), cell.style);
290                    }
291                }
292            }
293        }
294    }
295
296    /// Compute the differences between this buffer and another.
297    ///
298    /// Returns a vector of `Diff` operations representing the minimal changes.
299    #[must_use]
300    pub fn diff<'a>(&'a self, other: &'a Self) -> Vec<Diff<'a>> {
301        let mut diffs = Vec::new();
302
303        if self.area != other.area {
304            // If areas differ, return a full redraw
305            for y in other.area.top()..other.area.bottom() {
306                let mut start_x = None;
307                let mut current_style = None;
308
309                for x in other.area.left()..other.area.right() {
310                    if let Some(cell) = other.get(x, y) {
311                        if cell.skip {
312                            continue;
313                        }
314
315                        if start_x.is_none() {
316                            start_x = Some(x);
317                            current_style = Some(cell.style);
318                        }
319
320                        if Some(cell.style) != current_style {
321                            // Style changed, flush current run
322                            if let Some(sx) = start_x {
323                                diffs.push(Diff {
324                                    x: sx,
325                                    y,
326                                    cells: Vec::new(), // Simplified for now
327                                });
328                            }
329                            start_x = Some(x);
330                            current_style = Some(cell.style);
331                        }
332                    }
333                }
334            }
335            return diffs;
336        }
337
338        // Row-by-row diff
339        for y in self.area.top()..self.area.bottom() {
340            let mut x = self.area.left();
341            while x < self.area.right() {
342                let old_cell = self.get(x, y);
343                let new_cell = other.get(x, y);
344
345                if old_cell != new_cell {
346                    if let Some(new_cell) = new_cell {
347                        diffs.push(Diff {
348                            x,
349                            y,
350                            cells: alloc::vec![new_cell],
351                        });
352                    }
353                }
354                x += 1;
355            }
356        }
357
358        diffs
359    }
360}
361
362impl fmt::Display for Buffer {
363    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
364        for y in self.area.top()..self.area.bottom() {
365            for x in self.area.left()..self.area.right() {
366                if let Some(cell) = self.get(x, y) {
367                    if !cell.skip {
368                        write!(f, "{}", cell.symbol)?;
369                    }
370                }
371            }
372            if y < self.area.bottom() - 1 {
373                writeln!(f)?;
374            }
375        }
376        Ok(())
377    }
378}
379
380/// A diff operation representing changes between two buffers.
381#[derive(Debug, Clone)]
382pub struct Diff<'a> {
383    /// X coordinate
384    pub x: u16,
385    /// Y coordinate
386    pub y: u16,
387    /// Cells that changed at this position
388    pub cells: Vec<&'a Cell>,
389}
390
391#[cfg(test)]
392mod tests {
393    use super::*;
394    use crate::style::Color;
395
396    #[test]
397    fn test_buffer_set_get() {
398        let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 10));
399        buffer.set(5, 5, "X", Style::default());
400
401        let cell = buffer.get(5, 5).unwrap();
402        assert_eq!(cell.symbol, "X");
403    }
404
405    #[test]
406    fn test_buffer_set_string() {
407        let mut buffer = Buffer::empty(Rect::new(0, 0, 20, 5));
408        let end_x = buffer.set_string(0, 0, "Hello", Style::default());
409
410        assert_eq!(end_x, 5);
411        assert_eq!(buffer.get(0, 0).unwrap().symbol, "H");
412        assert_eq!(buffer.get(4, 0).unwrap().symbol, "o");
413    }
414
415    #[test]
416    fn test_buffer_clear() {
417        let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 10));
418        buffer.set(5, 5, "X", Style::default().fg(Color::Red));
419        buffer.clear();
420
421        let cell = buffer.get(5, 5).unwrap();
422        assert_eq!(cell.symbol, " ");
423        assert_eq!(cell.style, Style::default());
424    }
425
426    #[test]
427    fn test_buffer_merge() {
428        let mut base = Buffer::empty(Rect::new(0, 0, 10, 10));
429        let mut overlay = Buffer::empty(Rect::new(0, 0, 10, 10));
430
431        overlay.set(5, 5, "O", Style::default());
432        base.merge(&overlay);
433
434        assert_eq!(base.get(5, 5).unwrap().symbol, "O");
435    }
436}