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}