Skip to main content

saorsa_core/
buffer.rs

1//! Screen buffer — a 2D grid of terminal cells.
2
3use crate::cell::Cell;
4use crate::geometry::Size;
5
6/// A 2D grid of terminal cells representing one frame of terminal content.
7#[derive(Clone, Debug)]
8pub struct ScreenBuffer {
9    cells: Vec<Cell>,
10    width: u16,
11    height: u16,
12}
13
14impl ScreenBuffer {
15    /// Create a new screen buffer filled with blank cells.
16    pub fn new(size: Size) -> Self {
17        let len = usize::from(size.width) * usize::from(size.height);
18        Self {
19            cells: vec![Cell::blank(); len],
20            width: size.width,
21            height: size.height,
22        }
23    }
24
25    /// Get the buffer dimensions.
26    pub fn size(&self) -> Size {
27        Size::new(self.width, self.height)
28    }
29
30    /// Get the buffer width.
31    pub fn width(&self) -> u16 {
32        self.width
33    }
34
35    /// Get the buffer height.
36    pub fn height(&self) -> u16 {
37        self.height
38    }
39
40    /// Clear the buffer, resetting all cells to blank.
41    pub fn clear(&mut self) {
42        for cell in &mut self.cells {
43            *cell = Cell::blank();
44        }
45    }
46
47    /// Resize the buffer. Contents are lost (filled with blanks).
48    pub fn resize(&mut self, size: Size) {
49        self.width = size.width;
50        self.height = size.height;
51        let len = usize::from(size.width) * usize::from(size.height);
52        self.cells.clear();
53        self.cells.resize(len, Cell::blank());
54    }
55
56    /// Get a reference to the cell at (x, y), or `None` if out of bounds.
57    pub fn get(&self, x: u16, y: u16) -> Option<&Cell> {
58        if x < self.width && y < self.height {
59            let idx = self.index(x, y);
60            self.cells.get(idx)
61        } else {
62            None
63        }
64    }
65
66    /// Get a mutable reference to the cell at (x, y), or `None` if out of bounds.
67    pub fn get_mut(&mut self, x: u16, y: u16) -> Option<&mut Cell> {
68        if x < self.width && y < self.height {
69            let idx = self.index(x, y);
70            self.cells.get_mut(idx)
71        } else {
72            None
73        }
74    }
75
76    /// Set a cell at (x, y). If the cell is wide (width > 1), the next
77    /// cell is automatically set to a continuation cell. No-op if out of bounds.
78    ///
79    /// This method handles wide character edge cases:
80    /// - If writing over a continuation cell, the preceding wide character's
81    ///   primary cell is blanked to avoid leaving a half-rendered glyph.
82    /// - If writing over a wide character's primary cell, the old continuation
83    ///   cell at x+1 is blanked.
84    /// - If a wide character would place its continuation cell beyond the last
85    ///   column, the wide character is replaced with a single space instead.
86    pub fn set(&mut self, x: u16, y: u16, cell: Cell) {
87        if x >= self.width || y >= self.height {
88            return;
89        }
90
91        let is_wide = cell.is_wide();
92
93        // If this is a wide character at the last column (continuation would be out of bounds),
94        // replace with a space instead of placing a half-visible wide character.
95        if is_wide && x + 1 >= self.width {
96            let idx = self.index(x, y);
97            if let Some(c) = self.cells.get_mut(idx) {
98                *c = Cell::blank();
99            }
100            return;
101        }
102
103        // If the cell we are about to overwrite is a continuation cell (width == 0),
104        // blank the preceding cell that was the primary half of the wide character.
105        let idx = self.index(x, y);
106        if let Some(existing) = self.cells.get(idx)
107            && existing.is_continuation()
108            && x > 0
109        {
110            let prev_idx = self.index(x - 1, y);
111            if let Some(prev) = self.cells.get_mut(prev_idx) {
112                *prev = Cell::blank();
113            }
114        }
115
116        // If the cell we are about to overwrite is a wide character (width > 1),
117        // blank the old continuation cell at x+1.
118        if let Some(existing) = self.cells.get(idx)
119            && existing.is_wide()
120        {
121            let next_x = x + 1;
122            if next_x < self.width {
123                let next_idx = self.index(next_x, y);
124                if let Some(cont) = self.cells.get_mut(next_idx) {
125                    *cont = Cell::blank();
126                }
127            }
128        }
129
130        // Write the new cell
131        if let Some(c) = self.cells.get_mut(idx) {
132            *c = cell;
133        }
134
135        // Set continuation cell for wide characters
136        if is_wide {
137            let next_x = x + 1;
138            if next_x < self.width {
139                // If the continuation target is itself a wide character's primary cell,
140                // blank that wide character's continuation cell too.
141                let next_idx = self.index(next_x, y);
142                if let Some(next_cell) = self.cells.get(next_idx)
143                    && next_cell.is_wide()
144                {
145                    let after_next = next_x + 1;
146                    if after_next < self.width {
147                        let after_idx = self.index(after_next, y);
148                        if let Some(after_cell) = self.cells.get_mut(after_idx) {
149                            *after_cell = Cell::blank();
150                        }
151                    }
152                }
153                if let Some(c) = self.cells.get_mut(next_idx) {
154                    *c = Cell::continuation();
155                }
156            }
157        }
158    }
159
160    /// Get a row of cells as a slice.
161    pub fn get_row(&self, y: u16) -> Option<&[Cell]> {
162        if y < self.height {
163            let start = self.index(0, y);
164            let end = start + usize::from(self.width);
165            Some(&self.cells[start..end])
166        } else {
167            None
168        }
169    }
170
171    /// Compute the differences between this buffer and a previous buffer.
172    /// Returns a list of cell changes needed to update the terminal.
173    pub fn diff(&self, previous: &ScreenBuffer) -> Vec<CellChange> {
174        // If sizes differ, emit all non-blank cells as changes (full redraw)
175        if self.width != previous.width || self.height != previous.height {
176            return self.full_diff();
177        }
178
179        let mut changes = Vec::new();
180        for y in 0..self.height {
181            for x in 0..self.width {
182                let idx = self.index(x, y);
183                let current = &self.cells[idx];
184                let prev = &previous.cells[idx];
185                if current != prev {
186                    changes.push(CellChange {
187                        x,
188                        y,
189                        cell: current.clone(),
190                    });
191                }
192            }
193        }
194        changes
195    }
196
197    /// Generate changes for every cell (used when sizes differ).
198    fn full_diff(&self) -> Vec<CellChange> {
199        let mut changes = Vec::new();
200        for y in 0..self.height {
201            for x in 0..self.width {
202                let idx = self.index(x, y);
203                let cell = &self.cells[idx];
204                changes.push(CellChange {
205                    x,
206                    y,
207                    cell: cell.clone(),
208                });
209            }
210        }
211        changes
212    }
213
214    /// Convert (x, y) to a linear index.
215    fn index(&self, x: u16, y: u16) -> usize {
216        usize::from(y) * usize::from(self.width) + usize::from(x)
217    }
218}
219
220/// A single cell change: position + new cell value.
221#[derive(Clone, Debug, PartialEq, Eq)]
222pub struct CellChange {
223    /// Column position.
224    pub x: u16,
225    /// Row position.
226    pub y: u16,
227    /// New cell value.
228    pub cell: Cell,
229}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234    use crate::color::{Color, NamedColor};
235    use crate::style::Style;
236
237    #[test]
238    fn new_buffer_all_blank() {
239        let buf = ScreenBuffer::new(Size::new(10, 5));
240        assert_eq!(buf.width(), 10);
241        assert_eq!(buf.height(), 5);
242        for y in 0..5 {
243            for x in 0..10 {
244                let cell = buf.get(x, y);
245                assert!(cell.is_some());
246                assert!(cell.is_some_and(|c| c.is_blank()));
247            }
248        }
249    }
250
251    #[test]
252    fn set_and_get() {
253        let mut buf = ScreenBuffer::new(Size::new(10, 5));
254        let style = Style::new().fg(Color::Named(NamedColor::Red));
255        let cell = Cell::new("A", style.clone());
256        buf.set(3, 2, cell.clone());
257        let got = buf.get(3, 2);
258        assert_eq!(got, Some(&cell));
259    }
260
261    #[test]
262    fn wide_char_sets_continuation() {
263        let mut buf = ScreenBuffer::new(Size::new(10, 5));
264        let wide = Cell::new("\u{4e16}", Style::default()); // 世 (width 2)
265        buf.set(3, 1, wide.clone());
266        assert_eq!(buf.get(3, 1), Some(&wide));
267        // Next cell should be continuation
268        let cont = buf.get(4, 1);
269        assert!(cont.is_some());
270        assert_eq!(cont.map(|c| c.width), Some(0));
271    }
272
273    #[test]
274    fn wide_char_at_right_edge() {
275        let mut buf = ScreenBuffer::new(Size::new(5, 1));
276        let wide = Cell::new("\u{4e16}", Style::default());
277        // Set at column 4 (last column) — continuation would be at col 5, out of bounds
278        // Wide char should be replaced with a blank space
279        buf.set(4, 0, wide);
280        let cell = buf.get(4, 0);
281        assert!(cell.is_some());
282        match cell {
283            Some(c) => {
284                assert!(c.is_blank(), "Wide char at last column should become blank");
285            }
286            None => unreachable!(),
287        }
288    }
289
290    #[test]
291    fn out_of_bounds_returns_none() {
292        let buf = ScreenBuffer::new(Size::new(5, 3));
293        assert!(buf.get(5, 0).is_none());
294        assert!(buf.get(0, 3).is_none());
295        assert!(buf.get(100, 100).is_none());
296    }
297
298    #[test]
299    fn out_of_bounds_set_is_noop() {
300        let mut buf = ScreenBuffer::new(Size::new(5, 3));
301        buf.set(10, 10, Cell::new("X", Style::default()));
302        // Should not crash
303    }
304
305    #[test]
306    fn get_row() {
307        let buf = ScreenBuffer::new(Size::new(5, 3));
308        let row = buf.get_row(0);
309        assert!(row.is_some());
310        assert_eq!(row.map(|r| r.len()), Some(5));
311        assert!(buf.get_row(3).is_none());
312    }
313
314    #[test]
315    fn clear_resets_all_cells() {
316        let mut buf = ScreenBuffer::new(Size::new(5, 3));
317        buf.set(2, 1, Cell::new("X", Style::new().bold(true)));
318        buf.clear();
319        for y in 0..3 {
320            for x in 0..5 {
321                assert!(buf.get(x, y).is_some_and(|c| c.is_blank()));
322            }
323        }
324    }
325
326    #[test]
327    fn resize_fills_with_blank() {
328        let mut buf = ScreenBuffer::new(Size::new(5, 3));
329        buf.set(2, 1, Cell::new("X", Style::default()));
330        buf.resize(Size::new(10, 8));
331        assert_eq!(buf.width(), 10);
332        assert_eq!(buf.height(), 8);
333        for y in 0..8 {
334            for x in 0..10 {
335                assert!(buf.get(x, y).is_some_and(|c| c.is_blank()));
336            }
337        }
338    }
339
340    #[test]
341    fn diff_no_changes() {
342        let buf1 = ScreenBuffer::new(Size::new(5, 3));
343        let buf2 = ScreenBuffer::new(Size::new(5, 3));
344        let changes = buf1.diff(&buf2);
345        assert!(changes.is_empty());
346    }
347
348    #[test]
349    fn diff_single_change() {
350        let mut current = ScreenBuffer::new(Size::new(5, 3));
351        let previous = ScreenBuffer::new(Size::new(5, 3));
352        current.set(2, 1, Cell::new("A", Style::default()));
353        let changes = current.diff(&previous);
354        assert_eq!(changes.len(), 1);
355        assert_eq!(changes[0].x, 2);
356        assert_eq!(changes[0].y, 1);
357        assert_eq!(changes[0].cell.grapheme, "A");
358    }
359
360    #[test]
361    fn diff_style_change() {
362        let mut current = ScreenBuffer::new(Size::new(5, 3));
363        let mut previous = ScreenBuffer::new(Size::new(5, 3));
364        previous.set(0, 0, Cell::new("A", Style::default()));
365        current.set(0, 0, Cell::new("A", Style::new().bold(true)));
366        let changes = current.diff(&previous);
367        assert_eq!(changes.len(), 1);
368    }
369
370    #[test]
371    fn diff_size_mismatch_full_redraw() {
372        let current = ScreenBuffer::new(Size::new(5, 3));
373        let previous = ScreenBuffer::new(Size::new(10, 8));
374        let changes = current.diff(&previous);
375        // Full redraw = all cells
376        assert_eq!(changes.len(), 15); // 5 * 3
377    }
378
379    #[test]
380    fn diff_wide_char_change() {
381        let mut current = ScreenBuffer::new(Size::new(10, 1));
382        let previous = ScreenBuffer::new(Size::new(10, 1));
383        current.set(3, 0, Cell::new("\u{4e16}", Style::default())); // 世
384        let changes = current.diff(&previous);
385        // Should have 2 changes: the wide char and the continuation
386        assert_eq!(changes.len(), 2);
387    }
388
389    // --- Wide character protection tests ---
390
391    #[test]
392    fn overwrite_continuation_blanks_preceding_wide() {
393        let mut buf = ScreenBuffer::new(Size::new(10, 1));
394        // Place wide char at column 3 (continuation at 4)
395        buf.set(3, 0, Cell::new("\u{4e16}", Style::default()));
396        // Overwrite the continuation cell at column 4 with a narrow char
397        buf.set(4, 0, Cell::new("X", Style::default()));
398        // The preceding wide char at column 3 should now be blank
399        match buf.get(3, 0) {
400            Some(c) => assert!(c.is_blank(), "Preceding wide char should be blanked"),
401            None => unreachable!(),
402        }
403        // Column 4 should have "X"
404        match buf.get(4, 0) {
405            Some(c) => assert_eq!(c.grapheme, "X"),
406            None => unreachable!(),
407        }
408    }
409
410    #[test]
411    fn overwrite_wide_with_narrow_blanks_continuation() {
412        let mut buf = ScreenBuffer::new(Size::new(10, 1));
413        // Place wide char at column 3 (continuation at 4)
414        buf.set(3, 0, Cell::new("\u{4e16}", Style::default()));
415        // Overwrite the wide char primary cell with a narrow char
416        buf.set(3, 0, Cell::new("A", Style::default()));
417        // Column 3 should have "A"
418        match buf.get(3, 0) {
419            Some(c) => assert_eq!(c.grapheme, "A"),
420            None => unreachable!(),
421        }
422        // Old continuation at column 4 should now be blank
423        match buf.get(4, 0) {
424            Some(c) => assert!(c.is_blank(), "Old continuation should be blanked"),
425            None => unreachable!(),
426        }
427    }
428
429    #[test]
430    fn wide_char_last_column_replaced_with_space() {
431        let mut buf = ScreenBuffer::new(Size::new(10, 1));
432        // Column 9 is the last column (width=10)
433        buf.set(9, 0, Cell::new("\u{4e16}", Style::default()));
434        match buf.get(9, 0) {
435            Some(c) => {
436                assert!(c.is_blank(), "Wide char at last column should become space");
437            }
438            None => unreachable!(),
439        }
440    }
441
442    #[test]
443    fn wide_char_second_to_last_fits() {
444        let mut buf = ScreenBuffer::new(Size::new(10, 1));
445        // Column 8, continuation at 9 — fits exactly
446        let wide = Cell::new("\u{4e16}", Style::default());
447        buf.set(8, 0, wide.clone());
448        match buf.get(8, 0) {
449            Some(c) => {
450                assert_eq!(c.grapheme, "\u{4e16}");
451                assert_eq!(c.width, 2);
452            }
453            None => unreachable!(),
454        }
455        match buf.get(9, 0) {
456            Some(c) => assert!(c.is_continuation()),
457            None => unreachable!(),
458        }
459    }
460
461    #[test]
462    fn set_narrow_over_narrow_no_side_effects() {
463        let mut buf = ScreenBuffer::new(Size::new(10, 1));
464        buf.set(3, 0, Cell::new("A", Style::default()));
465        buf.set(3, 0, Cell::new("B", Style::default()));
466        match buf.get(3, 0) {
467            Some(c) => assert_eq!(c.grapheme, "B"),
468            None => unreachable!(),
469        }
470        // Neighbors should be unaffected (blank)
471        match buf.get(2, 0) {
472            Some(c) => assert!(c.is_blank()),
473            None => unreachable!(),
474        }
475        match buf.get(4, 0) {
476            Some(c) => assert!(c.is_blank()),
477            None => unreachable!(),
478        }
479    }
480
481    #[test]
482    fn set_wide_over_wide_old_continuation_cleaned() {
483        let mut buf = ScreenBuffer::new(Size::new(10, 1));
484        // Place first wide char at column 2 (continuation at 3)
485        buf.set(2, 0, Cell::new("\u{4e16}", Style::default()));
486        // Place second wide char at column 2 (new continuation at 3)
487        buf.set(2, 0, Cell::new("\u{754c}", Style::default()));
488        match buf.get(2, 0) {
489            Some(c) => {
490                assert_eq!(c.grapheme, "\u{754c}");
491                assert_eq!(c.width, 2);
492            }
493            None => unreachable!(),
494        }
495        match buf.get(3, 0) {
496            Some(c) => assert!(c.is_continuation()),
497            None => unreachable!(),
498        }
499    }
500
501    #[test]
502    fn multiple_wide_chars_in_sequence() {
503        let mut buf = ScreenBuffer::new(Size::new(10, 1));
504        // Place three wide chars: 0-1, 2-3, 4-5
505        buf.set(0, 0, Cell::new("\u{4e16}", Style::default())); // 世
506        buf.set(2, 0, Cell::new("\u{754c}", Style::default())); // 界
507        buf.set(4, 0, Cell::new("\u{4eba}", Style::default())); // 人
508
509        for col in [0, 2, 4] {
510            match buf.get(col, 0) {
511                Some(c) => assert_eq!(c.width, 2),
512                None => unreachable!(),
513            }
514        }
515        for col in [1, 3, 5] {
516            match buf.get(col, 0) {
517                Some(c) => assert!(c.is_continuation()),
518                None => unreachable!(),
519            }
520        }
521    }
522
523    #[test]
524    fn overwrite_middle_of_adjacent_wide_chars() {
525        let mut buf = ScreenBuffer::new(Size::new(10, 1));
526        // Place wide chars at 0-1 and 2-3
527        buf.set(0, 0, Cell::new("\u{4e16}", Style::default()));
528        buf.set(2, 0, Cell::new("\u{754c}", Style::default()));
529        // Overwrite column 1 (continuation of first wide) with narrow char
530        buf.set(1, 0, Cell::new("X", Style::default()));
531        // First wide char at 0 should be blanked
532        match buf.get(0, 0) {
533            Some(c) => assert!(c.is_blank(), "First wide char should be blanked"),
534            None => unreachable!(),
535        }
536        // Column 1 should have "X"
537        match buf.get(1, 0) {
538            Some(c) => assert_eq!(c.grapheme, "X"),
539            None => unreachable!(),
540        }
541        // Second wide char at 2 should be unaffected
542        match buf.get(2, 0) {
543            Some(c) => {
544                assert_eq!(c.grapheme, "\u{754c}");
545                assert_eq!(c.width, 2);
546            }
547            None => unreachable!(),
548        }
549    }
550
551    #[test]
552    fn wide_char_at_column_zero() {
553        let mut buf = ScreenBuffer::new(Size::new(10, 1));
554        buf.set(0, 0, Cell::new("\u{4e16}", Style::default()));
555        match buf.get(0, 0) {
556            Some(c) => {
557                assert_eq!(c.grapheme, "\u{4e16}");
558                assert_eq!(c.width, 2);
559            }
560            None => unreachable!(),
561        }
562        match buf.get(1, 0) {
563            Some(c) => assert!(c.is_continuation()),
564            None => unreachable!(),
565        }
566    }
567
568    #[test]
569    fn wide_char_continuation_exactly_at_last_column() {
570        // Buffer width 6: wide char at column 4, continuation at column 5 (last column) — fits
571        let mut buf = ScreenBuffer::new(Size::new(6, 1));
572        buf.set(4, 0, Cell::new("\u{4e16}", Style::default()));
573        match buf.get(4, 0) {
574            Some(c) => {
575                assert_eq!(c.grapheme, "\u{4e16}");
576                assert_eq!(c.width, 2);
577            }
578            None => unreachable!(),
579        }
580        match buf.get(5, 0) {
581            Some(c) => assert!(c.is_continuation()),
582            None => unreachable!(),
583        }
584    }
585
586    // --- Task 6: Unicode buffer reading tests ---
587
588    #[test]
589    fn get_row_with_cjk_primary_and_continuation() {
590        // Write 3 CJK chars: each width 2 => 6 cells total (3 primary + 3 continuation)
591        let mut buf = ScreenBuffer::new(Size::new(10, 1));
592        buf.set(0, 0, Cell::new("\u{4e16}", Style::default())); // 世
593        buf.set(2, 0, Cell::new("\u{754c}", Style::default())); // 界
594        buf.set(4, 0, Cell::new("\u{4eba}", Style::default())); // 人
595
596        let row = buf.get_row(0);
597        assert!(row.is_some());
598        match row {
599            Some(cells) => {
600                assert_eq!(cells.len(), 10);
601                // Primary cells at 0, 2, 4
602                assert_eq!(cells[0].grapheme, "\u{4e16}");
603                assert_eq!(cells[0].width, 2);
604                assert_eq!(cells[2].grapheme, "\u{754c}");
605                assert_eq!(cells[2].width, 2);
606                assert_eq!(cells[4].grapheme, "\u{4eba}");
607                assert_eq!(cells[4].width, 2);
608                // Continuation cells at 1, 3, 5
609                assert!(cells[1].is_continuation());
610                assert!(cells[3].is_continuation());
611                assert!(cells[5].is_continuation());
612                // Remaining cells are blank
613                assert!(cells[6].is_blank());
614                assert!(cells[7].is_blank());
615            }
616            None => unreachable!(),
617        }
618    }
619
620    #[test]
621    fn diff_with_wide_char_produces_two_change_entries() {
622        let mut current = ScreenBuffer::new(Size::new(10, 1));
623        let previous = ScreenBuffer::new(Size::new(10, 1));
624        // Write two CJK chars at columns 0 and 4
625        current.set(0, 0, Cell::new("\u{4e16}", Style::default()));
626        current.set(4, 0, Cell::new("\u{754c}", Style::default()));
627        let changes = current.diff(&previous);
628        // Each wide char produces 2 changes (primary + continuation)
629        assert_eq!(changes.len(), 4);
630        // First wide char: change at x=0 and x=1
631        assert_eq!(changes[0].x, 0);
632        assert_eq!(changes[0].cell.width, 2);
633        assert_eq!(changes[1].x, 1);
634        assert_eq!(changes[1].cell.width, 0); // continuation
635        // Second wide char: change at x=4 and x=5
636        assert_eq!(changes[2].x, 4);
637        assert_eq!(changes[2].cell.width, 2);
638        assert_eq!(changes[3].x, 5);
639        assert_eq!(changes[3].cell.width, 0); // continuation
640    }
641
642    #[test]
643    fn clear_after_wide_char_writes_all_blank() {
644        let mut buf = ScreenBuffer::new(Size::new(10, 2));
645        // Write wide chars
646        buf.set(0, 0, Cell::new("\u{4e16}", Style::default()));
647        buf.set(2, 0, Cell::new("\u{754c}", Style::default()));
648        buf.set(0, 1, Cell::new("\u{1f600}", Style::default())); // emoji
649        // Verify something is there
650        match buf.get(0, 0) {
651            Some(c) => assert!(!c.is_blank()),
652            None => unreachable!(),
653        }
654        // Clear
655        buf.clear();
656        // All cells should be blank
657        for y in 0..2 {
658            for x in 0..10 {
659                match buf.get(x, y) {
660                    Some(c) => assert!(c.is_blank(), "Cell ({x},{y}) should be blank after clear"),
661                    None => unreachable!(),
662                }
663            }
664        }
665    }
666}