//! The 12-byte display cell: its colours, its attributes, and the table that //! holds the combining-mark sequences cells refer to by id. //! //! The layout rationale lives on [`Cell`] itself. Nothing here knows about the //! grid; the grid is what owns a [`MarkTable`] and hands ids out. use std::collections::HashMap; /// A terminal color. /// /// [`Color::Default`] means "resolve at render time to the theme's default fg /// or bg" — kept out of `Rgb` so we don't lose the "this cell was never /// styled" signal (matters for e.g. transparent backgrounds). #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum Color { Default, Named(u8), Indexed(u8), Rgb(u8, u8, u8), } /// SGR-set attributes for a cell. #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] pub struct Attrs { pub bold: bool, pub italic: bool, pub underline: bool, pub reverse: bool, pub dim: bool, pub strikethrough: bool, } /// One cell of the display grid — 12 bytes, matching foot's layout. /// /// Three `u32`s so the struct aligns to 4 (a `u64` would pad to 16). Each /// color word holds a 24-bit RGB payload, a 2-bit source tag, and a 6-bit /// attribute half — the attribute bits live in `fg_word`, `bg_word`'s upper /// 6 bits are spare. /// /// Layout of `fg_word` / `bg_word`: /// - bits 0–23: color payload — RGB if `src == Rgb`, low byte = palette index if Named/Indexed /// - bits 24–25: source tag (`SRC_DEFAULT`/`NAMED`/`INDEXED`/`RGB`) /// - bits 26–31: attribute flags (fg_word only; bg_word width flags + spare) /// /// Layout of `c_raw`: /// - bits 0–20: the base character. A `char` is 21 bits, so this is the whole /// of one and [`Cell::c`] is a mask away. /// - bits 21–31: id into the grid's mark table, 0 meaning none. /// /// Keeping the base character INLINE and interning only the marks after it is /// what makes combining marks cost nothing to everything that does not care: /// the renderer's glyph lookup, the fills scan and word boundaries all read /// `c()` and never learn the table exists. It also keeps the cell `Copy` and /// memcpy-able, which is what lets `resize_buf`, the ring's origin advance and /// the scrollback rewrap move cells around without knowing any of this. #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] #[repr(C)] pub struct Cell { pub(crate) c_raw: u32, pub(crate) fg_word: u32, pub(crate) bg_word: u32, } /// A `char` is 21 bits; `c_raw`'s low 21 hold one whole. const CHAR_MASK: u32 = (1 << 21) - 1; const MARKS_SHIFT: u32 = 21; /// Eleven bits of mark-sequence id, 0 reserved for "no marks". Distinct mark /// SEQUENCES in real text run to dozens — it is distinct clusters that run to /// thousands, and those are not what is interned here — so 2047 is not a /// ceiling anything is expected to reach. const MARKS_LIMIT: u32 = (1 << 11) - 1; /// How many combining marks one cell will accumulate. /// /// A stream of combining marks is unbounded input, and a cluster long enough to /// need more than this is not text anyone is reading. Past the cap the mark is /// dropped rather than the cell rewritten. pub(crate) const MAX_MARKS: usize = 8; const SRC_DEFAULT: u32 = 0; const SRC_NAMED: u32 = 1; const SRC_INDEXED: u32 = 2; const SRC_RGB: u32 = 3; const SRC_SHIFT: u32 = 24; const SRC_MASK: u32 = 0b11 << SRC_SHIFT; const RGB_MASK: u32 = 0x00FF_FFFF; // Width flags, on `bg_word`'s spare upper bits. A double-width character is one // cell holding the character with `FLAG_WIDE` set, followed by one holding a // blank with `FLAG_SPACER` set. The pair is always adjacent and always in that // order; `heal_pair` is what keeps that true when a write lands on half of one. // // The spacer carries the lead's colours so a background fill covers both halves // with no seam, and holds a blank so every reader that draws or copies a cell's // character already does the right thing with it without being taught to. const FLAG_WIDE: u32 = 1 << 26; const FLAG_SPACER: u32 = 1 << 27; const FLAG_WIDTH_MASK: u32 = FLAG_WIDE | FLAG_SPACER; const ATTR_BOLD: u32 = 1 << 26; const ATTR_ITALIC: u32 = 1 << 27; const ATTR_UNDERLINE: u32 = 1 << 28; const ATTR_REVERSE: u32 = 1 << 29; const ATTR_DIM: u32 = 1 << 30; const ATTR_STRIKE: u32 = 1 << 31; pub(crate) fn encode_color(c: Color) -> u32 { match c { Color::Default => SRC_DEFAULT << SRC_SHIFT, Color::Named(i) => (SRC_NAMED << SRC_SHIFT) | i as u32, Color::Indexed(i) => (SRC_INDEXED << SRC_SHIFT) | i as u32, Color::Rgb(r, g, b) => { (SRC_RGB << SRC_SHIFT) | ((r as u32) << 16) | ((g as u32) << 8) | b as u32 } } } fn decode_color(word: u32) -> Color { let payload = word & RGB_MASK; match (word & SRC_MASK) >> SRC_SHIFT { SRC_NAMED => Color::Named(payload as u8), SRC_INDEXED => Color::Indexed(payload as u8), SRC_RGB => Color::Rgb((payload >> 16) as u8, (payload >> 8) as u8, payload as u8), _ => Color::Default, } } pub(crate) fn encode_attrs(a: Attrs) -> u32 { let mut bits = 0u32; if a.bold { bits |= ATTR_BOLD; } if a.italic { bits |= ATTR_ITALIC; } if a.underline { bits |= ATTR_UNDERLINE; } if a.reverse { bits |= ATTR_REVERSE; } if a.dim { bits |= ATTR_DIM; } if a.strikethrough { bits |= ATTR_STRIKE; } bits } impl Cell { pub fn new(c: char, fg: Color, bg: Color, attrs: Attrs) -> Self { Self { c_raw: c as u32, fg_word: encode_color(fg) | encode_attrs(attrs), bg_word: encode_color(bg), } } /// The cell's base character, with any combining marks left behind. /// /// This is what the renderer draws and what word boundaries read. The marks /// change how a cluster looks, not what it is or how wide it is, so nothing /// laying out columns needs them. #[inline] pub fn c(&self) -> char { // Grid only ever writes valid `char` values into `c_raw`. char::from_u32(self.c_raw & CHAR_MASK).unwrap_or('\u{FFFD}') } /// Id of this cell's combining marks in the grid's table, or 0 for none. #[inline] pub fn marks_id(&self) -> u16 { ((self.c_raw >> MARKS_SHIFT) & MARKS_LIMIT) as u16 } #[inline] pub(crate) fn with_marks(self, id: u16) -> Self { Self { c_raw: (self.c_raw & CHAR_MASK) | (u32::from(id) << MARKS_SHIFT), ..self } } #[inline] pub fn fg(&self) -> Color { decode_color(self.fg_word) } #[inline] pub fn bg(&self) -> Color { decode_color(self.bg_word) } pub fn attrs(&self) -> Attrs { let b = self.fg_word; Attrs { bold: b & ATTR_BOLD != 0, italic: b & ATTR_ITALIC != 0, underline: b & ATTR_UNDERLINE != 0, reverse: b & ATTR_REVERSE != 0, dim: b & ATTR_DIM != 0, strikethrough: b & ATTR_STRIKE != 0, } } // Hot-path attribute readers — used by the renderer's per-frame fills // scan, which touches every visible cell. Avoids materializing `Attrs`. #[inline] pub fn reverse(&self) -> bool { self.fg_word & ATTR_REVERSE != 0 } #[inline] pub fn underline(&self) -> bool { self.fg_word & ATTR_UNDERLINE != 0 } /// Fast "does this cell need a bg fill drawn" check for the fills scan. /// True when bg is not Default OR reverse is set. #[inline] pub fn has_bg(&self) -> bool { (self.bg_word & SRC_MASK) != 0 || self.reverse() } /// Whether this cell holds a character that covers the column after it too. #[inline] pub fn is_wide(&self) -> bool { self.bg_word & FLAG_WIDE != 0 } /// Whether this cell is the second column of the character before it. /// /// A spacer is not a character of its own. Anything turning cells back into /// text — copy, emit, word boundaries — has to skip it, or one `日` comes /// out as a `日` and a space. #[inline] pub fn is_spacer(&self) -> bool { self.bg_word & FLAG_SPACER != 0 } /// How many columns this cell's character occupies: 2 on the lead of a /// wide pair, 1 otherwise (a spacer included — it is one column, it just /// is not its own character). #[inline] pub fn cols(&self) -> u16 { if self.is_wide() { 2 } else { 1 } } /// The column a wide character could not fit into at the right edge. /// /// A spacer with no lead in front of it: it holds a column on screen and is /// not a character, which is exactly what that column is. pub(crate) fn pad() -> Self { Self { bg_word: FLAG_SPACER, ..Self::default() } } /// The lead of a wide pair, and the spacer that follows it. pub(crate) fn wide_pair(c: char, fg_word: u32, bg_word: u32) -> (Self, Self) { let bg_word = bg_word & !FLAG_WIDTH_MASK; ( Self { c_raw: c as u32, fg_word, bg_word: bg_word | FLAG_WIDE, }, Self { c_raw: ' ' as u32, fg_word, bg_word: bg_word | FLAG_SPACER, }, ) } } /// The combining-mark sequences the cells refer to by id. /// /// Append-only and interned. Real text draws from a handful of distinct /// sequences however much of it goes past — one `U+0301` is every acute accent /// on screen — so this stays in the dozens and never needs freeing. Interning /// the marks rather than whole clusters is what keeps it that small: the bases /// they attach to are what vary. #[derive(Debug, Default)] pub(crate) struct MarkTable { /// Id `n` is at index `n - 1`; id 0 means a cell has no marks and is never /// stored. seqs: Vec>, ids: HashMap, u16>, } impl MarkTable { /// The marks for `id`, or empty for id 0. pub(crate) fn get(&self, id: u16) -> &[char] { match id.checked_sub(1) { Some(i) => self.seqs.get(i as usize).map_or(&[], |s| s), None => &[], } } /// The id for a sequence, interning it if it is new. /// /// `None` once the id space is exhausted, which is the caller's cue to drop /// the mark. Refusing to store a mark keeps the cell as it was; there is no /// id it could be given that would not mean somebody else's marks. pub(crate) fn intern(&mut self, seq: &[char]) -> Option { if let Some(id) = self.ids.get(seq) { return Some(*id); } let id = u16::try_from(self.seqs.len() + 1).ok()?; if u32::from(id) > MARKS_LIMIT { return None; } let seq: Box<[char]> = seq.into(); self.seqs.push(seq.clone()); self.ids.insert(seq, id); Some(id) } } /// How many columns a character occupies, as the application computing its own /// cursor moves will have counted it. /// /// Zero is a real answer here rather than a degenerate one: a combining mark /// occupies no column of its own, it modifies the one before it. `place_char` /// reads it as an instruction to amend rather than to place. /// /// A character with no width at all (a control) counts as one. `execute` /// handles C0 so one should not reach a print, and one column is the answer /// that leaves the cursor where the application put it if one does. pub(crate) fn char_cols(c: char) -> u16 { match unicode_width::UnicodeWidthChar::width(c) { Some(2) => 2, Some(0) => 0, _ => 1, } } impl Default for Cell { fn default() -> Self { Self { c_raw: ' ' as u32, fg_word: 0, bg_word: 0, } } } const _: () = assert!(std::mem::size_of::() == 12); #[cfg(test)] mod tests { use crate::*; #[test] fn a_cell_is_still_twelve_bytes_and_still_copy() { // The property the whole layout choice exists to protect: cells move by // memcpy through resize, the ring origin and history. assert_eq!(std::mem::size_of::(), 12); let a = Cell::default(); let b = a; assert_eq!(a, b); } }