Skip to main content

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`](https://github.com/kihyun1998/justerm/blob/master/docs/architecture.md) "Cell").
11    ///
12    /// **No `#[non_exhaustive]` ([#844](https://github.com/kihyun1998/justerm/issues/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 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. 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`.
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](https://github.com/kihyun1998/justerm/blob/master/docs/adr/0025-row-and-wide-pair-cell-state-ownership.md)
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. 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.
88    Curly = 3,
89    /// `4:4` — a dotted line. 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. 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 and an OSC 8 hyperlink 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/// 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](https://github.com/kihyun1998/justerm/issues/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](https://github.com/kihyun1998/justerm/issues/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.
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. [`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 — 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 — the URI itself, not an index into a buffer-wide
407    /// pool — 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)? 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), 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. 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.**
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**, 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
510    /// repair). 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? 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), and the continuation still
547    /// begins with the wide lead that could not fit (`Term::repair_wrap_artefact_above` owns that
548    /// one). 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 the defect this precondition exists to prevent (`Term::vacate_for_wrap` is the one
560    /// place that establishes the precondition; reflow is the other set site).
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.
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}