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)]
399#[allow(clippy::unwrap_used, clippy::disallowed_methods)]
400mod tests {
401    use super::*;
402
403    #[test]
404    fn test_modifiers_empty() {
405        let m = Modifiers::empty();
406        assert!(m.is_empty());
407        assert_eq!(m.bits(), 0);
408    }
409
410    #[test]
411    fn test_modifiers_with() {
412        let m = Modifiers::NONE.with(Modifiers::BOLD);
413        assert!(m.contains(Modifiers::BOLD));
414        assert!(!m.contains(Modifiers::ITALIC));
415    }
416
417    #[test]
418    fn test_modifiers_without() {
419        let m = Modifiers::BOLD.with(Modifiers::ITALIC);
420        let m2 = m.without(Modifiers::BOLD);
421        assert!(!m2.contains(Modifiers::BOLD));
422        assert!(m2.contains(Modifiers::ITALIC));
423    }
424
425    #[test]
426    fn test_modifiers_bitor() {
427        let m = Modifiers::BOLD | Modifiers::ITALIC;
428        assert!(m.contains(Modifiers::BOLD));
429        assert!(m.contains(Modifiers::ITALIC));
430    }
431
432    #[test]
433    fn test_modifiers_bitor_assign() {
434        let mut m = Modifiers::BOLD;
435        m |= Modifiers::ITALIC;
436        assert!(m.contains(Modifiers::BOLD));
437        assert!(m.contains(Modifiers::ITALIC));
438    }
439
440    #[test]
441    fn test_modifiers_bitand() {
442        let m1 = Modifiers::BOLD | Modifiers::ITALIC;
443        let m2 = Modifiers::BOLD | Modifiers::UNDERLINE;
444        let m3 = m1 & m2;
445        assert!(m3.contains(Modifiers::BOLD));
446        assert!(!m3.contains(Modifiers::ITALIC));
447    }
448
449    #[test]
450    fn test_modifiers_from_bits() {
451        let m = Modifiers::from_bits(0b0000_0011);
452        assert!(m.contains(Modifiers::BOLD));
453        assert!(m.contains(Modifiers::ITALIC));
454    }
455
456    #[test]
457    fn test_cell_default() {
458        let cell = Cell::default();
459        assert_eq!(cell.symbol.as_str(), " ");
460        assert_eq!(cell.fg, Color::WHITE);
461        assert_eq!(cell.bg, Color::TRANSPARENT);
462        assert_eq!(cell.modifiers, Modifiers::NONE);
463        assert_eq!(cell.width(), 1);
464    }
465
466    #[test]
467    fn test_cell_new() {
468        let cell = Cell::new("A", Color::RED, Color::BLUE, Modifiers::BOLD);
469        assert_eq!(cell.symbol.as_str(), "A");
470        assert_eq!(cell.fg, Color::RED);
471        assert_eq!(cell.bg, Color::BLUE);
472        assert!(cell.modifiers.contains(Modifiers::BOLD));
473        assert_eq!(cell.width(), 1);
474    }
475
476    #[test]
477    fn test_cell_wide_char() {
478        let cell = Cell::new("日", Color::WHITE, Color::BLACK, Modifiers::NONE);
479        assert_eq!(cell.width(), 2);
480    }
481
482    #[test]
483    fn test_cell_update() {
484        let mut cell = Cell::default();
485        cell.update("X", Color::GREEN, Color::YELLOW, Modifiers::ITALIC);
486        assert_eq!(cell.symbol.as_str(), "X");
487        assert_eq!(cell.fg, Color::GREEN);
488        assert_eq!(cell.bg, Color::YELLOW);
489        assert!(cell.modifiers.contains(Modifiers::ITALIC));
490    }
491
492    #[test]
493    fn test_cell_continuation() {
494        let mut cell = Cell::new("日", Color::WHITE, Color::BLACK, Modifiers::NONE);
495        cell.make_continuation();
496        assert!(cell.is_continuation());
497        assert_eq!(cell.width(), 0);
498    }
499
500    #[test]
501    fn test_cell_reset() {
502        let mut cell = Cell::new("X", Color::RED, Color::BLUE, Modifiers::BOLD);
503        cell.reset();
504        assert_eq!(cell.symbol.as_str(), " ");
505        assert_eq!(cell.fg, Color::WHITE);
506        assert_eq!(cell.bg, Color::TRANSPARENT);
507        assert!(cell.modifiers.is_empty());
508    }
509
510    #[test]
511    fn test_buffer_creation() {
512        let buf = CellBuffer::new(80, 24);
513        assert_eq!(buf.width(), 80);
514        assert_eq!(buf.height(), 24);
515        assert_eq!(buf.len(), 1920);
516        assert!(!buf.is_empty());
517    }
518
519    #[test]
520    fn test_buffer_empty() {
521        let buf = CellBuffer::new(0, 0);
522        assert!(buf.is_empty());
523    }
524
525    #[test]
526    fn test_buffer_index() {
527        let buf = CellBuffer::new(10, 5);
528        assert_eq!(buf.index(0, 0), 0);
529        assert_eq!(buf.index(5, 0), 5);
530        assert_eq!(buf.index(0, 1), 10);
531        assert_eq!(buf.index(5, 2), 25);
532    }
533
534    #[test]
535    fn test_buffer_coords() {
536        let buf = CellBuffer::new(10, 5);
537        assert_eq!(buf.coords(0), (0, 0));
538        assert_eq!(buf.coords(5), (5, 0));
539        assert_eq!(buf.coords(10), (0, 1));
540        assert_eq!(buf.coords(25), (5, 2));
541    }
542
543    #[test]
544    fn test_buffer_get() {
545        let buf = CellBuffer::new(10, 5);
546        assert!(buf.get(0, 0).is_some());
547        assert!(buf.get(9, 4).is_some());
548        assert!(buf.get(10, 0).is_none());
549        assert!(buf.get(0, 5).is_none());
550    }
551
552    #[test]
553    fn test_buffer_get_mut() {
554        let mut buf = CellBuffer::new(10, 5);
555        assert!(buf.get_mut(0, 0).is_some());
556        assert!(buf.get_mut(10, 0).is_none());
557    }
558
559    #[test]
560    fn test_buffer_set() {
561        let mut buf = CellBuffer::new(10, 5);
562        let cell = Cell::new("X", Color::RED, Color::BLUE, Modifiers::NONE);
563        buf.set(5, 2, cell);
564
565        let retrieved = buf.get(5, 2).unwrap();
566        assert_eq!(retrieved.symbol.as_str(), "X");
567        assert!(buf.dirty_count() > 0);
568    }
569
570    #[test]
571    fn test_buffer_set_out_of_bounds() {
572        let mut buf = CellBuffer::new(10, 5);
573        let cell = Cell::new("X", Color::RED, Color::BLUE, Modifiers::NONE);
574        buf.set(100, 100, cell); // Should not panic
575    }
576
577    #[test]
578    fn test_buffer_update() {
579        let mut buf = CellBuffer::new(10, 5);
580        buf.update(3, 3, "Y", Color::GREEN, Color::BLACK, Modifiers::BOLD);
581
582        let cell = buf.get(3, 3).unwrap();
583        assert_eq!(cell.symbol.as_str(), "Y");
584        assert_eq!(cell.fg, Color::GREEN);
585    }
586
587    #[test]
588    fn test_buffer_dirty_tracking() {
589        let mut buf = CellBuffer::new(10, 5);
590        assert_eq!(buf.dirty_count(), 0);
591
592        buf.mark_dirty(0, 0);
593        assert_eq!(buf.dirty_count(), 1);
594
595        buf.mark_all_dirty();
596        assert_eq!(buf.dirty_count(), 50);
597
598        buf.clear_dirty();
599        assert_eq!(buf.dirty_count(), 0);
600    }
601
602    #[test]
603    fn test_buffer_iter_dirty() {
604        let mut buf = CellBuffer::new(10, 5);
605        buf.mark_dirty(1, 1);
606        buf.mark_dirty(3, 3);
607
608        let dirty: Vec<usize> = buf.iter_dirty().collect();
609        assert_eq!(dirty.len(), 2);
610        assert!(dirty.contains(&buf.index(1, 1)));
611        assert!(dirty.contains(&buf.index(3, 3)));
612    }
613
614    #[test]
615    fn test_buffer_resize() {
616        let mut buf = CellBuffer::new(10, 5);
617        buf.update(0, 0, "X", Color::RED, Color::BLACK, Modifiers::NONE);
618
619        buf.resize(20, 10);
620        assert_eq!(buf.width(), 20);
621        assert_eq!(buf.height(), 10);
622        assert_eq!(buf.len(), 200);
623        // Content should be cleared
624        assert_eq!(buf.get(0, 0).unwrap().symbol.as_str(), " ");
625        // All should be dirty after resize
626        assert_eq!(buf.dirty_count(), 200);
627    }
628
629    #[test]
630    fn test_buffer_clear() {
631        let mut buf = CellBuffer::new(10, 5);
632        buf.update(0, 0, "X", Color::RED, Color::BLACK, Modifiers::BOLD);
633        buf.clear_dirty();
634
635        buf.clear();
636        let cell = buf.get(0, 0).unwrap();
637        assert_eq!(cell.symbol.as_str(), " ");
638        assert!(cell.modifiers.is_empty());
639        assert_eq!(buf.dirty_count(), 50);
640    }
641
642    #[test]
643    fn test_buffer_fill_rect() {
644        let mut buf = CellBuffer::new(10, 10);
645        buf.fill_rect(2, 2, 3, 3, Color::WHITE, Color::RED);
646
647        // Inside rect
648        assert_eq!(buf.get(3, 3).unwrap().bg, Color::RED);
649        // Outside rect - default is TRANSPARENT
650        assert_eq!(buf.get(0, 0).unwrap().bg, Color::TRANSPARENT);
651    }
652
653    #[test]
654    fn test_buffer_fill_rect_clipped() {
655        let mut buf = CellBuffer::new(10, 10);
656        buf.fill_rect(8, 8, 5, 5, Color::WHITE, Color::BLUE);
657
658        // Should be clipped to buffer bounds
659        assert_eq!(buf.get(9, 9).unwrap().bg, Color::BLUE);
660    }
661
662    #[test]
663    fn test_buffer_cells_access() {
664        let mut buf = CellBuffer::new(10, 5);
665        assert_eq!(buf.cells().len(), 50);
666        assert_eq!(buf.cells_mut().len(), 50);
667    }
668
669    #[test]
670    fn test_cell_empty_string() {
671        let cell = Cell::new("", Color::WHITE, Color::BLACK, Modifiers::NONE);
672        // Width should be at least 1
673        assert_eq!(cell.width(), 1);
674    }
675
676    #[test]
677    fn test_modifiers_all_flags() {
678        let all = Modifiers::BOLD
679            | Modifiers::ITALIC
680            | Modifiers::UNDERLINE
681            | Modifiers::STRIKETHROUGH
682            | Modifiers::DIM
683            | Modifiers::BLINK
684            | Modifiers::REVERSE
685            | Modifiers::HIDDEN;
686
687        assert!(all.contains(Modifiers::BOLD));
688        assert!(all.contains(Modifiers::ITALIC));
689        assert!(all.contains(Modifiers::UNDERLINE));
690        assert!(all.contains(Modifiers::STRIKETHROUGH));
691        assert!(all.contains(Modifiers::DIM));
692        assert!(all.contains(Modifiers::BLINK));
693        assert!(all.contains(Modifiers::REVERSE));
694        assert!(all.contains(Modifiers::HIDDEN));
695    }
696}