Skip to main content

spec_ai/spec_ai_tui/buffer/
buffer.rs

1//! 2D buffer of cells for rendering
2
3use super::Cell;
4use crate::spec_ai_tui::geometry::Rect;
5use crate::spec_ai_tui::style::{Line, Span, Style};
6
7/// 2D buffer of cells for rendering
8#[derive(Debug, Clone)]
9pub struct Buffer {
10    /// The area this buffer represents
11    area: Rect,
12    /// Flat array of cells (row-major order)
13    cells: Vec<Cell>,
14}
15
16impl Buffer {
17    /// Create a new buffer for the given area
18    pub fn new(area: Rect) -> Self {
19        let size = area.area() as usize;
20        Self {
21            area,
22            cells: vec![Cell::empty(); size],
23        }
24    }
25
26    /// Create a buffer filled with a specific cell
27    pub fn filled(area: Rect, cell: Cell) -> Self {
28        let size = area.area() as usize;
29        Self {
30            area,
31            cells: vec![cell; size],
32        }
33    }
34
35    /// Get the buffer area
36    pub fn area(&self) -> Rect {
37        self.area
38    }
39
40    /// Convert absolute (x, y) to index in the cells array
41    fn index(&self, x: u16, y: u16) -> Option<usize> {
42        if x >= self.area.x && x < self.area.right() && y >= self.area.y && y < self.area.bottom() {
43            let local_x = (x - self.area.x) as usize;
44            let local_y = (y - self.area.y) as usize;
45            Some(local_y * self.area.width as usize + local_x)
46        } else {
47            None
48        }
49    }
50
51    /// Get a cell at position (returns None if out of bounds)
52    pub fn get(&self, x: u16, y: u16) -> Option<&Cell> {
53        self.index(x, y).map(|i| &self.cells[i])
54    }
55
56    /// Get a mutable cell at position (returns None if out of bounds)
57    pub fn get_mut(&mut self, x: u16, y: u16) -> Option<&mut Cell> {
58        self.index(x, y).map(|i| &mut self.cells[i])
59    }
60
61    /// Set a cell at position (does nothing if out of bounds)
62    pub fn set(&mut self, x: u16, y: u16, cell: Cell) {
63        if let Some(idx) = self.index(x, y) {
64            self.cells[idx] = cell;
65        }
66    }
67
68    /// Set just the symbol at a position
69    pub fn set_symbol(&mut self, x: u16, y: u16, symbol: &str) {
70        if let Some(cell) = self.get_mut(x, y) {
71            cell.symbol = symbol.to_string();
72        }
73    }
74
75    /// Set a string starting at position with the given style
76    pub fn set_string(&mut self, x: u16, y: u16, s: &str, style: Style) {
77        let mut current_x = x;
78        for c in s.chars() {
79            if current_x >= self.area.right() {
80                break;
81            }
82            if let Some(cell) = self.get_mut(current_x, y) {
83                cell.symbol = c.to_string();
84                cell.fg = style.fg;
85                cell.bg = style.bg;
86                cell.modifier = style.modifier;
87            }
88            // Handle wide characters
89            let width = unicode_width::UnicodeWidthChar::width(c).unwrap_or(1);
90            current_x = current_x.saturating_add(width as u16);
91        }
92    }
93
94    /// Set a string with default style
95    pub fn set_string_raw(&mut self, x: u16, y: u16, s: &str) {
96        self.set_string(x, y, s, Style::default());
97    }
98
99    /// Set a span (styled string)
100    pub fn set_span(&mut self, x: u16, y: u16, span: &Span) {
101        self.set_string(x, y, &span.content, span.style);
102    }
103
104    /// Set a line (multiple spans)
105    pub fn set_line(&mut self, x: u16, y: u16, line: &Line) {
106        let mut current_x = x;
107        for span in &line.spans {
108            self.set_span(current_x, y, span);
109            current_x = current_x.saturating_add(span.width() as u16);
110            if current_x >= self.area.right() {
111                break;
112            }
113        }
114    }
115
116    /// Fill an area with a cell
117    pub fn fill(&mut self, area: Rect, cell: Cell) {
118        let clipped = self.area.intersect(&area);
119        for y in clipped.y..clipped.bottom() {
120            for x in clipped.x..clipped.right() {
121                self.set(x, y, cell.clone());
122            }
123        }
124    }
125
126    /// Fill an area with a specific style (preserves symbols)
127    pub fn fill_style(&mut self, area: Rect, style: Style) {
128        let clipped = self.area.intersect(&area);
129        for y in clipped.y..clipped.bottom() {
130            for x in clipped.x..clipped.right() {
131                if let Some(cell) = self.get_mut(x, y) {
132                    cell.fg = style.fg;
133                    cell.bg = style.bg;
134                    cell.modifier = style.modifier;
135                }
136            }
137        }
138    }
139
140    /// Clear the entire buffer (reset all cells to empty)
141    pub fn clear(&mut self) {
142        for cell in &mut self.cells {
143            cell.reset();
144        }
145    }
146
147    /// Clear an area within the buffer
148    pub fn clear_area(&mut self, area: Rect) {
149        self.fill(area, Cell::empty());
150    }
151
152    /// Iterate over all cells with their positions
153    pub fn iter(&self) -> impl Iterator<Item = (u16, u16, &Cell)> {
154        self.cells.iter().enumerate().map(move |(i, cell)| {
155            let x = self.area.x + (i % self.area.width as usize) as u16;
156            let y = self.area.y + (i / self.area.width as usize) as u16;
157            (x, y, cell)
158        })
159    }
160
161    /// Iterate over cells that differ from another buffer
162    pub fn diff<'a>(&'a self, other: &'a Buffer) -> impl Iterator<Item = (u16, u16, &'a Cell)> {
163        self.iter()
164            .filter(move |(x, y, cell)| other.get(*x, *y).map(|c| c != *cell).unwrap_or(true))
165    }
166
167    /// Merge another buffer into this one at its position
168    pub fn merge(&mut self, other: &Buffer) {
169        for (x, y, cell) in other.iter() {
170            self.set(x, y, cell.clone());
171        }
172    }
173
174    /// Resize the buffer to a new area
175    pub fn resize(&mut self, area: Rect) {
176        let new_size = area.area() as usize;
177        let mut new_cells = vec![Cell::empty(); new_size];
178
179        // Copy existing cells that fit in the new area
180        let copy_area = self.area.intersect(&area);
181        for y in copy_area.y..copy_area.bottom() {
182            for x in copy_area.x..copy_area.right() {
183                if let Some(old_cell) = self.get(x, y) {
184                    let local_x = (x - area.x) as usize;
185                    let local_y = (y - area.y) as usize;
186                    let idx = local_y * area.width as usize + local_x;
187                    if idx < new_cells.len() {
188                        new_cells[idx] = old_cell.clone();
189                    }
190                }
191            }
192        }
193
194        self.area = area;
195        self.cells = new_cells;
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202    use crate::spec_ai_tui::style::Color;
203
204    #[test]
205    fn test_buffer_creation() {
206        let area = Rect::new(0, 0, 10, 5);
207        let buf = Buffer::new(area);
208        assert_eq!(buf.area(), area);
209        assert_eq!(buf.cells.len(), 50);
210    }
211
212    #[test]
213    fn test_buffer_get_set() {
214        let area = Rect::new(0, 0, 10, 5);
215        let mut buf = Buffer::new(area);
216
217        buf.set(5, 2, Cell::new("X").fg(Color::Red));
218
219        let cell = buf.get(5, 2).unwrap();
220        assert_eq!(cell.symbol, "X");
221        assert_eq!(cell.fg, Color::Red);
222    }
223
224    #[test]
225    fn test_buffer_bounds() {
226        let area = Rect::new(5, 5, 10, 10);
227        let buf = Buffer::new(area);
228
229        // Within bounds
230        assert!(buf.get(5, 5).is_some());
231        assert!(buf.get(14, 14).is_some());
232
233        // Out of bounds
234        assert!(buf.get(4, 5).is_none());
235        assert!(buf.get(15, 5).is_none());
236    }
237
238    #[test]
239    fn test_buffer_set_string() {
240        let area = Rect::new(0, 0, 20, 1);
241        let mut buf = Buffer::new(area);
242
243        buf.set_string(0, 0, "Hello", Style::new().fg(Color::Green));
244
245        assert_eq!(buf.get(0, 0).unwrap().symbol, "H");
246        assert_eq!(buf.get(4, 0).unwrap().symbol, "o");
247        assert_eq!(buf.get(5, 0).unwrap().symbol, " "); // After the string
248    }
249
250    #[test]
251    fn test_buffer_fill() {
252        let area = Rect::new(0, 0, 5, 5);
253        let mut buf = Buffer::new(area);
254
255        let fill_area = Rect::new(1, 1, 3, 3);
256        buf.fill(fill_area, Cell::new("#").fg(Color::Blue));
257
258        assert_eq!(buf.get(0, 0).unwrap().symbol, " "); // Outside fill area
259        assert_eq!(buf.get(1, 1).unwrap().symbol, "#"); // Inside fill area
260        assert_eq!(buf.get(3, 3).unwrap().symbol, "#"); // Inside fill area
261        assert_eq!(buf.get(4, 4).unwrap().symbol, " "); // Outside fill area
262    }
263
264    #[test]
265    fn test_buffer_diff() {
266        let area = Rect::new(0, 0, 5, 5);
267        let mut buf1 = Buffer::new(area);
268        let mut buf2 = Buffer::new(area);
269
270        buf1.set(1, 1, Cell::new("A"));
271        buf2.set(1, 1, Cell::new("B"));
272        buf2.set(2, 2, Cell::new("C"));
273
274        let diffs: Vec<_> = buf2.diff(&buf1).collect();
275        assert_eq!(diffs.len(), 2);
276    }
277
278    #[test]
279    fn test_buffer_clear() {
280        let area = Rect::new(0, 0, 5, 5);
281        let mut buf = Buffer::new(area);
282
283        buf.set(2, 2, Cell::new("X"));
284        buf.clear();
285
286        assert_eq!(buf.get(2, 2).unwrap().symbol, " ");
287    }
288}