Skip to main content

max / shop

12.0 KB · 368 lines History Blame Raw
1 //! The 12-byte display cell: its colours, its attributes, and the table that
2 //! holds the combining-mark sequences cells refer to by id.
3 //!
4 //! The layout rationale lives on [`Cell`] itself. Nothing here knows about the
5 //! grid; the grid is what owns a [`MarkTable`] and hands ids out.
6
7 use std::collections::HashMap;
8
9 /// A terminal color.
10 ///
11 /// [`Color::Default`] means "resolve at render time to the theme's default fg
12 /// or bg" — kept out of `Rgb` so we don't lose the "this cell was never
13 /// styled" signal (matters for e.g. transparent backgrounds).
14 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
15 pub enum Color {
16 Default,
17 Named(u8),
18 Indexed(u8),
19 Rgb(u8, u8, u8),
20 }
21
22 /// SGR-set attributes for a cell.
23 #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
24 pub struct Attrs {
25 pub bold: bool,
26 pub italic: bool,
27 pub underline: bool,
28 pub reverse: bool,
29 pub dim: bool,
30 pub strikethrough: bool,
31 }
32
33 /// One cell of the display grid — 12 bytes, matching foot's layout.
34 ///
35 /// Three `u32`s so the struct aligns to 4 (a `u64` would pad to 16). Each
36 /// color word holds a 24-bit RGB payload, a 2-bit source tag, and a 6-bit
37 /// attribute half — the attribute bits live in `fg_word`, `bg_word`'s upper
38 /// 6 bits are spare.
39 ///
40 /// Layout of `fg_word` / `bg_word`:
41 /// - bits 0–23: color payload — RGB if `src == Rgb`, low byte = palette index if Named/Indexed
42 /// - bits 24–25: source tag (`SRC_DEFAULT`/`NAMED`/`INDEXED`/`RGB`)
43 /// - bits 26–31: attribute flags (fg_word only; bg_word width flags + spare)
44 ///
45 /// Layout of `c_raw`:
46 /// - bits 0–20: the base character. A `char` is 21 bits, so this is the whole
47 /// of one and [`Cell::c`] is a mask away.
48 /// - bits 21–31: id into the grid's mark table, 0 meaning none.
49 ///
50 /// Keeping the base character INLINE and interning only the marks after it is
51 /// what makes combining marks cost nothing to everything that does not care:
52 /// the renderer's glyph lookup, the fills scan and word boundaries all read
53 /// `c()` and never learn the table exists. It also keeps the cell `Copy` and
54 /// memcpy-able, which is what lets `resize_buf`, the ring's origin advance and
55 /// the scrollback rewrap move cells around without knowing any of this.
56 #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
57 #[repr(C)]
58 pub struct Cell {
59 pub(crate) c_raw: u32,
60 pub(crate) fg_word: u32,
61 pub(crate) bg_word: u32,
62 }
63
64 /// A `char` is 21 bits; `c_raw`'s low 21 hold one whole.
65 const CHAR_MASK: u32 = (1 << 21) - 1;
66 const MARKS_SHIFT: u32 = 21;
67 /// Eleven bits of mark-sequence id, 0 reserved for "no marks". Distinct mark
68 /// SEQUENCES in real text run to dozens — it is distinct clusters that run to
69 /// thousands, and those are not what is interned here — so 2047 is not a
70 /// ceiling anything is expected to reach.
71 const MARKS_LIMIT: u32 = (1 << 11) - 1;
72
73 /// How many combining marks one cell will accumulate.
74 ///
75 /// A stream of combining marks is unbounded input, and a cluster long enough to
76 /// need more than this is not text anyone is reading. Past the cap the mark is
77 /// dropped rather than the cell rewritten.
78 pub(crate) const MAX_MARKS: usize = 8;
79
80 const SRC_DEFAULT: u32 = 0;
81 const SRC_NAMED: u32 = 1;
82 const SRC_INDEXED: u32 = 2;
83 const SRC_RGB: u32 = 3;
84
85 const SRC_SHIFT: u32 = 24;
86 const SRC_MASK: u32 = 0b11 << SRC_SHIFT;
87 const RGB_MASK: u32 = 0x00FF_FFFF;
88
89 // Width flags, on `bg_word`'s spare upper bits. A double-width character is one
90 // cell holding the character with `FLAG_WIDE` set, followed by one holding a
91 // blank with `FLAG_SPACER` set. The pair is always adjacent and always in that
92 // order; `heal_pair` is what keeps that true when a write lands on half of one.
93 //
94 // The spacer carries the lead's colours so a background fill covers both halves
95 // with no seam, and holds a blank so every reader that draws or copies a cell's
96 // character already does the right thing with it without being taught to.
97 const FLAG_WIDE: u32 = 1 << 26;
98 const FLAG_SPACER: u32 = 1 << 27;
99 const FLAG_WIDTH_MASK: u32 = FLAG_WIDE | FLAG_SPACER;
100
101 const ATTR_BOLD: u32 = 1 << 26;
102 const ATTR_ITALIC: u32 = 1 << 27;
103 const ATTR_UNDERLINE: u32 = 1 << 28;
104 const ATTR_REVERSE: u32 = 1 << 29;
105 const ATTR_DIM: u32 = 1 << 30;
106 const ATTR_STRIKE: u32 = 1 << 31;
107
108 pub(crate) fn encode_color(c: Color) -> u32 {
109 match c {
110 Color::Default => SRC_DEFAULT << SRC_SHIFT,
111 Color::Named(i) => (SRC_NAMED << SRC_SHIFT) | i as u32,
112 Color::Indexed(i) => (SRC_INDEXED << SRC_SHIFT) | i as u32,
113 Color::Rgb(r, g, b) => {
114 (SRC_RGB << SRC_SHIFT) | ((r as u32) << 16) | ((g as u32) << 8) | b as u32
115 }
116 }
117 }
118
119 fn decode_color(word: u32) -> Color {
120 let payload = word & RGB_MASK;
121 match (word & SRC_MASK) >> SRC_SHIFT {
122 SRC_NAMED => Color::Named(payload as u8),
123 SRC_INDEXED => Color::Indexed(payload as u8),
124 SRC_RGB => Color::Rgb((payload >> 16) as u8, (payload >> 8) as u8, payload as u8),
125 _ => Color::Default,
126 }
127 }
128
129 pub(crate) fn encode_attrs(a: Attrs) -> u32 {
130 let mut bits = 0u32;
131 if a.bold {
132 bits |= ATTR_BOLD;
133 }
134 if a.italic {
135 bits |= ATTR_ITALIC;
136 }
137 if a.underline {
138 bits |= ATTR_UNDERLINE;
139 }
140 if a.reverse {
141 bits |= ATTR_REVERSE;
142 }
143 if a.dim {
144 bits |= ATTR_DIM;
145 }
146 if a.strikethrough {
147 bits |= ATTR_STRIKE;
148 }
149 bits
150 }
151
152 impl Cell {
153 pub fn new(c: char, fg: Color, bg: Color, attrs: Attrs) -> Self {
154 Self {
155 c_raw: c as u32,
156 fg_word: encode_color(fg) | encode_attrs(attrs),
157 bg_word: encode_color(bg),
158 }
159 }
160
161 /// The cell's base character, with any combining marks left behind.
162 ///
163 /// This is what the renderer draws and what word boundaries read. The marks
164 /// change how a cluster looks, not what it is or how wide it is, so nothing
165 /// laying out columns needs them.
166 #[inline]
167 pub fn c(&self) -> char {
168 // Grid only ever writes valid `char` values into `c_raw`.
169 char::from_u32(self.c_raw & CHAR_MASK).unwrap_or('\u{FFFD}')
170 }
171
172 /// Id of this cell's combining marks in the grid's table, or 0 for none.
173 #[inline]
174 pub fn marks_id(&self) -> u16 {
175 ((self.c_raw >> MARKS_SHIFT) & MARKS_LIMIT) as u16
176 }
177
178 #[inline]
179 pub(crate) fn with_marks(self, id: u16) -> Self {
180 Self {
181 c_raw: (self.c_raw & CHAR_MASK) | (u32::from(id) << MARKS_SHIFT),
182 ..self
183 }
184 }
185
186 #[inline]
187 pub fn fg(&self) -> Color {
188 decode_color(self.fg_word)
189 }
190
191 #[inline]
192 pub fn bg(&self) -> Color {
193 decode_color(self.bg_word)
194 }
195
196 pub fn attrs(&self) -> Attrs {
197 let b = self.fg_word;
198 Attrs {
199 bold: b & ATTR_BOLD != 0,
200 italic: b & ATTR_ITALIC != 0,
201 underline: b & ATTR_UNDERLINE != 0,
202 reverse: b & ATTR_REVERSE != 0,
203 dim: b & ATTR_DIM != 0,
204 strikethrough: b & ATTR_STRIKE != 0,
205 }
206 }
207
208 // Hot-path attribute readers — used by the renderer's per-frame fills
209 // scan, which touches every visible cell. Avoids materializing `Attrs`.
210 #[inline]
211 pub fn reverse(&self) -> bool {
212 self.fg_word & ATTR_REVERSE != 0
213 }
214
215 #[inline]
216 pub fn underline(&self) -> bool {
217 self.fg_word & ATTR_UNDERLINE != 0
218 }
219
220 /// Fast "does this cell need a bg fill drawn" check for the fills scan.
221 /// True when bg is not Default OR reverse is set.
222 #[inline]
223 pub fn has_bg(&self) -> bool {
224 (self.bg_word & SRC_MASK) != 0 || self.reverse()
225 }
226
227 /// Whether this cell holds a character that covers the column after it too.
228 #[inline]
229 pub fn is_wide(&self) -> bool {
230 self.bg_word & FLAG_WIDE != 0
231 }
232
233 /// Whether this cell is the second column of the character before it.
234 ///
235 /// A spacer is not a character of its own. Anything turning cells back into
236 /// text — copy, emit, word boundaries — has to skip it, or one `日` comes
237 /// out as a `日` and a space.
238 #[inline]
239 pub fn is_spacer(&self) -> bool {
240 self.bg_word & FLAG_SPACER != 0
241 }
242
243 /// How many columns this cell's character occupies: 2 on the lead of a
244 /// wide pair, 1 otherwise (a spacer included — it is one column, it just
245 /// is not its own character).
246 #[inline]
247 pub fn cols(&self) -> u16 {
248 if self.is_wide() { 2 } else { 1 }
249 }
250
251 /// The column a wide character could not fit into at the right edge.
252 ///
253 /// A spacer with no lead in front of it: it holds a column on screen and is
254 /// not a character, which is exactly what that column is.
255 pub(crate) fn pad() -> Self {
256 Self {
257 bg_word: FLAG_SPACER,
258 ..Self::default()
259 }
260 }
261
262 /// The lead of a wide pair, and the spacer that follows it.
263 pub(crate) fn wide_pair(c: char, fg_word: u32, bg_word: u32) -> (Self, Self) {
264 let bg_word = bg_word & !FLAG_WIDTH_MASK;
265 (
266 Self {
267 c_raw: c as u32,
268 fg_word,
269 bg_word: bg_word | FLAG_WIDE,
270 },
271 Self {
272 c_raw: ' ' as u32,
273 fg_word,
274 bg_word: bg_word | FLAG_SPACER,
275 },
276 )
277 }
278 }
279
280 /// The combining-mark sequences the cells refer to by id.
281 ///
282 /// Append-only and interned. Real text draws from a handful of distinct
283 /// sequences however much of it goes past — one `U+0301` is every acute accent
284 /// on screen — so this stays in the dozens and never needs freeing. Interning
285 /// the marks rather than whole clusters is what keeps it that small: the bases
286 /// they attach to are what vary.
287 #[derive(Debug, Default)]
288 pub(crate) struct MarkTable {
289 /// Id `n` is at index `n - 1`; id 0 means a cell has no marks and is never
290 /// stored.
291 seqs: Vec<Box<[char]>>,
292 ids: HashMap<Box<[char]>, u16>,
293 }
294
295 impl MarkTable {
296 /// The marks for `id`, or empty for id 0.
297 pub(crate) fn get(&self, id: u16) -> &[char] {
298 match id.checked_sub(1) {
299 Some(i) => self.seqs.get(i as usize).map_or(&[], |s| s),
300 None => &[],
301 }
302 }
303
304 /// The id for a sequence, interning it if it is new.
305 ///
306 /// `None` once the id space is exhausted, which is the caller's cue to drop
307 /// the mark. Refusing to store a mark keeps the cell as it was; there is no
308 /// id it could be given that would not mean somebody else's marks.
309 pub(crate) fn intern(&mut self, seq: &[char]) -> Option<u16> {
310 if let Some(id) = self.ids.get(seq) {
311 return Some(*id);
312 }
313 let id = u16::try_from(self.seqs.len() + 1).ok()?;
314 if u32::from(id) > MARKS_LIMIT {
315 return None;
316 }
317 let seq: Box<[char]> = seq.into();
318 self.seqs.push(seq.clone());
319 self.ids.insert(seq, id);
320 Some(id)
321 }
322 }
323
324 /// How many columns a character occupies, as the application computing its own
325 /// cursor moves will have counted it.
326 ///
327 /// Zero is a real answer here rather than a degenerate one: a combining mark
328 /// occupies no column of its own, it modifies the one before it. `place_char`
329 /// reads it as an instruction to amend rather than to place.
330 ///
331 /// A character with no width at all (a control) counts as one. `execute`
332 /// handles C0 so one should not reach a print, and one column is the answer
333 /// that leaves the cursor where the application put it if one does.
334 pub(crate) fn char_cols(c: char) -> u16 {
335 match unicode_width::UnicodeWidthChar::width(c) {
336 Some(2) => 2,
337 Some(0) => 0,
338 _ => 1,
339 }
340 }
341
342 impl Default for Cell {
343 fn default() -> Self {
344 Self {
345 c_raw: ' ' as u32,
346 fg_word: 0,
347 bg_word: 0,
348 }
349 }
350 }
351
352 const _: () = assert!(std::mem::size_of::<Cell>() == 12);
353
354 #[cfg(test)]
355 mod tests {
356 use crate::*;
357
358 #[test]
359 fn a_cell_is_still_twelve_bytes_and_still_copy() {
360 // The property the whole layout choice exists to protect: cells move by
361 // memcpy through resize, the ring origin and history.
362 assert_eq!(std::mem::size_of::<Cell>(), 12);
363 let a = Cell::default();
364 let b = a;
365 assert_eq!(a, b);
366 }
367 }
368