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    #[allow(unsafe_code)]
329    pub fn grapheme(&self) -> Option<&str> {
330        if self.flags.contains(CellFlags::OVERFLOW) {
331            return None;
332        }
333        // SAFETY: We only store valid UTF-8 in the grapheme bytes
334        Some(unsafe {
335            std::str::from_utf8_unchecked(&self.grapheme[..self.grapheme_len as usize])
336        })
337    }
338
339    /// Get the overflow index if this is an overflow cell.
340    #[inline]
341    pub const fn overflow_index(&self) -> Option<u32> {
342        if self.flags.contains(CellFlags::OVERFLOW) {
343            Some(u32::from_le_bytes(self.grapheme))
344        } else {
345            None
346        }
347    }
348
349    /// Check if this cell uses overflow storage.
350    #[inline]
351    pub const fn is_overflow(&self) -> bool {
352        self.flags.contains(CellFlags::OVERFLOW)
353    }
354
355    /// Check if this is a wide-character continuation.
356    #[inline]
357    pub const fn is_wide_continuation(&self) -> bool {
358        self.flags.contains(CellFlags::WIDE_CONTINUATION)
359    }
360
361    /// Get the display width (0, 1, or 2).
362    #[inline]
363    pub const fn display_width(&self) -> u8 {
364        self.display_width
365    }
366
367    /// Get the foreground color.
368    #[inline]
369    pub const fn fg(&self) -> Rgb {
370        self.fg
371    }
372
373    /// Get the background color.
374    #[inline]
375    pub const fn bg(&self) -> Rgb {
376        self.bg
377    }
378
379    /// Get the modifiers.
380    #[inline]
381    pub const fn modifiers(&self) -> Modifiers {
382        self.modifiers
383    }
384
385    /// Get the flags.
386    #[inline]
387    pub const fn flags(&self) -> CellFlags {
388        self.flags
389    }
390
391    /// Set the foreground color.
392    #[inline]
393    pub const fn set_fg(&mut self, fg: Rgb) -> &mut Self {
394        self.fg = fg;
395        self
396    }
397
398    /// Set the background color.
399    #[inline]
400    pub const fn set_bg(&mut self, bg: Rgb) -> &mut Self {
401        self.bg = bg;
402        self
403    }
404
405    /// Set the modifiers.
406    #[inline]
407    pub const fn set_modifiers(&mut self, modifiers: Modifiers) -> &mut Self {
408        self.modifiers = modifiers;
409        self
410    }
411
412    /// Set the foreground color (builder pattern).
413    #[inline]
414    #[must_use]
415    pub const fn with_fg(mut self, fg: Rgb) -> Self {
416        self.fg = fg;
417        self
418    }
419
420    /// Set the background color (builder pattern).
421    #[inline]
422    #[must_use]
423    pub const fn with_bg(mut self, bg: Rgb) -> Self {
424        self.bg = bg;
425        self
426    }
427
428    /// Set the modifiers (builder pattern).
429    #[inline]
430    #[must_use]
431    pub const fn with_modifiers(mut self, modifiers: Modifiers) -> Self {
432        self.modifiers = modifiers;
433        self
434    }
435
436    /// Reset the cell to empty (space with default colors).
437    #[inline]
438    pub const fn reset(&mut self) {
439        *self = Self::EMPTY;
440    }
441}
442
443impl PartialEq for Cell {
444    /// Optimized equality check.
445    ///
446    /// We compare in order of most likely difference:
447    /// 1. Grapheme bytes (most frequently changing)
448    /// 2. Colors (next most common)
449    /// 3. Modifiers and flags (rarely differ)
450    #[inline]
451    fn eq(&self, other: &Self) -> bool {
452        // Fast path: compare grapheme first (most likely to differ)
453        self.grapheme == other.grapheme
454            && self.grapheme_len == other.grapheme_len
455            && self.fg == other.fg
456            && self.bg == other.bg
457            && self.modifiers == other.modifiers
458            && self.flags == other.flags
459            && self.display_width == other.display_width
460    }
461}
462
463impl Eq for Cell {}
464
465impl Hash for Cell {
466    fn hash<H: Hasher>(&self, state: &mut H) {
467        self.grapheme.hash(state);
468        self.grapheme_len.hash(state);
469        self.display_width.hash(state);
470        self.fg.hash(state);
471        self.bg.hash(state);
472        self.modifiers.hash(state);
473        self.flags.hash(state);
474    }
475}
476
477impl std::fmt::Debug for Cell {
478    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
479        let grapheme = self.grapheme().unwrap_or("<overflow>");
480        f.debug_struct("Cell")
481            .field("grapheme", &grapheme)
482            .field("width", &self.display_width)
483            .field("fg", &self.fg)
484            .field("bg", &self.bg)
485            .field("modifiers", &self.modifiers)
486            .field("flags", &self.flags)
487            .finish_non_exhaustive()
488    }
489}
490
491#[cfg(test)]
492mod tests {
493    use super::*;
494
495    #[test]
496    fn test_cell_size() {
497        assert_eq!(std::mem::size_of::<Cell>(), 16);
498    }
499
500    #[test]
501    fn test_rgb_from_tuple() {
502        let rgb: Rgb = (255, 128, 0).into();
503        assert_eq!(rgb.r, 255);
504        assert_eq!(rgb.g, 128);
505        assert_eq!(rgb.b, 0);
506    }
507
508    #[test]
509    fn test_rgb_from_hex() {
510        let rgb: Rgb = 0xFF8000.into();
511        assert_eq!(rgb.r, 255);
512        assert_eq!(rgb.g, 128);
513        assert_eq!(rgb.b, 0);
514    }
515
516    #[test]
517    fn test_cell_new_ascii() {
518        let cell = Cell::new('A');
519        assert_eq!(cell.grapheme(), Some("A"));
520        assert_eq!(cell.display_width(), 1);
521    }
522
523    #[test]
524    fn test_cell_from_char_unicode() {
525        let cell = Cell::from_char('日');
526        assert_eq!(cell.grapheme(), Some("日"));
527        assert_eq!(cell.display_width(), 2); // CJK is double-width
528    }
529
530    #[test]
531    fn test_cell_from_grapheme_fits() {
532        let cell = Cell::from_grapheme("é").unwrap();
533        assert_eq!(cell.grapheme(), Some("é"));
534        assert_eq!(cell.display_width(), 1);
535    }
536
537    #[test]
538    fn test_cell_from_grapheme_overflow() {
539        // This emoji ZWJ sequence is > 4 bytes
540        let result = Cell::from_grapheme("👨‍👩‍👧");
541        assert!(result.is_none());
542    }
543
544    #[test]
545    fn test_cell_overflow() {
546        let cell = Cell::overflow(42, 2);
547        assert!(cell.is_overflow());
548        assert_eq!(cell.overflow_index(), Some(42));
549        assert_eq!(cell.grapheme(), None);
550    }
551
552    #[test]
553    fn test_cell_equality() {
554        let a = Cell::new('A').with_fg(Rgb::new(255, 0, 0));
555        let b = Cell::new('A').with_fg(Rgb::new(255, 0, 0));
556        let c = Cell::new('A').with_fg(Rgb::new(0, 255, 0));
557
558        assert_eq!(a, b);
559        assert_ne!(a, c);
560    }
561
562    #[test]
563    fn test_cell_builder_pattern() {
564        let cell = Cell::new('X')
565            .with_fg(Rgb::new(255, 0, 0))
566            .with_bg(Rgb::new(0, 0, 255))
567            .with_modifiers(Modifiers::BOLD | Modifiers::ITALIC);
568
569        assert_eq!(cell.fg(), Rgb::new(255, 0, 0));
570        assert_eq!(cell.bg(), Rgb::new(0, 0, 255));
571        assert!(cell.modifiers().contains(Modifiers::BOLD));
572        assert!(cell.modifiers().contains(Modifiers::ITALIC));
573    }
574
575    #[test]
576    fn test_modifiers_bitflags() {
577        let mods = Modifiers::BOLD | Modifiers::UNDERLINE;
578        assert!(mods.contains(Modifiers::BOLD));
579        assert!(mods.contains(Modifiers::UNDERLINE));
580        assert!(!mods.contains(Modifiers::ITALIC));
581    }
582
583    #[test]
584    fn test_cell_reset() {
585        let mut cell = Cell::new('X').with_fg(Rgb::new(255, 0, 0));
586        cell.reset();
587        assert_eq!(cell, Cell::EMPTY);
588    }
589
590    #[test]
591    fn test_wide_continuation() {
592        let cont = Cell::wide_continuation();
593        assert!(cont.is_wide_continuation());
594        assert_eq!(cont.display_width(), 0);
595    }
596}