justerm_core/cell.rs
1//! The cell — one character position in the grid (see CONTEXT.md "Cell").
2
3use crate::color::Color;
4
5bitflags::bitflags! {
6 /// Per-cell flags: the standard SGR attributes plus layout markers.
7 ///
8 /// The high bits are intentionally left free so underline-style + underline
9 /// colour and an OSC 8 hyperlink id can be added later without a format
10 /// change (see `docs/architecture.md` "Cell").
11 ///
12 /// **No `#[non_exhaustive]` (#844): the question does not arise for a bitflags set.** New members
13 /// are bits inside the value, not fields, and the type is built through `empty()` / `from_bits`,
14 /// never by struct literal.
15 #[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
16 pub struct CellFlags: u16 {
17 // --- standard SGR attributes ---
18 const BOLD = 1 << 0;
19 const DIM = 1 << 1;
20 const ITALIC = 1 << 2;
21 const UNDERLINE = 1 << 3;
22 const BLINK = 1 << 4;
23 const INVERSE = 1 << 5;
24 const HIDDEN = 1 << 6;
25 const STRIKETHROUGH = 1 << 7;
26
27 // --- layout markers (not SGR): a width-2 glyph occupies two cells ---
28 /// The first cell of a width-2 glyph; holds the actual character.
29 const WIDE_CHAR = 1 << 8;
30 /// The trailing cell of a width-2 glyph. A distinct marker, *not* a
31 /// plain blank — overwrite, erase, selection, and cursor positioning all
32 /// depend on knowing this column belongs to the wide char to its left.
33 const WIDE_CHAR_SPACER = 1 << 9;
34 /// A row that soft-wrapped (auto-wrap) into the next — distinguishing it from a hard
35 /// CR/LF line-end so reflow (#7) can merge and re-split logical lines.
36 ///
37 /// **Wire-only.** The live grid holds this on the `Row` (`Grid::is_row_wrapped`); it used
38 /// to live here, where every whole-cell write and clear destroyed it and ordinary typing
39 /// in the last column silently split the logical line (#538). The wire has no per-row
40 /// slot, so it is derived back onto a span's last cell at encode time — which is why the
41 /// storage could move without a format change. On a cell read from the live grid this bit
42 /// is never set.
43 const WRAPLINE = 1 << 10;
44 // bits 11..=13 are the underline STYLE (#829) — a 3-bit field, not flags; read and
45 // written through `underline_style` / `set_underline_style`, never with `insert`.
46 // bits 14..=15 reserved (hyperlink id).
47 }
48}
49
50/// How a cell's underline is drawn — `SGR 4 : Ps` (#829).
51///
52/// **This is the storage, and `None` is a member of it.** There is no second boolean saying
53/// whether the cell is underlined: [`CellFlags::UNDERLINE`] survives as a *derived* view bit so
54/// existing consumers keep working, and the style is its only writer. So the two cannot disagree —
55/// which is not a stylistic preference but the defect three of the four references demonstrably
56/// pay for. alacritty's display layer inserts a plain `UNDERLINE` over a cell that already carries
57/// a curl and its renderer draws both rects; xterm.js has two readers that resolve the same
58/// conflict in opposite directions, pinned by a test; xterm leaves both bits set after
59/// `CSI 4m; CSI 21m` and lets each consumer pick a resolution. ghostty is the one where it is not
60/// representable — its `underline` is an `enum(u3)` with `none` among the members and there is no
61/// `underline: bool` — and that is the shape here.
62///
63/// Stored in the **content** word (bits 26..=28), not in a row side map: the packed cell is not
64/// full, so [the side-map
65/// invariant](https://github.com/kihyun1998/justerm/blob/master/docs/map/invariant/row-keyed-side-maps.md)
66/// does not apply, and packing is what makes the style ride along for free everywhere a cell
67/// moves — reflow, scroll, and the blank a wrapping wide glyph leaves behind. **One path needed an
68/// explicit carry and is worth naming rather than counting in**: `promote_cluster_to_wide` /
69/// `relocate_cluster_wide` *synthesise* the pair's spacer from the pen rather than moving it, so
70/// they take the style from the lead the same way they already take its extended attrs (ADR-0025
71/// D4). A refuting pass found that one; the other three are free.
72#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
73#[repr(u8)]
74pub enum UnderlineStyle {
75 /// Not underlined. The absence is a member rather than a separate flag.
76 #[default]
77 None = 0,
78 /// `SGR 4` or `4:1` — one straight line.
79 Single = 1,
80 /// Two straight lines — `4:2`, and also the legacy `SGR 21`, which is the **only** value with
81 /// a second spelling (#830). Both land on this field, so `SGR 24` clears either of them.
82 ///
83 /// The legacy form is not unanimous in the prior art and the spec is what settles it: `vte`
84 /// reads `21` as *cancel bold*, so an application meaning "stop bold" gets a double underline
85 /// here and keeps its bold. `SGR 22` is the arm that cancels bold.
86 Double = 2,
87 /// `4:3` — a curl. The mark #829 carries end to end.
88 Curly = 3,
89 /// `4:4` — a dotted line (#830). Drawn with a whole number of dots per cell, so the pattern
90 /// does not restart at a cell boundary.
91 Dotted = 4,
92 /// `4:5` — a dashed line (#830). One period per cell, with the dash split across the boundary
93 /// so adjacent cells' dashes join.
94 Dashed = 5,
95}
96
97impl UnderlineStyle {
98 /// The style for a raw 3-bit field, normalising anything outside the enum to
99 /// [`Single`](Self::Single) — the packing has eight representable values and six meanings, and
100 /// a total function here is what keeps `Cell`'s bit pattern canonical.
101 fn from_bits(v: u32) -> Self {
102 match v {
103 0 => Self::None,
104 2 => Self::Double,
105 3 => Self::Curly,
106 4 => Self::Dotted,
107 5 => Self::Dashed,
108 _ => Self::Single,
109 }
110 }
111}
112
113/// The underline style's field in the **view** (`CellFlags`), bits 11..=13.
114const CF_USTYLE_SHIFT: u32 = 11;
115const CF_USTYLE_MASK: u32 = 0b111 << CF_USTYLE_SHIFT;
116
117impl CellFlags {
118 /// The underline style this flag set carries.
119 pub fn underline_style(self) -> UnderlineStyle {
120 UnderlineStyle::from_bits((self.bits() as u32 & CF_USTYLE_MASK) >> CF_USTYLE_SHIFT)
121 }
122
123 /// Set the underline style, and [`UNDERLINE`](Self::UNDERLINE) with it.
124 ///
125 /// The single writer of both: setting a style arms the derived flag and clearing it disarms
126 /// the flag, so no caller can produce a set that disagrees with itself.
127 pub fn set_underline_style(&mut self, style: UnderlineStyle) {
128 let bits = (self.bits() as u32 & !CF_USTYLE_MASK) | ((style as u32) << CF_USTYLE_SHIFT);
129 *self = CellFlags::from_bits_retain(bits as u16);
130 self.set(CellFlags::UNDERLINE, style != UnderlineStyle::None);
131 }
132}
133
134// --- packed bit layout (#44) ----------------------------------------------
135//
136// Three 32-bit words mirroring xterm.js's `BufferLine` cell (verified against
137// `xtermjs/xterm.js@master` `src/common/buffer/Constants.ts`). The fg/bg colour
138// words are byte-identical to xterm's `Attributes` + `FgFlags`/`BgFlags`; the
139// content word keeps justerm's explicit layout-marker flags where xterm stores a
140// 2-bit `wcwidth` value (justerm's model is flag-based — the spacer markers are load-bearing for
141// overwrite/selection/reflow; WRAPLINE is wire-only, the live flag is on the `Row`, #538).
142//
143// content u32: codepoint(21) | COMBINED(1) | WIDE | SPACER | WRAP | reserved
144// fg/bg u32: colour value(24) | colour mode(2) | flags(6)
145//
146// COMBINED_PRESENT (content) and LINK_PRESENT (bg, xterm's HAS_EXTENDED slot) are
147// both live: combining clusters (#45) and OSC 8 hyperlink indices (#46) live in
148// per-row, column-keyed maps, and these bits gate every read of them. The cell is
149// now pure packed words — three u32, no `Option` field (the epic's 12 B target).
150
151const CODEPOINT_MASK: u32 = 0x001F_FFFF; // bits 0..21
152const C_COMBINED: u32 = 1 << 21; // a combining cluster lives in the row's map at this column (#45)
153const C_WIDE: u32 = 1 << 22;
154const C_SPACER: u32 = 1 << 23;
155const C_WRAP: u32 = 1 << 24;
156// The vacated column left when a width-2 glyph wraps off the right edge (#113):
157// a blank that holds no character but isn't a hard line-end. Unlike C_SPACER it
158// has *no wide lead to its left*, so the overwrite/erase repair paths (which key
159// off C_SPACER) must not treat it as one — it's a separate marker the text
160// extractors skip. Engine-internal: it stays in the content word and never
161// reaches `flags()` / the wire (a frame-mode consumer gets the already-correct
162// text, and the cell renders as the blank it is).
163const C_LEADING_SPACER: u32 = 1 << 25;
164const CONTENT_MARKER_MASK: u32 = C_WIDE | C_SPACER | C_WRAP;
165// The underline style (#829) — a 3-bit field, bits 26..=28 of the content word. It is deliberately
166// *outside* `CONTENT_MARKER_MASK`: that mask means "this column means something even though its
167// codepoint is a space", and a decoration does not, exactly as a colour does not (see
168// `Cell::is_blank`). Bits 29..=31 stay free.
169const C_USTYLE_SHIFT: u32 = 26;
170const C_USTYLE_MASK: u32 = 0b111 << C_USTYLE_SHIFT;
171
172const COLOR_VALUE_MASK: u32 = 0x00FF_FFFF; // bits 0..24
173const COLOR_MODE_SHIFT: u32 = 24; // bits 24..26
174const CM_DEFAULT: u32 = 0;
175const CM_INDEXED: u32 = 1;
176const CM_RGB: u32 = 2;
177
178// fg flags, bits 26..32 — xterm FgFlags order (HIDDEN == xterm INVISIBLE).
179const FG_INVERSE: u32 = 1 << 26;
180const FG_BOLD: u32 = 1 << 27;
181const FG_UNDERLINE: u32 = 1 << 28;
182const FG_BLINK: u32 = 1 << 29;
183const FG_HIDDEN: u32 = 1 << 30;
184const FG_STRIKE: u32 = 1 << 31;
185const FG_FLAG_MASK: u32 = FG_INVERSE | FG_BOLD | FG_UNDERLINE | FG_BLINK | FG_HIDDEN | FG_STRIKE;
186
187// bg flags, bits 26..28 — xterm BgFlags order.
188const BG_ITALIC: u32 = 1 << 26;
189const BG_DIM: u32 = 1 << 27;
190// LINK_PRESENT: an OSC 8 hyperlink URI lives in the row's link map at this
191// column (#46; the URI itself rather than a pool index since #628). Reuses xterm's `BgFlags.HAS_EXTENDED = 0x10000000` (bit 28)
192// exactly — in xterm this is a *shared* "extended attrs present" gate (link +
193// underline colour). justerm keeps the two concerns in *separate* per-row maps
194// (as combining and links are separate, #520/ADR-none), so it gates each with
195// its own bit rather than xterm's one shared object.
196const BG_LINK: u32 = 1 << 28;
197// UCOLOR_PRESENT (#520): a non-default underline colour (SGR 58) lives in the
198// row's ucolor map at this column. Its own presence bit, gating a separate map —
199// the 12-byte cell (three packed words) has no room for a fourth colour, so the
200// colour rides a side map exactly as the hyperlink does. **The underline style did NOT land
201// here** — this comment used to send the next reader to bits 30,31 for it, and two bits cannot
202// hold six values; it went to the CONTENT word at bits 26..=28 (#829). Bits 30,31 stay free for a
203// second extended attr.
204const BG_UCOLOR: u32 = 1 << 29;
205const BG_FLAG_MASK: u32 = BG_ITALIC | BG_DIM;
206
207/// Pack a colour reference into the low 26 bits of a colour word (mode + value);
208/// the high 6 bits are left for the SGR flags.
209fn pack_color(c: Color) -> u32 {
210 match c {
211 Color::Default => CM_DEFAULT << COLOR_MODE_SHIFT,
212 Color::Indexed(i) => (CM_INDEXED << COLOR_MODE_SHIFT) | i as u32,
213 Color::Rgb(r, g, b) => {
214 (CM_RGB << COLOR_MODE_SHIFT) | (r as u32) << 16 | (g as u32) << 8 | b as u32
215 }
216 }
217}
218
219/// Inverse of [`pack_color`] — reads only the mode + value bits, ignoring the
220/// flag bits that share the word.
221fn unpack_color(w: u32) -> Color {
222 match (w >> COLOR_MODE_SHIFT) & 0b11 {
223 CM_INDEXED => Color::Indexed((w & 0xFF) as u8),
224 CM_RGB => Color::Rgb((w >> 16) as u8, (w >> 8) as u8, w as u8),
225 _ => Color::Default, // CM_DEFAULT (and the unused mode 3) resolve to Default
226 }
227}
228
229/// Scatter a `CellFlags` bit set (as a `u32`) into the three words' flag-bit
230/// positions: `(content_markers, fg_flags, bg_flags)`. Branchless — each group is
231/// masked and shifted in one step. The `CellFlags` bit values are frozen by the
232/// wire format (`serialize` encodes `flags().bits()`), so the source positions are
233/// fixed; see the shift comments. One place for store / insert / remove to share.
234#[inline]
235fn flag_words(f: u32) -> (u32, u32, u32) {
236 // The underline style owns the underline (#829). A caller that set only `UNDERLINE` means a
237 // single one, so it is normalised here rather than stored as a styleless underline — which is
238 // both the single-owner rule and what keeps the packing canonical, the property `Cell`'s
239 // derived `Eq` is a bitwise compare because of.
240 let mut style = UnderlineStyle::from_bits((f & CF_USTYLE_MASK) >> CF_USTYLE_SHIFT);
241 if style == UnderlineStyle::None && f & 0x0008 != 0 {
242 style = UnderlineStyle::Single;
243 }
244 let content = ((f & 0x0700) << 14) // WIDE/SPACER/WRAP bits 8,9,10 -> 22,23,24
245 | ((style as u32) << C_USTYLE_SHIFT); // underline style -> 26,27,28
246 let fg = ((f & 0x0001) << 27) // BOLD bit 0 -> 27
247 | ((f & 0x0020) << 21) // INVERSE bit 5 -> 26
248 | ((f & 0x0010) << 25) // BLINK bit 4 -> 29
249 | ((f & 0x00C0) << 24); // HIDDEN/STRIKE bits 6,7 -> 30,31
250 // `FG_UNDERLINE` is deliberately **not written** (#829). The style in the content word is the
251 // single owner, and a mutation proved a derived copy here would be write-only: nothing reads
252 // it — `flags()` derives `UNDERLINE` from the style, and the wire carries
253 // `encode_color(cell.fg())`, a tagged colour, so the fg word's flag bits never leave the
254 // process. A duplicate nobody reads is still a duplicate: it is named like every authoritative
255 // flag beside it, so the next reader uses it, which is exactly how xterm.js ended up with two
256 // readers resolving the same question in opposite directions. The bit stays reserved for its
257 // xterm-mirror position and stays zero, cleared with the rest by `FG_FLAG_MASK`.
258 let bg = ((f & 0x0004) << 24) // ITALIC bit 2 -> 26
259 | ((f & 0x0002) << 26); // DIM bit 1 -> 27
260 (content, fg, bg)
261}
262
263/// One character position: a base glyph, fg/bg colour references, and flags.
264/// Combining marks (#45) and an OSC 8 hyperlink (#46) attach via per-row maps,
265/// signalled by the `COMBINED_PRESENT` / `LINK_PRESENT` bits — the cell itself is
266/// three packed words, no `Option` field. All access is through the accessor seam
267/// (#44); construct with [`Cell::from_parts`] or [`Cell::default`].
268///
269/// `Eq` is a derived bitwise compare, which is exact because the packing is
270/// canonical — every logical cell maps to one bit pattern (unused bits stay 0).
271///
272/// **No `#[non_exhaustive]` (#844).** Out-of-crate code builds one through `Cell::default()` rather
273/// than by literal, which is the consumer-chosen form of the same immunity to a new field — #843's
274/// rule (*an exhaustive type preserves the option to be forced*) as it lands on a struct.
275#[derive(Clone, Copy, PartialEq, Eq)]
276pub struct Cell {
277 content: u32,
278 fg: u32,
279 bg: u32,
280}
281
282impl Default for Cell {
283 fn default() -> Self {
284 // The packed form of a blank cell: ' ' (U+0020) in the codepoint field,
285 // every other word zero (Default colours, no flags, no combining/link
286 // bits). Built directly rather than through `from_parts` so scroll/erase
287 // blanking — which constructs defaults by the rowful — stays a cheap copy.
288 Cell {
289 content: ' ' as u32,
290 fg: 0,
291 bg: 0,
292 }
293 }
294}
295
296impl core::fmt::Debug for Cell {
297 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
298 f.debug_struct("Cell")
299 .field("c", &self.c())
300 .field("fg", &self.fg())
301 .field("bg", &self.bg())
302 .field("flags", &self.flags())
303 .field("combined", &self.is_combined())
304 .field("linked", &self.is_linked())
305 // Every cell-state bit that lives OUTSIDE `flags()` — the three presence
306 // bits and the engine-internal leading-spacer marker. `flags()` above
307 // covers the rest; these do not appear in it, so omitting one makes two
308 // unequal cells print as two identical lines. Not hypothetical: #531 read
309 // exactly that way in a failing `assert_eq!(frame, …)` (the ucolor bit was
310 // missing here), and `leading_spacer` reproduced it a second time in the
311 // same session. A bit added to this struct is added here too.
312 .field("ucolored", &self.is_ucolored())
313 .field("leading_spacer", &self.is_leading_spacer())
314 .finish()
315 }
316}
317
318impl Cell {
319 /// Assemble a cell from its logical parts. The single construction seam —
320 /// `Pen::cell` and the wire decoder funnel through here, so the bit-packing
321 /// lives in exactly one place (#44).
322 pub fn from_parts(c: char, fg: Color, bg: Color, flags: CellFlags) -> Self {
323 let mut cell = Cell {
324 content: c as u32, // a `char` is <= U+10FFFF, so it fits the 21-bit field
325 fg: pack_color(fg),
326 bg: pack_color(bg),
327 };
328 cell.store_flags(flags);
329 cell
330 }
331
332 /// Replace the flag bits across the three words from `flags`, preserving the
333 /// codepoint, colours, and the dormant presence bits. The inverse is
334 /// [`Cell::flags`].
335 fn store_flags(&mut self, flags: CellFlags) {
336 let (content, fg, bg) = flag_words(flags.bits() as u32);
337 self.content = (self.content & !(CONTENT_MARKER_MASK | C_USTYLE_MASK)) | content;
338 self.fg = (self.fg & !FG_FLAG_MASK) | fg;
339 self.bg = (self.bg & !BG_FLAG_MASK) | bg;
340 }
341
342 /// The base code point.
343 pub fn c(&self) -> char {
344 char::from_u32(self.content & CODEPOINT_MASK)
345 .expect("codepoint bits always hold a valid char")
346 }
347
348 /// Does this cell hold no **content** — no glyph and no layout marker?
349 ///
350 /// A blank the app never wrote and one it erased to a coloured background are both blank: the
351 /// background is not content. But a wide-char spacer, a leading-spacer wrap artefact, or a
352 /// combining-cluster carrier all *mean* something at their column even though their base
353 /// code point is a space — they are not blank. Used by reflow to find where a hard-ended
354 /// line ends (mirrors xterm.js `getTrimmedLength` / alacritty `line_length`, which likewise
355 /// test content, not the background); it says nothing about a cell's colour.
356 pub fn is_blank(&self) -> bool {
357 // Space codepoint, and none of the content-marker bits set. `content` holds the codepoint
358 // plus the COMBINED / WIDE / SPACER / WRAP / LEADING_SPACER markers, so a single check on
359 // the whole word covers every "means something here" case at once.
360 self.content & (CODEPOINT_MASK | CONTENT_MARKER_MASK | C_COMBINED | C_LEADING_SPACER)
361 == ' ' as u32
362 }
363
364 /// How this cell's underline is drawn (#829). [`UnderlineStyle::None`] means not underlined —
365 /// there is no separate boolean to consult, and [`CellFlags::UNDERLINE`] is derived from this.
366 pub fn underline_style(&self) -> UnderlineStyle {
367 UnderlineStyle::from_bits((self.content & C_USTYLE_MASK) >> C_USTYLE_SHIFT)
368 }
369
370 /// The foreground colour reference.
371 pub fn fg(&self) -> Color {
372 unpack_color(self.fg)
373 }
374
375 /// The background colour reference.
376 pub fn bg(&self) -> Color {
377 unpack_color(self.bg)
378 }
379
380 /// The cell's flags (SGR attributes + layout markers), reassembled from the
381 /// three words — the branchless inverse of `Cell::store_flags`.
382 pub fn flags(&self) -> CellFlags {
383 // `UNDERLINE` is *derived* from the style rather than read from `FG_UNDERLINE` (#829), so a
384 // reader can never be handed a set where the flag and the style disagree.
385 let style = (self.content & C_USTYLE_MASK) >> C_USTYLE_SHIFT;
386 let bits = ((self.content & CONTENT_MARKER_MASK) >> 14) // 22,23,24 -> 8,9,10
387 | (style << CF_USTYLE_SHIFT) // 26,27,28 -> 11,12,13
388 | if style == 0 { 0 } else { 0x0008 } // UNDERLINE, derived
389 | ((self.fg & FG_BOLD) >> 27) // 27 -> 0
390 | ((self.fg & FG_INVERSE) >> 21) // 26 -> 5
391 | ((self.fg & FG_BLINK) >> 25) // 29 -> 4
392 | ((self.fg & (FG_HIDDEN | FG_STRIKE)) >> 24) // 30,31 -> 6,7
393 | ((self.bg & BG_ITALIC) >> 24) // 26 -> 2
394 | ((self.bg & BG_DIM) >> 26); // 27 -> 1
395 CellFlags::from_bits_retain(bits as u16)
396 }
397
398 /// Does this column carry combining marks? When true, the cluster lives in
399 /// the row's combining map at this column (#45) — a flag-gated cache: never
400 /// read the map without first checking this bit.
401 pub fn is_combined(&self) -> bool {
402 self.content & C_COMBINED != 0
403 }
404
405 /// Does this column carry an OSC 8 hyperlink? When true, the URI lives in the
406 /// row's link map at this column (#46; the URI itself rather than an index into a
407 /// buffer-wide pool since #628) — flag-gated like combining: never read the link
408 /// map without first checking this bit.
409 pub fn is_linked(&self) -> bool {
410 self.bg & BG_LINK != 0
411 }
412
413 /// Does this column carry a non-default underline colour (SGR 58, #520)? When
414 /// true, the `Color` reference lives in the row's ucolor map at this column —
415 /// flag-gated exactly like the hyperlink: never read the ucolor map without
416 /// first checking this bit.
417 pub fn is_ucolored(&self) -> bool {
418 self.bg & BG_UCOLOR != 0
419 }
420
421 /// Overwrite the base code point, preserving the layout markers.
422 pub fn set_c(&mut self, c: char) {
423 self.content = (self.content & !CODEPOINT_MASK) | c as u32;
424 }
425
426 /// Overwrite the background colour (the BCE erase fill, #16), preserving the
427 /// bg-word flag bits.
428 pub fn set_bg(&mut self, bg: Color) {
429 self.bg = pack_color(bg) | (self.bg & !(COLOR_VALUE_MASK | (0b11 << COLOR_MODE_SHIFT)));
430 }
431
432 /// Mark (or unmark) this column as carrying combining marks in the row map.
433 pub fn set_combined(&mut self, on: bool) {
434 if on {
435 self.content |= C_COMBINED;
436 } else {
437 self.content &= !C_COMBINED;
438 }
439 }
440
441 /// Mark (or unmark) this column as carrying an OSC 8 hyperlink in the row map.
442 pub fn set_linked(&mut self, on: bool) {
443 if on {
444 self.bg |= BG_LINK;
445 } else {
446 self.bg &= !BG_LINK;
447 }
448 }
449
450 /// Mark (or unmark) this column as carrying a non-default underline colour in
451 /// the row's ucolor map (#520). Mirror of [`Cell::set_linked`].
452 pub fn set_ucolored(&mut self, on: bool) {
453 if on {
454 self.bg |= BG_UCOLOR;
455 } else {
456 self.bg &= !BG_UCOLOR;
457 }
458 }
459
460 /// Add the given flags (leaving the others set). Sets the word bits directly —
461 /// no round-trip through `flags()`/`store_flags`.
462 ///
463 /// **The underline is a field, not a bit, so it is *replaced* rather than OR-ed (#829).**
464 /// Bit-OR is the right operation for every other member and the wrong one for a 3-bit value:
465 /// OR-ing `Dotted` (4) into a `Single` (1) cell yields `Dashed` (5), a style neither the
466 /// caller nor the parser asked for, and a bit pattern no canonical cell has — which would
467 /// break the property this type's derived `Eq` is a bitwise compare because of. A completeness
468 /// pass found this; nothing in this repository reached it, but `Cell` is published.
469 pub fn insert_flags(&mut self, flags: CellFlags) {
470 let (content, fg, bg) = flag_words(flags.bits() as u32);
471 self.content |= content & !C_USTYLE_MASK;
472 self.fg |= fg;
473 self.bg |= bg;
474 // `flag_words` already normalised a bare `UNDERLINE` to `Single`, so "names the underline
475 // at all" is exactly "the normalised style is not `None`".
476 let named = UnderlineStyle::from_bits((content & C_USTYLE_MASK) >> C_USTYLE_SHIFT);
477 if named != UnderlineStyle::None {
478 self.set_underline_style(named);
479 }
480 }
481
482 /// Clear the given flags (leaving the others as they are).
483 ///
484 /// **Naming the underline clears the whole field (#829)**, whichever way it was named — the
485 /// `UNDERLINE` flag or a style value. Masking the bits off instead would turn one style into
486 /// another (clearing `UNDERLINE`, which normalises to `Single` = `0b001`, subtracts a bit from
487 /// `Curly` = `0b011` and leaves `Double`), so a method documented as clearing a flag would
488 /// return a cell that is still underlined, in a style no input can produce.
489 pub fn remove_flags(&mut self, flags: CellFlags) {
490 let (content, fg, bg) = flag_words(flags.bits() as u32);
491 self.content &= !(content & !C_USTYLE_MASK);
492 self.fg &= !fg;
493 self.bg &= !bg;
494 let named = UnderlineStyle::from_bits((content & C_USTYLE_MASK) >> C_USTYLE_SHIFT);
495 if named != UnderlineStyle::None {
496 self.set_underline_style(UnderlineStyle::None);
497 }
498 }
499
500 /// Set the underline style on this cell, arming or disarming the derived `UNDERLINE` view bit
501 /// with it. The one writer of the field on a built cell.
502 pub fn set_underline_style(&mut self, style: UnderlineStyle) {
503 self.content = (self.content & !C_USTYLE_MASK) | ((style as u32) << C_USTYLE_SHIFT);
504 }
505
506 /// Reset to a blank **default** cell — default background included.
507 ///
508 /// That is rarely what a terminal operation wants on its own: a blank the engine creates
509 /// carries the current background (BCE for an erase, and the same for a structural repair,
510 /// #530). Callers pair this with `set_bg`; `Term::free_cell` and the erase paths are the
511 /// places that do. Using it bare leaves an uncoloured notch in a coloured run.
512 pub fn reset(&mut self) {
513 *self = Cell::default();
514 }
515
516 /// Is this the lead cell of a width-2 glyph? Direct content-bit query — the
517 /// hot overwrite/erase/reflow paths use this instead of reconstructing the
518 /// full `flags()` to test one marker.
519 pub fn is_wide(&self) -> bool {
520 self.content & C_WIDE != 0
521 }
522
523 /// Is this the trailing spacer cell of a width-2 glyph?
524 pub fn is_wide_spacer(&self) -> bool {
525 self.content & C_SPACER != 0
526 }
527
528 /// Is this the blank column vacated when a wide glyph wrapped off the right
529 /// edge (#113)? It holds no character; unlike a trailing spacer it has no
530 /// wide lead to its left, so only the *text* extractors skip it.
531 pub fn is_leading_spacer(&self) -> bool {
532 self.content & C_LEADING_SPACER != 0
533 }
534
535 /// Does this column hold no text — either half of a wide glyph's trailing
536 /// spacer or a wide-wrap leading spacer? Used by the text extractors (search,
537 /// selection text, logical lines) to skip non-character columns.
538 pub fn is_spacer(&self) -> bool {
539 self.content & (C_SPACER | C_LEADING_SPACER) != 0
540 }
541
542 /// Drop the leading-spacer marker, leaving the cell otherwise untouched.
543 ///
544 /// The marker claims two things at once, and it has to go when **either** stops holding, or
545 /// the text extractors keep skipping a column that is now a real blank: the row still
546 /// soft-wraps (`Term::end_wrap` owns that half — #538, #540), and the continuation still
547 /// begins with the wide lead that could not fit (`Term::repair_wrap_artefact_above` owns that
548 /// one — #534). Clearing is deliberately one-way: nothing here re-arms the marker, because a
549 /// wide glyph typed at column 0 of the next row did not *wrap* from anywhere.
550 pub fn clear_leading_spacer(&mut self) {
551 self.content &= !C_LEADING_SPACER;
552 }
553
554 /// Mark this column as the leading spacer of a wrapped wide glyph.
555 ///
556 /// **Records** that the column is blank; it does not make it so. The caller must have
557 /// written the blank first — this only ORs a marker onto whatever cell is there. Setting
558 /// it over a live glyph leaves a cell the text extractors skip while a renderer still
559 /// draws it, which is exactly the defect #528 fixed (`Term::vacate_for_wrap` is the one
560 /// place that establishes the precondition; reflow is the other set site, #533).
561 pub fn set_leading_spacer(&mut self) {
562 self.content |= C_LEADING_SPACER;
563 }
564
565 /// Does this **wire** cell end a soft-wrapped row? See `CellFlags::WRAPLINE` — on the live
566 /// grid this is always false and `Grid::is_row_wrapped` is the question to ask (#538).
567 pub fn is_wrapline(&self) -> bool {
568 self.content & C_WRAP != 0
569 }
570}
571
572#[cfg(test)]
573mod tests {
574 use super::{Cell, CellFlags, UnderlineStyle};
575 use crate::color::Color;
576
577 /// Size pin: slice C moves `link` out of the cell into the row's link map (a
578 /// cell now signals a hyperlink with only the `LINK_PRESENT` bg bit), so `Cell`
579 /// is **12 bytes** — three packed `u32` words, matching xterm.js's `BufferLine`
580 /// cell. This is the epic's target (#43): combining and link both ride per-row
581 /// maps, the cell is pure packed words. Flood throughput is
582 /// memory-bandwidth-bound, so this size is touched on every print/scroll-blank.
583 /// [#42, #46]
584 #[test]
585 fn cell_is_12_bytes() {
586 assert_eq!(std::mem::size_of::<Cell>(), 12);
587 }
588
589 /// The packing must be lossless: every colour reference read back equal in
590 /// both the fg and bg word, including the tag-distinguished trio that must not
591 /// collapse (`Default` / `Indexed(0)` / `Rgb(0,0,0)`).
592 #[test]
593 fn every_colour_round_trips_in_both_words() {
594 let colours = [
595 Color::Default,
596 Color::Indexed(0),
597 Color::Indexed(255),
598 Color::Rgb(0, 0, 0),
599 Color::Rgb(255, 128, 1),
600 ];
601 for &fg in &colours {
602 for &bg in &colours {
603 let cell = Cell::from_parts('x', fg, bg, CellFlags::empty());
604 assert_eq!(cell.fg(), fg, "fg {fg:?} / bg {bg:?}");
605 assert_eq!(cell.bg(), bg, "fg {fg:?} / bg {bg:?}");
606 }
607 }
608 }
609
610 /// Every flag bit — SGR attributes (split across the fg/bg words) and the
611 /// layout markers (in the content word) — round-trips, alone and combined.
612 #[test]
613 fn every_flag_round_trips() {
614 // `UNDERLINE` is the one flag that deliberately does **not** round-trip as given (#829).
615 // The underline style is the single owner and `None` is one of its members, so a cell
616 // built with the flag alone is a cell with a *single* underline and reads back carrying
617 // that style — "underlined with no style" is not a state this model has. That is what
618 // keeps the packing canonical, which this type's derived `Eq` is a bitwise compare
619 // because of, and it is asserted here rather than accommodated so the normalisation
620 // cannot be mistaken for a leak.
621 let mut normalised_underline = CellFlags::UNDERLINE;
622 normalised_underline.set_underline_style(UnderlineStyle::Single);
623
624 let all = CellFlags::all();
625 for bit in all.iter() {
626 let cell = Cell::from_parts('x', Color::Default, Color::Default, bit);
627 let expected = if bit == CellFlags::UNDERLINE {
628 normalised_underline
629 } else {
630 bit
631 };
632 assert_eq!(cell.flags(), expected, "single {bit:?}");
633 }
634 let cell = Cell::from_parts('x', Color::Default, Color::Default, all);
635 assert_eq!(
636 cell.flags(),
637 all.union(normalised_underline),
638 "all flags at once",
639 );
640 }
641
642 /// The codepoint occupies 21 bits — the full Unicode range, up to the
643 /// maximum scalar value, survives alongside flags set in the same word.
644 #[test]
645 fn codepoint_round_trips_to_the_unicode_max() {
646 for c in ['a', ' ', '한', '🦀', '\u{10FFFF}'] {
647 let cell = Cell::from_parts(c, Color::Default, Color::Default, CellFlags::WIDE_CHAR);
648 assert_eq!(cell.c(), c, "codepoint {c:?}");
649 assert!(cell.flags().contains(CellFlags::WIDE_CHAR));
650 }
651 }
652
653 /// The combining-presence bit (content word) and link-presence bit (bg word)
654 /// are independent of each other, of the codepoint/markers, of the colours, and
655 /// of the SGR flags — toggling one must disturb none of the others.
656 #[test]
657 fn combined_and_linked_bits_are_independent() {
658 let mut cell = Cell::from_parts(
659 'e',
660 Color::Indexed(3),
661 Color::Rgb(1, 2, 3),
662 CellFlags::WIDE_CHAR | CellFlags::DIM,
663 );
664 assert!(!cell.is_combined());
665 assert!(!cell.is_linked());
666
667 cell.set_combined(true);
668 cell.set_linked(true);
669 assert!(cell.is_combined() && cell.is_linked());
670 // Everything else survives both bits being set.
671 assert_eq!(cell.c(), 'e');
672 assert_eq!(cell.fg(), Color::Indexed(3));
673 assert_eq!(
674 cell.bg(),
675 Color::Rgb(1, 2, 3),
676 "link bit shares the bg word"
677 );
678 assert!(cell.flags().contains(CellFlags::WIDE_CHAR | CellFlags::DIM));
679
680 cell.set_linked(false);
681 assert!(cell.is_combined() && !cell.is_linked());
682 cell.set_combined(false);
683 assert!(!cell.is_combined() && !cell.is_linked());
684 assert_eq!(
685 cell.bg(),
686 Color::Rgb(1, 2, 3),
687 "bg colour intact after clearing"
688 );
689
690 let spacer = Cell::from_parts(
691 ' ',
692 Color::Default,
693 Color::Default,
694 CellFlags::WIDE_CHAR_SPACER,
695 );
696 assert!(spacer.is_wide_spacer());
697 assert!(!Cell::default().is_wide_spacer());
698 }
699
700 /// The underline-colour presence bit (#520) is its own bg-word bit, independent
701 /// of the link bit that shares the word and of the bg colour value — toggling
702 /// it disturbs neither, and it does NOT grow the cell (the 12-byte pin above
703 /// still holds because the colour rides a side map, not the cell).
704 #[test]
705 fn the_ucolor_presence_bit_is_independent_of_the_link_bit_and_bg_colour() {
706 let mut cell = Cell::from_parts('u', Color::Default, Color::Rgb(1, 2, 3), CellFlags::DIM);
707 assert!(!cell.is_ucolored());
708 assert!(!cell.is_linked());
709
710 cell.set_ucolored(true);
711 cell.set_linked(true);
712 assert!(cell.is_ucolored() && cell.is_linked());
713 // The bg colour and the DIM flag (both in the bg word) survive both bits.
714 assert_eq!(cell.bg(), Color::Rgb(1, 2, 3));
715 assert!(cell.flags().contains(CellFlags::DIM));
716
717 // Clearing one leaves the other and the colour intact.
718 cell.set_linked(false);
719 assert!(cell.is_ucolored() && !cell.is_linked());
720 cell.set_ucolored(false);
721 assert!(!cell.is_ucolored());
722 assert_eq!(
723 cell.bg(),
724 Color::Rgb(1, 2, 3),
725 "bg colour intact after clearing"
726 );
727 }
728}