Skip to main content

flywheel/buffer/
cell.rs

1//! Cell: The atomic unit of terminal display.
2//!
3//! # Memory Layout
4//!
5//! The `Cell` struct is carefully designed for cache efficiency:
6//! - 16 bytes total, allowing 4 cells per cache line (64 bytes)
7//! - Inline grapheme storage covers 99%+ of real-world characters
8//! - Complex graphemes (emoji ZWJ sequences) spill to an external `HashMap`
9//!
10//! ```text
11//! ┌───────────────────────────────────────────────────────────────────────────┐
12//! │  Cell Layout (16 bytes)                                                   │
13//! ├─────────────┬─────────────┬───────────┬───────────┬─────┬───────┬─────────┤
14//! │  grapheme   │  len + width│    fg     │    bg     │ mod │ flags │ padding │
15//! │  [u8; 4]    │  u8 + u8    │  [u8; 3]  │  [u8; 3]  │ u8  │  u8   │ [u8; 2] │
16//! │  4 bytes    │  2 bytes    │  3 bytes  │  3 bytes  │ 1b  │  1b   │  2 b    │
17//! └─────────────┴─────────────┴───────────┴───────────┴─────┴───────┴─────────┘
18//! ```
19
20use bitflags::bitflags;
21use std::hash::{Hash, Hasher};
22
23/// True-color RGB representation.
24///
25/// Uses 3 bytes for 24-bit color depth, supporting 16.7 million colors.
26/// This is essential for precise brand colors in commercial applications.
27#[repr(C)]
28#[derive(Clone, Copy, PartialEq, Eq, Default, Hash)]
29pub struct Rgb {
30    /// Red channel (0-255)
31    pub r: u8,
32    /// Green channel (0-255)
33    pub g: u8,
34    /// Blue channel (0-255)
35    pub b: u8,
36}
37
38impl Rgb {
39    /// Create a new RGB color.
40    #[inline]
41    pub const fn new(r: u8, g: u8, b: u8) -> Self {
42        Self { r, g, b }
43    }
44
45    /// Black (0, 0, 0)
46    pub const BLACK: Self = Self::new(0, 0, 0);
47    /// White (255, 255, 255)
48    pub const WHITE: Self = Self::new(255, 255, 255);
49    /// Default foreground (white)
50    pub const DEFAULT_FG: Self = Self::WHITE;
51    /// Default background (black)
52    pub const DEFAULT_BG: Self = Self::BLACK;
53
54    /// Create from a 24-bit hex color (e.g., 0xFF5500).
55    #[inline]
56    pub const fn from_u32(hex: u32) -> Self {
57        Self::new(
58            ((hex >> 16) & 0xFF) as u8,
59            ((hex >> 8) & 0xFF) as u8,
60            (hex & 0xFF) as u8,
61        )
62    }
63}
64
65impl std::fmt::Debug for Rgb {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        write!(f, "#{:02x}{:02x}{:02x}", self.r, self.g, self.b)
68    }
69}
70
71impl From<(u8, u8, u8)> for Rgb {
72    #[inline]
73    fn from((r, g, b): (u8, u8, u8)) -> Self {
74        Self::new(r, g, b)
75    }
76}
77
78impl From<u32> for Rgb {
79    /// Convert from a 24-bit hex color (e.g., 0xFF5500)
80    #[inline]
81    fn from(hex: u32) -> Self {
82        Self::new(
83            ((hex >> 16) & 0xFF) as u8,
84            ((hex >> 8) & 0xFF) as u8,
85            (hex & 0xFF) as u8,
86        )
87    }
88}
89
90bitflags! {
91    /// Text style modifiers.
92    ///
93    /// These can be combined using bitwise OR.
94    ///
95    /// # Example
96    /// ```
97    /// use flywheel::Modifiers;
98    /// let style = Modifiers::BOLD | Modifiers::ITALIC;
99    /// ```
100    #[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
101    pub struct Modifiers: u8 {
102        /// Bold text
103        const BOLD = 0b0000_0001;
104        /// Dim/faint text
105        const DIM = 0b0000_0010;
106        /// Italic text
107        const ITALIC = 0b0000_0100;
108        /// Underlined text
109        const UNDERLINE = 0b0000_1000;
110        /// Blinking text
111        const BLINK = 0b0001_0000;
112        /// Reversed colors (fg/bg swapped)
113        const REVERSED = 0b0010_0000;
114        /// Hidden/invisible text
115        const HIDDEN = 0b0100_0000;
116        /// Strikethrough text
117        const STRIKETHROUGH = 0b1000_0000;
118    }
119}
120
121impl std::fmt::Debug for Modifiers {
122    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123        bitflags::parser::to_writer(self, f)
124    }
125}
126
127bitflags! {
128    /// Cell-level flags for special states.
129    #[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
130    pub struct CellFlags: u8 {
131        /// Grapheme overflows inline storage; check overflow HashMap
132        const OVERFLOW = 0b0000_0001;
133        /// Cell has been modified since last render
134        const DIRTY = 0b0000_0010;
135        /// This cell is a continuation of a wide character
136        const WIDE_CONTINUATION = 0b0000_0100;
137    }
138}
139
140impl std::fmt::Debug for CellFlags {
141    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142        bitflags::parser::to_writer(self, f)
143    }
144}
145
146/// A single terminal cell.
147///
148/// This is the atomic unit of display in Flywheel. Each cell contains:
149/// - A grapheme (the character to display)
150/// - Foreground and background colors
151/// - Text modifiers (bold, italic, etc.)
152///
153/// # Memory Layout
154///
155/// The struct is carefully laid out to be exactly 16 bytes:
156/// - 4 bytes for inline grapheme storage
157/// - 2 bytes for grapheme metadata (length + display width)
158/// - 6 bytes for colors (3 bytes fg + 3 bytes bg)
159/// - 1 byte for modifiers
160/// - 1 byte for flags
161/// - 2 bytes padding (power-of-2 alignment)
162///
163/// # Grapheme Handling
164///
165/// Most characters (ASCII, Latin, CJK) fit within the 4-byte inline storage.
166/// For complex graphemes like emoji ZWJ sequences (👨‍👩‍👧‍👦), we set the
167/// `OVERFLOW` flag and store an index in the grapheme bytes that points
168/// to an external overflow storage.
169#[repr(C)]
170#[derive(Clone, Copy)]
171pub struct Cell {
172    /// Inline grapheme storage (UTF-8 bytes).
173    /// For overflowed graphemes, this contains a u32 index.
174    grapheme: [u8; 4],
175    /// Actual byte length of the grapheme (0-4, or 0 if overflowed).
176    grapheme_len: u8,
177    /// Display width of the grapheme (0=continuation, 1=normal, 2=wide CJK).
178    display_width: u8,
179    /// Foreground color.
180    fg: Rgb,
181    /// Background color.
182    bg: Rgb,
183    /// Text modifiers (bold, italic, etc.).
184    modifiers: Modifiers,
185    /// Cell flags (overflow, dirty, etc.).
186    flags: CellFlags,
187    /// Padding to reach 16 bytes (power of 2, cache-friendly).
188    _padding: [u8; 2],
189}
190
191// Compile-time assertion: Cell must be exactly 16 bytes
192const _: () = assert!(
193    std::mem::size_of::<Cell>() == 16,
194    "Cell must be exactly 16 bytes for cache efficiency"
195);
196
197impl Default for Cell {
198    fn default() -> Self {
199        Self::EMPTY
200    }
201}
202
203impl Cell {
204    /// An empty cell (space character with default colors).
205    pub const EMPTY: Self = Self {
206        grapheme: [b' ', 0, 0, 0],
207        grapheme_len: 1,
208        display_width: 1,
209        fg: Rgb::DEFAULT_FG,
210        bg: Rgb::DEFAULT_BG,
211        modifiers: Modifiers::empty(),
212        flags: CellFlags::empty(),
213        _padding: [0, 0],
214    };
215
216    /// Create a new cell with a single ASCII character.
217    ///
218    /// # Panics
219    /// Panics if the character is not ASCII.
220    #[inline]
221    pub fn new(c: char) -> Self {
222        debug_assert!(c.is_ascii(), "Use Cell::from_char for non-ASCII");
223        Self {
224            grapheme: [c as u8, 0, 0, 0],
225            grapheme_len: 1,
226            display_width: 1,
227            fg: Rgb::DEFAULT_FG,
228            bg: Rgb::DEFAULT_BG,
229            modifiers: Modifiers::empty(),
230            flags: CellFlags::empty(),
231            _padding: [0, 0],
232        }
233    }
234
235    /// Create a cell from any character.
236    ///
237    /// Returns `None` if the character's UTF-8 encoding exceeds 4 bytes
238    /// (which never happens for a single `char`, but may happen for
239    /// grapheme clusters when using `from_grapheme`).
240    #[inline]
241    #[allow(clippy::missing_panics_doc)]
242    pub fn from_char(c: char) -> Self {
243        let mut grapheme = [0u8; 4];
244        let s = c.encode_utf8(&mut grapheme);
245        let len = u8::try_from(s.len()).unwrap();
246        let width = u8::try_from(unicode_width::UnicodeWidthChar::width(c).unwrap_or(0)).unwrap();
247
248        Self {
249            grapheme,
250            grapheme_len: len,
251            display_width: width,
252            fg: Rgb::DEFAULT_FG,
253            bg: Rgb::DEFAULT_BG,
254            modifiers: Modifiers::empty(),
255            flags: CellFlags::empty(),
256            _padding: [0, 0],
257        }
258    }
259
260    /// Create a cell from a grapheme string.
261    ///
262    /// If the grapheme fits in 4 bytes, it's stored inline.
263    /// Otherwise, returns `None` and the caller should use overflow storage.
264    #[inline]
265    #[allow(clippy::missing_panics_doc)]
266    pub fn from_grapheme(s: &str) -> Option<Self> {
267        let bytes = s.as_bytes();
268        if bytes.len() > 4 {
269            // Caller needs to handle overflow
270            return None;
271        }
272
273        let mut grapheme = [0u8; 4];
274        grapheme[..bytes.len()].copy_from_slice(bytes);
275        let width = u8::try_from(unicode_width::UnicodeWidthStr::width(s)).unwrap_or(1);
276
277        Some(Self {
278            grapheme,
279            grapheme_len: u8::try_from(bytes.len()).unwrap(),
280            display_width: width,
281            fg: Rgb::DEFAULT_FG,
282            bg: Rgb::DEFAULT_BG,
283            modifiers: Modifiers::empty(),
284            flags: CellFlags::empty(),
285            _padding: [0, 0],
286        })
287    }
288
289    /// Create an overflow cell with an index to external storage.
290    ///
291    /// The index is stored in the grapheme bytes as a little-endian u32.
292    #[inline]
293    pub const fn overflow(index: u32, display_width: u8) -> Self {
294        Self {
295            grapheme: index.to_le_bytes(),
296            grapheme_len: 0, // Indicates overflow
297            display_width,
298            fg: Rgb::DEFAULT_FG,
299            bg: Rgb::DEFAULT_BG,
300            modifiers: Modifiers::empty(),
301            flags: CellFlags::OVERFLOW,
302            _padding: [0, 0],
303        }
304    }
305
306    /// Create a wide-character continuation cell.
307    ///
308    /// This is placed after a wide CJK character that takes 2 columns.
309    #[inline]
310    pub const fn wide_continuation() -> Self {
311        Self {
312            grapheme: [0, 0, 0, 0],
313            grapheme_len: 0,
314            display_width: 0,
315            fg: Rgb::DEFAULT_FG,
316            bg: Rgb::DEFAULT_BG,
317            modifiers: Modifiers::empty(),
318            flags: CellFlags::WIDE_CONTINUATION,
319            _padding: [0, 0],
320        }
321    }
322
323    /// Get the grapheme as a string slice.
324    ///
325    /// Returns `None` if this is an overflow cell (caller should check `is_overflow()`
326    /// and look up the grapheme in the overflow storage).
327    #[inline]
328    pub fn grapheme(&self) -> Option<&str> {
329        if self.flags.contains(CellFlags::OVERFLOW) {
330            return None;
331        }
332        // Safe UTF-8 conversion - we validate on input
333        std::str::from_utf8(&self.grapheme[..self.grapheme_len as usize]).ok()
334    }
335
336    /// Get the overflow index if this is an overflow cell.
337    #[inline]
338    pub const fn overflow_index(&self) -> Option<u32> {
339        if self.flags.contains(CellFlags::OVERFLOW) {
340            Some(u32::from_le_bytes(self.grapheme))
341        } else {
342            None
343        }
344    }
345
346    /// Check if this cell uses overflow storage.
347    #[inline]
348    pub const fn is_overflow(&self) -> bool {
349        self.flags.contains(CellFlags::OVERFLOW)
350    }
351
352    /// Check if this is a wide-character continuation.
353    #[inline]
354    pub const fn is_wide_continuation(&self) -> bool {
355        self.flags.contains(CellFlags::WIDE_CONTINUATION)
356    }
357
358    /// Get the display width (0, 1, or 2).
359    #[inline]
360    pub const fn display_width(&self) -> u8 {
361        self.display_width
362    }
363
364    /// Get the foreground color.
365    #[inline]
366    pub const fn fg(&self) -> Rgb {
367        self.fg
368    }
369
370    /// Get the background color.
371    #[inline]
372    pub const fn bg(&self) -> Rgb {
373        self.bg
374    }
375
376    /// Get the modifiers.
377    #[inline]
378    pub const fn modifiers(&self) -> Modifiers {
379        self.modifiers
380    }
381
382    /// Get the flags.
383    #[inline]
384    pub const fn flags(&self) -> CellFlags {
385        self.flags
386    }
387
388    /// Set the foreground color.
389    #[inline]
390    pub const fn set_fg(&mut self, fg: Rgb) -> &mut Self {
391        self.fg = fg;
392        self
393    }
394
395    /// Set the background color.
396    #[inline]
397    pub const fn set_bg(&mut self, bg: Rgb) -> &mut Self {
398        self.bg = bg;
399        self
400    }
401
402    /// Set the modifiers.
403    #[inline]
404    pub const fn set_modifiers(&mut self, modifiers: Modifiers) -> &mut Self {
405        self.modifiers = modifiers;
406        self
407    }
408
409    /// Set the foreground color (builder pattern).
410    #[inline]
411    #[must_use]
412    pub const fn with_fg(mut self, fg: Rgb) -> Self {
413        self.fg = fg;
414        self
415    }
416
417    /// Set the background color (builder pattern).
418    #[inline]
419    #[must_use]
420    pub const fn with_bg(mut self, bg: Rgb) -> Self {
421        self.bg = bg;
422        self
423    }
424
425    /// Set the modifiers (builder pattern).
426    #[inline]
427    #[must_use]
428    pub const fn with_modifiers(mut self, modifiers: Modifiers) -> Self {
429        self.modifiers = modifiers;
430        self
431    }
432
433    /// Reset the cell to empty (space with default colors).
434    #[inline]
435    pub const fn reset(&mut self) {
436        *self = Self::EMPTY;
437    }
438}
439
440impl PartialEq for Cell {
441    /// Optimized equality check.
442    ///
443    /// We compare in order of most likely difference:
444    /// 1. Grapheme bytes (most frequently changing)
445    /// 2. Colors (next most common)
446    /// 3. Modifiers and flags (rarely differ)
447    #[inline]
448    fn eq(&self, other: &Self) -> bool {
449        // Fast path: compare grapheme first (most likely to differ)
450        self.grapheme == other.grapheme
451            && self.grapheme_len == other.grapheme_len
452            && self.fg == other.fg
453            && self.bg == other.bg
454            && self.modifiers == other.modifiers
455            && self.flags == other.flags
456            && self.display_width == other.display_width
457    }
458}
459
460impl Eq for Cell {}
461
462impl Hash for Cell {
463    fn hash<H: Hasher>(&self, state: &mut H) {
464        self.grapheme.hash(state);
465        self.grapheme_len.hash(state);
466        self.display_width.hash(state);
467        self.fg.hash(state);
468        self.bg.hash(state);
469        self.modifiers.hash(state);
470        self.flags.hash(state);
471    }
472}
473
474impl std::fmt::Debug for Cell {
475    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
476        let grapheme = self.grapheme().unwrap_or("<overflow>");
477        f.debug_struct("Cell")
478            .field("grapheme", &grapheme)
479            .field("width", &self.display_width)
480            .field("fg", &self.fg)
481            .field("bg", &self.bg)
482            .field("modifiers", &self.modifiers)
483            .field("flags", &self.flags)
484            .finish_non_exhaustive()
485    }
486}
487
488#[cfg(test)]
489mod tests {
490    use super::*;
491
492    #[test]
493    fn test_cell_size() {
494        assert_eq!(std::mem::size_of::<Cell>(), 16);
495    }
496
497    #[test]
498    fn test_rgb_from_tuple() {
499        let rgb: Rgb = (255, 128, 0).into();
500        assert_eq!(rgb.r, 255);
501        assert_eq!(rgb.g, 128);
502        assert_eq!(rgb.b, 0);
503    }
504
505    #[test]
506    fn test_rgb_from_hex() {
507        let rgb: Rgb = 0xFF8000.into();
508        assert_eq!(rgb.r, 255);
509        assert_eq!(rgb.g, 128);
510        assert_eq!(rgb.b, 0);
511    }
512
513    #[test]
514    fn test_cell_new_ascii() {
515        let cell = Cell::new('A');
516        assert_eq!(cell.grapheme(), Some("A"));
517        assert_eq!(cell.display_width(), 1);
518    }
519
520    #[test]
521    fn test_cell_from_char_unicode() {
522        let cell = Cell::from_char('日');
523        assert_eq!(cell.grapheme(), Some("日"));
524        assert_eq!(cell.display_width(), 2); // CJK is double-width
525    }
526
527    #[test]
528    fn test_cell_from_grapheme_fits() {
529        let cell = Cell::from_grapheme("é").unwrap();
530        assert_eq!(cell.grapheme(), Some("é"));
531        assert_eq!(cell.display_width(), 1);
532    }
533
534    #[test]
535    fn test_cell_from_grapheme_overflow() {
536        // This emoji ZWJ sequence is > 4 bytes
537        let result = Cell::from_grapheme("👨‍👩‍👧");
538        assert!(result.is_none());
539    }
540
541    #[test]
542    fn test_cell_overflow() {
543        let cell = Cell::overflow(42, 2);
544        assert!(cell.is_overflow());
545        assert_eq!(cell.overflow_index(), Some(42));
546        assert_eq!(cell.grapheme(), None);
547    }
548
549    #[test]
550    fn test_cell_equality() {
551        let a = Cell::new('A').with_fg(Rgb::new(255, 0, 0));
552        let b = Cell::new('A').with_fg(Rgb::new(255, 0, 0));
553        let c = Cell::new('A').with_fg(Rgb::new(0, 255, 0));
554
555        assert_eq!(a, b);
556        assert_ne!(a, c);
557    }
558
559    #[test]
560    fn test_cell_builder_pattern() {
561        let cell = Cell::new('X')
562            .with_fg(Rgb::new(255, 0, 0))
563            .with_bg(Rgb::new(0, 0, 255))
564            .with_modifiers(Modifiers::BOLD | Modifiers::ITALIC);
565
566        assert_eq!(cell.fg(), Rgb::new(255, 0, 0));
567        assert_eq!(cell.bg(), Rgb::new(0, 0, 255));
568        assert!(cell.modifiers().contains(Modifiers::BOLD));
569        assert!(cell.modifiers().contains(Modifiers::ITALIC));
570    }
571
572    #[test]
573    fn test_modifiers_bitflags() {
574        let mods = Modifiers::BOLD | Modifiers::UNDERLINE;
575        assert!(mods.contains(Modifiers::BOLD));
576        assert!(mods.contains(Modifiers::UNDERLINE));
577        assert!(!mods.contains(Modifiers::ITALIC));
578    }
579
580    #[test]
581    fn test_cell_reset() {
582        let mut cell = Cell::new('X').with_fg(Rgb::new(255, 0, 0));
583        cell.reset();
584        assert_eq!(cell, Cell::EMPTY);
585    }
586
587    #[test]
588    fn test_wide_continuation() {
589        let cont = Cell::wide_continuation();
590        assert!(cont.is_wide_continuation());
591        assert_eq!(cont.display_width(), 0);
592    }
593}