Skip to main content

presentar_terminal/direct/
cell_buffer.rs

1//! Cell buffer with zero-allocation steady state.
2//!
3//! Uses `CompactString` to inline small strings (≤24 bytes), avoiding
4//! heap allocations for typical terminal content.
5
6use bitvec::prelude::*;
7use compact_str::CompactString;
8use presentar_core::Color;
9use unicode_width::UnicodeWidthStr;
10
11/// Text modifiers for terminal cells.
12#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
13pub struct Modifiers(u8);
14
15impl Modifiers {
16    /// No modifiers.
17    pub const NONE: Self = Self(0);
18    /// Bold text.
19    pub const BOLD: Self = Self(1 << 0);
20    /// Italic text.
21    pub const ITALIC: Self = Self(1 << 1);
22    /// Underlined text.
23    pub const UNDERLINE: Self = Self(1 << 2);
24    /// Strikethrough text.
25    pub const STRIKETHROUGH: Self = Self(1 << 3);
26    /// Dim/faint text.
27    pub const DIM: Self = Self(1 << 4);
28    /// Blinking text.
29    pub const BLINK: Self = Self(1 << 5);
30    /// Reversed colors.
31    pub const REVERSE: Self = Self(1 << 6);
32    /// Hidden text.
33    pub const HIDDEN: Self = Self(1 << 7);
34
35    /// Create empty modifiers.
36    #[must_use]
37    pub const fn empty() -> Self {
38        Self::NONE
39    }
40
41    /// Check if modifiers is empty.
42    #[must_use]
43    pub const fn is_empty(self) -> bool {
44        self.0 == 0
45    }
46
47    /// Check if a specific modifier is set.
48    #[must_use]
49    pub const fn contains(self, other: Self) -> bool {
50        (self.0 & other.0) == other.0
51    }
52
53    /// Add a modifier.
54    #[must_use]
55    pub const fn with(self, other: Self) -> Self {
56        Self(self.0 | other.0)
57    }
58
59    /// Remove a modifier.
60    #[must_use]
61    pub const fn without(self, other: Self) -> Self {
62        Self(self.0 & !other.0)
63    }
64
65    /// Get raw bits.
66    #[must_use]
67    pub const fn bits(self) -> u8 {
68        self.0
69    }
70
71    /// Create from raw bits.
72    #[must_use]
73    pub const fn from_bits(bits: u8) -> Self {
74        Self(bits)
75    }
76}
77
78impl std::ops::BitOr for Modifiers {
79    type Output = Self;
80
81    fn bitor(self, rhs: Self) -> Self::Output {
82        Self(self.0 | rhs.0)
83    }
84}
85
86impl std::ops::BitOrAssign for Modifiers {
87    fn bitor_assign(&mut self, rhs: Self) {
88        self.0 |= rhs.0;
89    }
90}
91
92impl std::ops::BitAnd for Modifiers {
93    type Output = Self;
94
95    fn bitand(self, rhs: Self) -> Self::Output {
96        Self(self.0 & rhs.0)
97    }
98}
99
100/// A single terminal cell.
101///
102/// Uses `CompactString` for zero-allocation storage of typical graphemes.
103/// Memory layout is optimized for cache efficiency (40 bytes total).
104#[derive(Clone, Debug, PartialEq)]
105pub struct Cell {
106    /// The symbol displayed in this cell (inlined for ≤24 bytes).
107    pub symbol: CompactString,
108    /// Foreground color.
109    pub fg: Color,
110    /// Background color.
111    pub bg: Color,
112    /// Text modifiers.
113    pub modifiers: Modifiers,
114    /// Display width of the symbol (1 for normal, 2 for wide chars, 0 for continuation).
115    width: u8,
116}
117
118impl Default for Cell {
119    fn default() -> Self {
120        Self {
121            symbol: CompactString::const_new(" "),
122            fg: Color::WHITE,
123            // Use transparent background so unpainted areas don't show black
124            bg: Color::TRANSPARENT,
125            modifiers: Modifiers::NONE,
126            width: 1,
127        }
128    }
129}
130
131impl Cell {
132    /// Create a new cell with the given content.
133    #[must_use]
134    pub fn new(symbol: &str, fg: Color, bg: Color, modifiers: Modifiers) -> Self {
135        let width = UnicodeWidthStr::width(symbol).min(255) as u8;
136        Self {
137            symbol: CompactString::new(symbol),
138            fg,
139            bg,
140            modifiers,
141            width: width.max(1),
142        }
143    }
144
145    /// Update the cell content (zero-allocation for small strings).
146    pub fn update(&mut self, symbol: &str, fg: Color, bg: Color, modifiers: Modifiers) {
147        self.symbol.clear();
148        self.symbol.push_str(symbol);
149        self.fg = fg;
150        self.bg = bg;
151        self.modifiers = modifiers;
152        self.width = UnicodeWidthStr::width(symbol).clamp(1, 255) as u8;
153    }
154
155    /// Mark this cell as a continuation of a wide character.
156    pub fn make_continuation(&mut self) {
157        self.symbol.clear();
158        self.width = 0;
159    }
160
161    /// Check if this is a continuation cell.
162    #[must_use]
163    pub const fn is_continuation(&self) -> bool {
164        self.width == 0
165    }
166
167    /// Get the display width of this cell.
168    #[must_use]
169    pub const fn width(&self) -> u8 {
170        self.width
171    }
172
173    /// Reset to default (space with transparent background).
174    pub fn reset(&mut self) {
175        self.symbol.clear();
176        self.symbol.push(' ');
177        self.fg = Color::WHITE;
178        self.bg = Color::TRANSPARENT;
179        self.modifiers = Modifiers::NONE;
180        self.width = 1;
181    }
182}
183
184/// Buffer of terminal cells with dirty tracking.
185///
186/// Memory footprint for 80×24 terminal: ~75KB
187/// (1920 cells × 40 bytes per cell ≈ 76KB)
188#[derive(Debug)]
189pub struct CellBuffer {
190    /// The cell storage.
191    cells: Vec<Cell>,
192    /// Terminal width.
193    width: u16,
194    /// Terminal height.
195    height: u16,
196    /// Dirty bit per cell (1 bit per cell).
197    dirty: BitVec,
198}
199
200impl CellBuffer {
201    /// Create a new buffer with the given dimensions.
202    #[must_use]
203    pub fn new(width: u16, height: u16) -> Self {
204        let size = (width as usize) * (height as usize);
205        Self {
206            cells: vec![Cell::default(); size],
207            width,
208            height,
209            dirty: bitvec![0; size],
210        }
211    }
212
213    /// Get the buffer width.
214    #[must_use]
215    pub const fn width(&self) -> u16 {
216        self.width
217    }
218
219    /// Get the buffer height.
220    #[must_use]
221    pub const fn height(&self) -> u16 {
222        self.height
223    }
224
225    /// Get total cell count.
226    #[must_use]
227    pub fn len(&self) -> usize {
228        self.cells.len()
229    }
230
231    /// Check if buffer is empty.
232    #[must_use]
233    pub fn is_empty(&self) -> bool {
234        self.cells.is_empty()
235    }
236
237    /// Convert (x, y) to linear index.
238    #[must_use]
239    pub fn index(&self, x: u16, y: u16) -> usize {
240        debug_assert!(x < self.width, "x coordinate out of bounds");
241        debug_assert!(y < self.height, "y coordinate out of bounds");
242        (y as usize) * (self.width as usize) + (x as usize)
243    }
244
245    /// Convert linear index to (x, y).
246    #[must_use]
247    pub fn coords(&self, idx: usize) -> (u16, u16) {
248        debug_assert!(idx < self.cells.len(), "index out of bounds");
249        let x = (idx % (self.width as usize)) as u16;
250        let y = (idx / (self.width as usize)) as u16;
251        debug_assert!(
252            x < self.width && y < self.height,
253            "coords must be in bounds"
254        );
255        (x, y)
256    }
257
258    /// Get a cell reference.
259    #[must_use]
260    pub fn get(&self, x: u16, y: u16) -> Option<&Cell> {
261        if x < self.width && y < self.height {
262            Some(&self.cells[self.index(x, y)])
263        } else {
264            None
265        }
266    }
267
268    /// Get a mutable cell reference.
269    pub fn get_mut(&mut self, x: u16, y: u16) -> Option<&mut Cell> {
270        if x < self.width && y < self.height {
271            let idx = self.index(x, y);
272            Some(&mut self.cells[idx])
273        } else {
274            None
275        }
276    }
277
278    /// Set a cell and mark it dirty.
279    pub fn set(&mut self, x: u16, y: u16, cell: Cell) {
280        if x < self.width && y < self.height {
281            let idx = self.index(x, y);
282            self.cells[idx] = cell;
283            self.dirty.set(idx, true);
284        }
285    }
286
287    /// Update a cell's content and mark it dirty.
288    pub fn update(
289        &mut self,
290        x: u16,
291        y: u16,
292        symbol: &str,
293        fg: Color,
294        bg: Color,
295        modifiers: Modifiers,
296    ) {
297        if x < self.width && y < self.height {
298            let idx = self.index(x, y);
299            self.cells[idx].update(symbol, fg, bg, modifiers);
300            self.dirty.set(idx, true);
301        }
302    }
303
304    /// Mark a cell as dirty.
305    pub fn mark_dirty(&mut self, x: u16, y: u16) {
306        if x < self.width && y < self.height {
307            let idx = self.index(x, y);
308            self.dirty.set(idx, true);
309        }
310    }
311
312    /// Mark all cells as dirty (for full redraw).
313    pub fn mark_all_dirty(&mut self) {
314        self.dirty.fill(true);
315    }
316
317    /// Clear dirty flags.
318    pub fn clear_dirty(&mut self) {
319        self.dirty.fill(false);
320    }
321
322    /// Count dirty cells.
323    #[must_use]
324    pub fn dirty_count(&self) -> usize {
325        self.dirty.count_ones()
326    }
327
328    /// Iterate over dirty cell indices.
329    pub fn iter_dirty(&self) -> impl Iterator<Item = usize> + '_ {
330        self.dirty.iter_ones()
331    }
332
333    /// Get cells slice.
334    #[must_use]
335    pub fn cells(&self) -> &[Cell] {
336        &self.cells
337    }
338
339    /// Get cells mutable slice.
340    pub fn cells_mut(&mut self) -> &mut [Cell] {
341        &mut self.cells
342    }
343
344    /// Resize the buffer (clears all content).
345    pub fn resize(&mut self, width: u16, height: u16) {
346        let size = (width as usize) * (height as usize);
347        self.width = width;
348        self.height = height;
349        self.cells.clear();
350        self.cells.resize(size, Cell::default());
351        self.dirty = bitvec![0; size];
352        self.mark_all_dirty();
353    }
354
355    /// Clear the buffer (reset all cells to default).
356    pub fn clear(&mut self) {
357        for cell in &mut self.cells {
358            cell.reset();
359        }
360        self.mark_all_dirty();
361    }
362
363    /// Fill a rectangular region.
364    pub fn fill_rect(&mut self, x: u16, y: u16, width: u16, height: u16, fg: Color, bg: Color) {
365        let x_end = (x + width).min(self.width);
366        let y_end = (y + height).min(self.height);
367
368        for cy in y..y_end {
369            for cx in x..x_end {
370                self.update(cx, cy, " ", fg, bg, Modifiers::NONE);
371            }
372        }
373    }
374
375    /// Set a single character at the given position (keeps existing colors/modifiers).
376    pub fn set_char(&mut self, x: u16, y: u16, ch: char) {
377        if let Some(cell) = self.get_mut(x, y) {
378            let mut buf = [0u8; 4];
379            let s = ch.encode_utf8(&mut buf);
380            cell.symbol = CompactString::from(&*s);
381            self.mark_dirty(x, y);
382        }
383    }
384
385    /// Write a string starting at the given position (keeps existing colors/modifiers).
386    pub fn write_str(&mut self, x: u16, y: u16, s: &str) {
387        let mut cx = x;
388        for ch in s.chars() {
389            self.set_char(cx, y, ch);
390            cx = cx.saturating_add(1);
391            if cx >= self.width {
392                break;
393            }
394        }
395    }
396}
397
398#[cfg(test)]
399mod tests {
400    use super::*;
401
402    #[test]
403    fn test_modifiers_empty() {
404        let m = Modifiers::empty();
405        assert!(m.is_empty());
406        assert_eq!(m.bits(), 0);
407    }
408
409    #[test]
410    fn test_modifiers_with() {
411        let m = Modifiers::NONE.with(Modifiers::BOLD);
412        assert!(m.contains(Modifiers::BOLD));
413        assert!(!m.contains(Modifiers::ITALIC));
414    }
415
416    #[test]
417    fn test_modifiers_without() {
418        let m = Modifiers::BOLD.with(Modifiers::ITALIC);
419        let m2 = m.without(Modifiers::BOLD);
420        assert!(!m2.contains(Modifiers::BOLD));
421        assert!(m2.contains(Modifiers::ITALIC));
422    }
423
424    #[test]
425    fn test_modifiers_bitor() {
426        let m = Modifiers::BOLD | Modifiers::ITALIC;
427        assert!(m.contains(Modifiers::BOLD));
428        assert!(m.contains(Modifiers::ITALIC));
429    }
430
431    #[test]
432    fn test_modifiers_bitor_assign() {
433        let mut m = Modifiers::BOLD;
434        m |= Modifiers::ITALIC;
435        assert!(m.contains(Modifiers::BOLD));
436        assert!(m.contains(Modifiers::ITALIC));
437    }
438
439    #[test]
440    fn test_modifiers_bitand() {
441        let m1 = Modifiers::BOLD | Modifiers::ITALIC;
442        let m2 = Modifiers::BOLD | Modifiers::UNDERLINE;
443        let m3 = m1 & m2;
444        assert!(m3.contains(Modifiers::BOLD));
445        assert!(!m3.contains(Modifiers::ITALIC));
446    }
447
448    #[test]
449    fn test_modifiers_from_bits() {
450        let m = Modifiers::from_bits(0b0000_0011);
451        assert!(m.contains(Modifiers::BOLD));
452        assert!(m.contains(Modifiers::ITALIC));
453    }
454
455    #[test]
456    fn test_cell_default() {
457        let cell = Cell::default();
458        assert_eq!(cell.symbol.as_str(), " ");
459        assert_eq!(cell.fg, Color::WHITE);
460        assert_eq!(cell.bg, Color::TRANSPARENT);
461        assert_eq!(cell.modifiers, Modifiers::NONE);
462        assert_eq!(cell.width(), 1);
463    }
464
465    #[test]
466    fn test_cell_new() {
467        let cell = Cell::new("A", Color::RED, Color::BLUE, Modifiers::BOLD);
468        assert_eq!(cell.symbol.as_str(), "A");
469        assert_eq!(cell.fg, Color::RED);
470        assert_eq!(cell.bg, Color::BLUE);
471        assert!(cell.modifiers.contains(Modifiers::BOLD));
472        assert_eq!(cell.width(), 1);
473    }
474
475    #[test]
476    fn test_cell_wide_char() {
477        let cell = Cell::new("日", Color::WHITE, Color::BLACK, Modifiers::NONE);
478        assert_eq!(cell.width(), 2);
479    }
480
481    #[test]
482    fn test_cell_update() {
483        let mut cell = Cell::default();
484        cell.update("X", Color::GREEN, Color::YELLOW, Modifiers::ITALIC);
485        assert_eq!(cell.symbol.as_str(), "X");
486        assert_eq!(cell.fg, Color::GREEN);
487        assert_eq!(cell.bg, Color::YELLOW);
488        assert!(cell.modifiers.contains(Modifiers::ITALIC));
489    }
490
491    #[test]
492    fn test_cell_continuation() {
493        let mut cell = Cell::new("日", Color::WHITE, Color::BLACK, Modifiers::NONE);
494        cell.make_continuation();
495        assert!(cell.is_continuation());
496        assert_eq!(cell.width(), 0);
497    }
498
499    #[test]
500    fn test_cell_reset() {
501        let mut cell = Cell::new("X", Color::RED, Color::BLUE, Modifiers::BOLD);
502        cell.reset();
503        assert_eq!(cell.symbol.as_str(), " ");
504        assert_eq!(cell.fg, Color::WHITE);
505        assert_eq!(cell.bg, Color::TRANSPARENT);
506        assert!(cell.modifiers.is_empty());
507    }
508
509    #[test]
510    fn test_buffer_creation() {
511        let buf = CellBuffer::new(80, 24);
512        assert_eq!(buf.width(), 80);
513        assert_eq!(buf.height(), 24);
514        assert_eq!(buf.len(), 1920);
515        assert!(!buf.is_empty());
516    }
517
518    #[test]
519    fn test_buffer_empty() {
520        let buf = CellBuffer::new(0, 0);
521        assert!(buf.is_empty());
522    }
523
524    #[test]
525    fn test_buffer_index() {
526        let buf = CellBuffer::new(10, 5);
527        assert_eq!(buf.index(0, 0), 0);
528        assert_eq!(buf.index(5, 0), 5);
529        assert_eq!(buf.index(0, 1), 10);
530        assert_eq!(buf.index(5, 2), 25);
531    }
532
533    #[test]
534    fn test_buffer_coords() {
535        let buf = CellBuffer::new(10, 5);
536        assert_eq!(buf.coords(0), (0, 0));
537        assert_eq!(buf.coords(5), (5, 0));
538        assert_eq!(buf.coords(10), (0, 1));
539        assert_eq!(buf.coords(25), (5, 2));
540    }
541
542    #[test]
543    fn test_buffer_get() {
544        let buf = CellBuffer::new(10, 5);
545        assert!(buf.get(0, 0).is_some());
546        assert!(buf.get(9, 4).is_some());
547        assert!(buf.get(10, 0).is_none());
548        assert!(buf.get(0, 5).is_none());
549    }
550
551    #[test]
552    fn test_buffer_get_mut() {
553        let mut buf = CellBuffer::new(10, 5);
554        assert!(buf.get_mut(0, 0).is_some());
555        assert!(buf.get_mut(10, 0).is_none());
556    }
557
558    #[test]
559    fn test_buffer_set() {
560        let mut buf = CellBuffer::new(10, 5);
561        let cell = Cell::new("X", Color::RED, Color::BLUE, Modifiers::NONE);
562        buf.set(5, 2, cell);
563
564        let retrieved = buf.get(5, 2).unwrap();
565        assert_eq!(retrieved.symbol.as_str(), "X");
566        assert!(buf.dirty_count() > 0);
567    }
568
569    #[test]
570    fn test_buffer_set_out_of_bounds() {
571        let mut buf = CellBuffer::new(10, 5);
572        let cell = Cell::new("X", Color::RED, Color::BLUE, Modifiers::NONE);
573        buf.set(100, 100, cell); // Should not panic
574    }
575
576    #[test]
577    fn test_buffer_update() {
578        let mut buf = CellBuffer::new(10, 5);
579        buf.update(3, 3, "Y", Color::GREEN, Color::BLACK, Modifiers::BOLD);
580
581        let cell = buf.get(3, 3).unwrap();
582        assert_eq!(cell.symbol.as_str(), "Y");
583        assert_eq!(cell.fg, Color::GREEN);
584    }
585
586    #[test]
587    fn test_buffer_dirty_tracking() {
588        let mut buf = CellBuffer::new(10, 5);
589        assert_eq!(buf.dirty_count(), 0);
590
591        buf.mark_dirty(0, 0);
592        assert_eq!(buf.dirty_count(), 1);
593
594        buf.mark_all_dirty();
595        assert_eq!(buf.dirty_count(), 50);
596
597        buf.clear_dirty();
598        assert_eq!(buf.dirty_count(), 0);
599    }
600
601    #[test]
602    fn test_buffer_iter_dirty() {
603        let mut buf = CellBuffer::new(10, 5);
604        buf.mark_dirty(1, 1);
605        buf.mark_dirty(3, 3);
606
607        let dirty: Vec<usize> = buf.iter_dirty().collect();
608        assert_eq!(dirty.len(), 2);
609        assert!(dirty.contains(&buf.index(1, 1)));
610        assert!(dirty.contains(&buf.index(3, 3)));
611    }
612
613    #[test]
614    fn test_buffer_resize() {
615        let mut buf = CellBuffer::new(10, 5);
616        buf.update(0, 0, "X", Color::RED, Color::BLACK, Modifiers::NONE);
617
618        buf.resize(20, 10);
619        assert_eq!(buf.width(), 20);
620        assert_eq!(buf.height(), 10);
621        assert_eq!(buf.len(), 200);
622        // Content should be cleared
623        assert_eq!(buf.get(0, 0).unwrap().symbol.as_str(), " ");
624        // All should be dirty after resize
625        assert_eq!(buf.dirty_count(), 200);
626    }
627
628    #[test]
629    fn test_buffer_clear() {
630        let mut buf = CellBuffer::new(10, 5);
631        buf.update(0, 0, "X", Color::RED, Color::BLACK, Modifiers::BOLD);
632        buf.clear_dirty();
633
634        buf.clear();
635        let cell = buf.get(0, 0).unwrap();
636        assert_eq!(cell.symbol.as_str(), " ");
637        assert!(cell.modifiers.is_empty());
638        assert_eq!(buf.dirty_count(), 50);
639    }
640
641    #[test]
642    fn test_buffer_fill_rect() {
643        let mut buf = CellBuffer::new(10, 10);
644        buf.fill_rect(2, 2, 3, 3, Color::WHITE, Color::RED);
645
646        // Inside rect
647        assert_eq!(buf.get(3, 3).unwrap().bg, Color::RED);
648        // Outside rect - default is TRANSPARENT
649        assert_eq!(buf.get(0, 0).unwrap().bg, Color::TRANSPARENT);
650    }
651
652    #[test]
653    fn test_buffer_fill_rect_clipped() {
654        let mut buf = CellBuffer::new(10, 10);
655        buf.fill_rect(8, 8, 5, 5, Color::WHITE, Color::BLUE);
656
657        // Should be clipped to buffer bounds
658        assert_eq!(buf.get(9, 9).unwrap().bg, Color::BLUE);
659    }
660
661    #[test]
662    fn test_buffer_cells_access() {
663        let mut buf = CellBuffer::new(10, 5);
664        assert_eq!(buf.cells().len(), 50);
665        assert_eq!(buf.cells_mut().len(), 50);
666    }
667
668    #[test]
669    fn test_cell_empty_string() {
670        let cell = Cell::new("", Color::WHITE, Color::BLACK, Modifiers::NONE);
671        // Width should be at least 1
672        assert_eq!(cell.width(), 1);
673    }
674
675    #[test]
676    fn test_modifiers_all_flags() {
677        let all = Modifiers::BOLD
678            | Modifiers::ITALIC
679            | Modifiers::UNDERLINE
680            | Modifiers::STRIKETHROUGH
681            | Modifiers::DIM
682            | Modifiers::BLINK
683            | Modifiers::REVERSE
684            | Modifiers::HIDDEN;
685
686        assert!(all.contains(Modifiers::BOLD));
687        assert!(all.contains(Modifiers::ITALIC));
688        assert!(all.contains(Modifiers::UNDERLINE));
689        assert!(all.contains(Modifiers::STRIKETHROUGH));
690        assert!(all.contains(Modifiers::DIM));
691        assert!(all.contains(Modifiers::BLINK));
692        assert!(all.contains(Modifiers::REVERSE));
693        assert!(all.contains(Modifiers::HIDDEN));
694    }
695}