Skip to main content

lattice_cells/
cell.rs

1//! 16-byte cell type — the atom of the cell-grid renderer.
2//!
3//! Cells are cursor-invariant: anything that changes layout or
4//! glyph content lives here; anything cursor-coupled lives in
5//! `OverlayState` (S2). See
6//! `docs/dev/architecture/cell-grid-renderer.md` § Decoration
7//! assignment for the full rule.
8
9/// Bit positions inside `Cell::flags`. Use the named constants;
10/// raw bit math here would be a future maintenance hazard.
11pub mod flags {
12    /// Cell came from an inlay-hint splice rather than source
13    /// text. The body codepoint is still the inlay's character;
14    /// this bit only marks provenance so byte↔column remap can
15    /// distinguish inlay positions from source bytes.
16    pub const INLAY: u16 = 1 << 0;
17    /// Whitespace-marker cell (e.g. tab indicator `→`, EOL `·`).
18    /// Source text at this position was whitespace; the renderer
19    /// substituted a marker glyph. Used by `listchars`-style
20    /// rendering.
21    pub const WS_MARKER: u16 = 1 << 1;
22    /// S3.a (2026-05-26): text-attribute modifier — bold glyph.
23    /// Set by the cell-builder from
24    /// `host::Theme::syntax_style(style).modifiers.bold` so the
25    /// renderer paints the cell with its font's bold weight.
26    pub const BOLD: u16 = 1 << 2;
27    /// Text-attribute modifier — italic glyph. From
28    /// `host::Theme::syntax_style(style).modifiers.italic`.
29    pub const ITALIC: u16 = 1 << 3;
30    /// Text-attribute modifier — underlined glyph. From
31    /// `host::Theme::syntax_style(style).modifiers.underline`.
32    /// The renderer is responsible for the underline geometry
33    /// (font baseline + 1px, etc.).
34    pub const UNDERLINE: u16 = 1 << 4;
35    /// Text-attribute modifier — dimmed cell. From
36    /// `host::Theme::syntax_style(style).modifiers.dim`. The
37    /// renderer typically blends fg toward the pane background.
38    pub const DIM: u16 = 1 << 5;
39    /// Text-attribute modifier — reverse video (swap fg/bg).
40    /// From `host::Theme::syntax_style(style).modifiers.reverse`.
41    pub const REVERSE: u16 = 1 << 6;
42    /// B2.2 (2026-06-04): trailing-whitespace marker. Set in
43    /// addition to [`WS_MARKER`] when the marker glyph stands in for
44    /// whitespace *after* the last non-blank char on the line (or on
45    /// an all-blank line). Lets a consumer resolve the
46    /// trailing-whitespace foreground (`theme.whitespace_trailing_style`)
47    /// from the run/cell alone — the `DisplayLine` model carries no
48    /// byte positions, so without this bit a `DisplayLine → CellMatrix`
49    /// projection could not reproduce the cell path's trailing-red.
50    pub const WS_TRAILING: u16 = 1 << 7;
51}
52
53/// One renderable cell. Exactly 16 bytes: codepoint (4) + fg (4) +
54/// bg (4) + flags (2) + padding (2). 4 cells per 64-byte cache
55/// line, which makes row walks cache-friendly.
56///
57/// Construction is direct (`Cell { codepoint, fg, bg, flags, .. }`)
58/// or via the [`Cell::blank`] / [`Cell::with_codepoint`]
59/// constructors below.
60#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
61#[repr(C)]
62pub struct Cell {
63    /// Unicode scalar value. `0` is the empty/blank cell sentinel.
64    pub codepoint: u32,
65    /// `0xRRGGBB`. Theme-resolved foreground colour.
66    pub fg: u32,
67    /// `0xRRGGBB`. Theme-resolved background colour; `0` is
68    /// "transparent" → renderer uses the pane background.
69    pub bg: u32,
70    /// Bit flags from [`flags`].
71    pub flags: u16,
72    /// Padding so the struct is 16 bytes. Never read.
73    _padding: u16,
74}
75
76impl Cell {
77    /// All-zero cell: blank codepoint, transparent fg/bg, no
78    /// flags. The default value for unused trailing cells in a
79    /// shorter-than-row source line.
80    pub const BLANK: Self = Self {
81        codepoint: 0,
82        fg: 0,
83        bg: 0,
84        flags: 0,
85        _padding: 0,
86    };
87
88    /// Construct a default-coloured cell at `codepoint`. fg/bg
89    /// default to 0 (renderer fills from theme); call sites that
90    /// know colours should construct the struct literal directly.
91    pub const fn with_codepoint(codepoint: u32) -> Self {
92        Self {
93            codepoint,
94            fg: 0,
95            bg: 0,
96            flags: 0,
97            _padding: 0,
98        }
99    }
100
101    /// Convenience builder used in tests + cell-builder fixtures.
102    /// Direct struct construction is the normal path.
103    pub const fn new(codepoint: u32, fg: u32, bg: u32, flags: u16) -> Self {
104        Self {
105            codepoint,
106            fg,
107            bg,
108            flags,
109            _padding: 0,
110        }
111    }
112
113    /// `true` when the cell has the blank-sentinel codepoint. Does
114    /// not consider fg/bg/flags — a blank-codepoint cell with a
115    /// non-zero bg is still considered blank for content purposes
116    /// (it's used for trailing-cell padding within a row).
117    pub fn is_blank(&self) -> bool {
118        self.codepoint == 0
119    }
120
121    /// `true` when this cell came from an inlay-hint splice.
122    pub fn is_inlay(&self) -> bool {
123        self.flags & flags::INLAY != 0
124    }
125
126    /// `true` when this cell is a whitespace marker glyph.
127    pub fn is_ws_marker(&self) -> bool {
128        self.flags & flags::WS_MARKER != 0
129    }
130
131    /// S3.a: `true` iff the bold modifier bit is set.
132    pub fn is_bold(&self) -> bool {
133        self.flags & flags::BOLD != 0
134    }
135
136    /// S3.a: `true` iff the italic modifier bit is set.
137    pub fn is_italic(&self) -> bool {
138        self.flags & flags::ITALIC != 0
139    }
140
141    /// S3.a: `true` iff the underline modifier bit is set.
142    pub fn is_underline(&self) -> bool {
143        self.flags & flags::UNDERLINE != 0
144    }
145
146    /// S3.a: `true` iff the dim modifier bit is set.
147    pub fn is_dim(&self) -> bool {
148        self.flags & flags::DIM != 0
149    }
150
151    /// S3.a: `true` iff the reverse modifier bit is set.
152    pub fn is_reverse(&self) -> bool {
153        self.flags & flags::REVERSE != 0
154    }
155}
156
157impl Default for Cell {
158    fn default() -> Self {
159        Self::BLANK
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    /// Load-bearing: `Cell` must be 16 bytes for the cache-line
168    /// argument in `cell-grid-renderer.md` to hold. If this assert
169    /// fires, adjust padding before doing anything else.
170    #[test]
171    fn cell_is_16_bytes() {
172        assert_eq!(std::mem::size_of::<Cell>(), 16);
173        assert_eq!(std::mem::align_of::<Cell>(), 4);
174    }
175
176    #[test]
177    fn blank_has_zero_codepoint() {
178        let c = Cell::BLANK;
179        assert!(c.is_blank());
180        assert_eq!(c.codepoint, 0);
181        assert_eq!(c.fg, 0);
182        assert_eq!(c.bg, 0);
183        assert_eq!(c.flags, 0);
184    }
185
186    #[test]
187    fn default_equals_blank() {
188        assert_eq!(Cell::default(), Cell::BLANK);
189    }
190
191    #[test]
192    fn with_codepoint_only_sets_codepoint() {
193        let c = Cell::with_codepoint(b'x' as u32);
194        assert_eq!(c.codepoint, b'x' as u32);
195        assert_eq!(c.fg, 0);
196        assert_eq!(c.bg, 0);
197        assert_eq!(c.flags, 0);
198        assert!(!c.is_blank());
199    }
200
201    #[test]
202    fn new_with_colors_and_flags() {
203        let c = Cell::new(b'a' as u32, 0xcdd6f4, 0x1e1e2e, flags::INLAY);
204        assert_eq!(c.codepoint, b'a' as u32);
205        assert_eq!(c.fg, 0xcdd6f4);
206        assert_eq!(c.bg, 0x1e1e2e);
207        assert!(c.is_inlay());
208        assert!(!c.is_ws_marker());
209        assert!(!c.is_blank());
210    }
211
212    #[test]
213    fn ws_marker_flag_independent_of_inlay() {
214        let c = Cell::new(b'.' as u32, 0, 0, flags::WS_MARKER);
215        assert!(c.is_ws_marker());
216        assert!(!c.is_inlay());
217        let c2 = Cell::new(b':' as u32, 0, 0, flags::INLAY | flags::WS_MARKER);
218        assert!(c2.is_ws_marker());
219        assert!(c2.is_inlay());
220    }
221
222    /// S3.a: each modifier-flag bit toggles independently and its
223    /// query helper returns the right answer in isolation + when
224    /// combined with other flags.
225    #[test]
226    fn modifier_flag_bits_compose_independently() {
227        // Each modifier alone.
228        let bold = Cell::new(b'a' as u32, 0, 0, flags::BOLD);
229        assert!(bold.is_bold());
230        assert!(!bold.is_italic());
231        assert!(!bold.is_underline());
232        assert!(!bold.is_dim());
233        assert!(!bold.is_reverse());
234
235        let italic = Cell::new(b'a' as u32, 0, 0, flags::ITALIC);
236        assert!(italic.is_italic());
237        assert!(!italic.is_bold());
238
239        let under = Cell::new(b'a' as u32, 0, 0, flags::UNDERLINE);
240        assert!(under.is_underline());
241
242        let dim = Cell::new(b'a' as u32, 0, 0, flags::DIM);
243        assert!(dim.is_dim());
244
245        let rev = Cell::new(b'a' as u32, 0, 0, flags::REVERSE);
246        assert!(rev.is_reverse());
247
248        // Composition: bold + italic + underline + INLAY all set.
249        let all = Cell::new(
250            b'a' as u32,
251            0,
252            0,
253            flags::BOLD | flags::ITALIC | flags::UNDERLINE | flags::INLAY,
254        );
255        assert!(all.is_bold());
256        assert!(all.is_italic());
257        assert!(all.is_underline());
258        assert!(all.is_inlay());
259        assert!(!all.is_dim());
260        assert!(!all.is_reverse());
261        assert!(!all.is_ws_marker());
262    }
263
264    /// Modifier bits don't collide with the INLAY / WS_MARKER bits
265    /// (sanity check for future flag additions).
266    #[test]
267    fn flag_bits_dont_overlap() {
268        let all = [
269            flags::INLAY,
270            flags::WS_MARKER,
271            flags::BOLD,
272            flags::ITALIC,
273            flags::UNDERLINE,
274            flags::DIM,
275            flags::REVERSE,
276            flags::WS_TRAILING,
277        ];
278        let mut seen: u16 = 0;
279        for f in all {
280            assert!(
281                seen & f == 0,
282                "flag {f:#06x} overlaps an earlier flag in {seen:#06x}"
283            );
284            seen |= f;
285        }
286    }
287}