Skip to main content

rio_vt/crosswords/
square.rs

1// Copyright (c) 2023-present, Raphael Amorim.
2//
3// This source code is licensed under the MIT license found in the
4// LICENSE file in the root directory of this source tree.
5//
6// In rio it has been rewritten as a packed `u64`. The previous design held
7// `c`, `fg`, `bg`, `flags`, and an `Option<Arc<CellExtra>>` inline (24 bytes
8// per cell + per-cell heap allocations for "extras"). All variable-sized
9// data now lives in per-grid side tables (`StyleSet`, `ExtrasTable`); the
10// cell itself is 8 bytes.
11
12use crate::crosswords::grid::GridSquare;
13use crate::crosswords::style::{StyleId, DEFAULT_STYLE_ID};
14use crate::crosswords::Column;
15use crate::crosswords::Row;
16use bitflags::bitflags;
17use std::sync::atomic::{AtomicU32, Ordering};
18use std::sync::Arc;
19
20// ---------------------------------------------------------------------------
21// Bit layout for Square(u64)
22//
23// bits 0..20 (21): codepoint (Unicode scalar value, max 0x10_FFFF)
24// OR low bits of bg color when content_tag != Codepoint
25// bits 21..22 (2): wide (Wide enum)
26// bits 23..29 (7): per-cell flag bits (CellFlags), incl WRAPLINE at bit 0
27// bits 30..31 (2): content_tag (NEW)
28// 0 = Codepoint (text cell, use style_id below)
29// 1 = BgPalette (bg-only cell, palette index in 32..39)
30// 2 = BgRgb (bg-only cell, RGB packed in 32..55)
31// 3 = reserved
32// bits 32..47 (16): style_id (when tag == Codepoint)
33// bg palette idx in low 8 (when tag == BgPalette)
34// bg RGB.r:g in low 16 (when tag == BgRgb)
35// bits 48..63 (16): extras_id (when tag == Codepoint)
36// bg RGB.b in low 8 (when tag == BgRgb)
37//
38// The bg-only encoding (BgPalette / BgRgb) is the trick: cells that
39// represent a colored background with no text don't need a style table
40// lookup at all, which is the dominant cost for large filled regions
41// (selection, padding, blank lines after `clear`, color blocks).
42// ---------------------------------------------------------------------------
43
44const CODEPOINT_SHIFT: u64 = 0;
45const CODEPOINT_MASK: u64 = (1 << 21) - 1;
46
47const WIDE_SHIFT: u64 = 21;
48const WIDE_MASK: u64 = 0b11 << WIDE_SHIFT;
49
50const CELL_FLAGS_SHIFT: u64 = 23;
51const CELL_FLAGS_MASK: u64 = 0x7F << CELL_FLAGS_SHIFT; // 7 bits incl WRAPLINE
52
53const CONTENT_TAG_SHIFT: u64 = 30;
54
55const STYLE_ID_SHIFT: u64 = 32;
56const STYLE_ID_MASK: u64 = 0xFFFF << STYLE_ID_SHIFT;
57
58const EXTRAS_ID_SHIFT: u64 = 48;
59const EXTRAS_ID_MASK: u64 = 0xFFFF << EXTRAS_ID_SHIFT;
60
61// Bg-color slot reuse (only valid when content_tag != Codepoint).
62const BG_PALETTE_SHIFT: u64 = 32;
63const BG_PALETTE_MASK: u64 = 0xFF << BG_PALETTE_SHIFT;
64
65const BG_RGB_R_SHIFT: u64 = 32;
66const BG_RGB_G_SHIFT: u64 = 40;
67const BG_RGB_B_SHIFT: u64 = 48;
68
69/// Wide-character state for a cell. Encoded in 2 bits.
70#[repr(u8)]
71#[derive(Clone, Copy, Debug, Eq, PartialEq)]
72pub enum Wide {
73    /// Normal single-cell character.
74    Narrow = 0,
75    /// First cell of a double-wide character.
76    Wide = 1,
77    /// Second cell of a double-wide character.
78    Spacer = 2,
79    /// Trailing spacer at end of a soft-wrapped line indicating a wide
80    /// character continues on the next line.
81    LeadingSpacer = 3,
82}
83
84impl Wide {
85    #[inline]
86    fn from_bits(bits: u64) -> Wide {
87        match (bits >> WIDE_SHIFT) & 0b11 {
88            0 => Wide::Narrow,
89            1 => Wide::Wide,
90            2 => Wide::Spacer,
91            _ => Wide::LeadingSpacer,
92        }
93    }
94}
95
96/// Discriminator for what the cell's payload represents. Default is
97/// `Codepoint`, the standard text cell. The bg-only variants are an
98/// optimization for cells that carry only a background color (selection,
99/// padding, color rectangles, blank lines after `clear`) — they encode the
100/// color directly in the cell, skipping the style table entirely.
101#[repr(u8)]
102#[derive(Clone, Copy, Debug, Eq, PartialEq)]
103pub enum ContentTag {
104    /// Standard text cell. Codepoint in bits 0..20, style_id in bits 32..47.
105    Codepoint = 0,
106    /// Bg-only cell with a palette-indexed background.
107    /// Palette index in bits 32..39.
108    BgPalette = 1,
109    /// Bg-only cell with an RGB background.
110    /// RGB packed in bits 32..55 (R, G, B).
111    BgRgb = 2,
112}
113
114impl ContentTag {
115    /// Decode the content tag from a raw `Square` u64. Public so render
116    /// hot loops can read the cell once and dispatch on the tag without
117    /// going through the `Square::content_tag()` method (which reloads
118    /// the cell from memory if the optimizer can't prove it aliases).
119    #[inline(always)]
120    pub fn from_bits(bits: u64) -> ContentTag {
121        match (bits >> CONTENT_TAG_SHIFT) & 0b11 {
122            0 => ContentTag::Codepoint,
123            1 => ContentTag::BgPalette,
124            _ => ContentTag::BgRgb,
125        }
126    }
127}
128
129bitflags! {
130 /// Per-cell flags that DON'T live in the style table. SGR-related
131 /// attributes (bold, italic, underline, etc.) live in `StyleFlags`.
132    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
133    pub struct CellFlags: u8 {
134 /// Soft-wrap continuation marker on the last cell of a wrapped line.
135        const WRAPLINE         = 1 << 0;
136 /// Cell carries graphics data (sixel / iTerm2 inline image piece).
137 /// Look up the actual graphic in `Grid::extras_table` via `extras_id`.
138        // Bit 1 is reserved (was the per-cell graphics flag before
139        // sixel/iTerm2 moved to placements).
140        const RESERVED_GRAPHICS = 1 << 1;
141 /// Cell carries hyperlink metadata. Lookup via extras_id.
142        const HYPERLINK        = 1 << 2;
143 /// Cell carries multi-codepoint grapheme cluster. Lookup via extras_id.
144        const GRAPHEME         = 1 << 3;
145    }
146}
147
148/// Counter for hyperlinks without explicit ID.
149static HYPERLINK_ID_SUFFIX: AtomicU32 = AtomicU32::new(0);
150
151#[derive(Debug, Clone, PartialEq, Eq, Hash)]
152pub struct Hyperlink {
153    inner: Arc<HyperlinkInner>,
154}
155
156impl Hyperlink {
157    pub fn new<T: ToString>(id: Option<T>, uri: T) -> Self {
158        let inner = Arc::new(HyperlinkInner::new(id, uri));
159        Self { inner }
160    }
161
162    pub fn id(&self) -> &str {
163        &self.inner.id
164    }
165
166    pub fn uri(&self) -> &str {
167        &self.inner.uri
168    }
169}
170
171#[derive(Debug, PartialEq, Eq, Hash)]
172struct HyperlinkInner {
173    id: String,
174    uri: String,
175}
176
177impl HyperlinkInner {
178    pub fn new<T: ToString>(id: Option<T>, uri: T) -> Self {
179        let id = match id {
180            Some(id) => id.to_string(),
181            None => {
182                let mut id = HYPERLINK_ID_SUFFIX
183                    .fetch_add(1, Ordering::Relaxed)
184                    .to_string();
185                id.push_str("_rio");
186                id
187            }
188        };
189
190        Self {
191            id,
192            uri: uri.to_string(),
193        }
194    }
195}
196
197/// Index into `Grid::extras_table`. `0` means "no extras".
198pub type ExtrasId = u16;
199
200/// Storage for the rare per-cell data that used to live inside `CellExtra`.
201/// Allocated only for cells that need it; pooled in a `Vec` on the grid.
202#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
203pub struct Extras {
204    pub zerowidth: Vec<char>,
205    pub hyperlink: Option<Hyperlink>,
206}
207
208impl Extras {
209    #[inline]
210    pub fn is_empty(&self) -> bool {
211        self.zerowidth.is_empty() && self.hyperlink.is_none()
212    }
213}
214
215#[repr(transparent)]
216#[derive(Clone, Copy, Debug, PartialEq, Eq)]
217pub struct Square(u64);
218
219impl Default for Square {
220    #[inline]
221    fn default() -> Square {
222        Square(0)
223    }
224}
225
226impl Square {
227    /// Create a cell with the given codepoint and the default style/extras.
228    #[inline]
229    pub fn from_char(c: char) -> Self {
230        let mut s = Square(0);
231        s.set_c(c);
232        s
233    }
234
235    /// Build a cell by OR-ing a codepoint into a `template` whose codepoint
236    /// field is zero (i.e. style / extras / flags only). One store, no
237    /// read-modify-write — used by the print-run fast path where the
238    /// template is hoisted out of the per-cell loop. Because a codepoint is
239    /// 21 bits at shift 0 and the template's codepoint bits are zero, the OR
240    /// is exactly `default() + set_style_id + set_extras_id + set_cell_flags
241    /// + set_c(c)`.
242    #[inline]
243    pub fn from_template(template: Square, c: char) -> Self {
244        let cp = c as u32 as u64;
245        debug_assert!(cp <= CODEPOINT_MASK, "codepoint exceeds 21 bits");
246        debug_assert!(
247            template.0 & CODEPOINT_MASK == 0,
248            "template must have a zero codepoint field"
249        );
250        Square(template.0 | cp)
251    }
252
253    /// Read the underlying packed bits. Used by render hot loops that want
254    /// to extract multiple fields from a single cell load — calling the
255    /// individual accessors would otherwise reload the cell from memory
256    /// each time the optimizer can't prove they alias.
257    #[inline(always)]
258    pub fn raw(self) -> u64 {
259        self.0
260    }
261
262    /// The cell's base character only.
263    ///
264    /// This is **not** the cell's full text: combining marks, ZWJ joiners and
265    /// variation selectors live in [`Extras::zerowidth`], so reading only this
266    /// silently drops them. Use `Grid::cell_text` when you want the text.
267    #[inline]
268    pub fn c(self) -> char {
269        let cp = ((self.0 >> CODEPOINT_SHIFT) & CODEPOINT_MASK) as u32;
270        // Safety: we only ever store valid Unicode scalar values via set_c.
271        char::from_u32(cp).unwrap_or('\0')
272    }
273
274    #[inline]
275    pub fn set_c(&mut self, c: char) {
276        let cp = c as u32 as u64;
277        debug_assert!(cp <= CODEPOINT_MASK, "codepoint exceeds 21 bits");
278        self.0 = (self.0 & !CODEPOINT_MASK) | ((cp & CODEPOINT_MASK) << CODEPOINT_SHIFT);
279    }
280
281    /// True if overwriting this cell needs wide-char neighbour cleanup (it is
282    /// a wide lead or its trailing spacer). A cheap bit test so a print run
283    /// can bulk-classify its target cells before a vectorized fill.
284    #[inline]
285    pub fn needs_wide_cleanup(self) -> bool {
286        matches!(self.wide(), Wide::Wide | Wide::Spacer)
287    }
288
289    #[inline]
290    pub fn wide(self) -> Wide {
291        Wide::from_bits(self.0)
292    }
293
294    #[inline]
295    pub fn set_wide(&mut self, w: Wide) {
296        self.0 = (self.0 & !WIDE_MASK) | ((w as u64) << WIDE_SHIFT);
297    }
298
299    #[inline]
300    pub fn cell_flags(self) -> CellFlags {
301        let bits = ((self.0 & CELL_FLAGS_MASK) >> CELL_FLAGS_SHIFT) as u8;
302        CellFlags::from_bits_truncate(bits)
303    }
304
305    #[inline]
306    pub fn set_cell_flags(&mut self, f: CellFlags) {
307        self.0 = (self.0 & !CELL_FLAGS_MASK) | ((f.bits() as u64) << CELL_FLAGS_SHIFT);
308    }
309
310    #[inline]
311    pub fn insert_cell_flag(&mut self, f: CellFlags) {
312        let mut cur = self.cell_flags();
313        cur.insert(f);
314        self.set_cell_flags(cur);
315    }
316
317    #[inline]
318    pub fn remove_cell_flag(&mut self, f: CellFlags) {
319        let mut cur = self.cell_flags();
320        cur.remove(f);
321        self.set_cell_flags(cur);
322    }
323
324    #[inline]
325    pub fn contains_cell_flag(self, f: CellFlags) -> bool {
326        self.cell_flags().contains(f)
327    }
328
329    /// Read the raw style id bits.
330    ///
331    /// **The caller must check `content_tag()` first.** For non-Codepoint
332    /// cells the upper 32 bits hold the bg color encoding, not a style id.
333    /// We deliberately do not branch on the tag here so the renderer hot
334    /// loop stays branchless when it has already established the cell is
335    /// a Codepoint cell.
336    #[inline(always)]
337    pub fn style_id(self) -> StyleId {
338        ((self.0 & STYLE_ID_MASK) >> STYLE_ID_SHIFT) as StyleId
339    }
340
341    #[inline]
342    pub fn set_style_id(&mut self, id: StyleId) {
343        self.0 = (self.0 & !STYLE_ID_MASK) | ((id as u64) << STYLE_ID_SHIFT);
344    }
345
346    /// Tag-checked variant of [`Self::style_id`], safe on any cell:
347    /// `None` for bg-only cells, whose upper bits hold color bits
348    /// rather than a style id. Deliberately an exhaustive match so a
349    /// new `ContentTag` variant forces a decision here.
350    #[inline]
351    pub fn style_id_checked(self) -> Option<StyleId> {
352        match self.content_tag() {
353            ContentTag::Codepoint => Some(self.style_id()),
354            ContentTag::BgPalette | ContentTag::BgRgb => None,
355        }
356    }
357
358    /// Tag-checked variant of [`Self::extras_id`], safe on any cell.
359    /// Exhaustive for the same reason as [`Self::style_id_checked`].
360    #[inline]
361    pub fn extras_id_checked(self) -> Option<ExtrasId> {
362        match self.content_tag() {
363            ContentTag::Codepoint => self.extras_id(),
364            ContentTag::BgPalette | ContentTag::BgRgb => None,
365        }
366    }
367
368    /// Read the cell's extras id, if any.
369    ///
370    /// **The caller must check `content_tag()` first** if the cell might be
371    /// a bg-only cell — those reuse the upper 32 bits for the bg color, so
372    /// `extras_id()` would return garbage. For Codepoint cells (the
373    /// overwhelming majority) this is a single bit extract.
374    #[inline(always)]
375    pub fn extras_id(self) -> Option<ExtrasId> {
376        let id = ((self.0 & EXTRAS_ID_MASK) >> EXTRAS_ID_SHIFT) as ExtrasId;
377        if id == 0 {
378            None
379        } else {
380            Some(id)
381        }
382    }
383
384    #[inline]
385    pub fn set_extras_id(&mut self, id: Option<ExtrasId>) {
386        let bits = id.unwrap_or(0) as u64;
387        self.0 = (self.0 & !EXTRAS_ID_MASK) | (bits << EXTRAS_ID_SHIFT);
388    }
389
390    #[inline]
391    pub fn content_tag(self) -> ContentTag {
392        ContentTag::from_bits(self.0)
393    }
394
395    /// Convert this cell into a bg-only cell holding a palette-indexed
396    /// background. Drops codepoint, wide, style_id, and extras_id; preserves
397    /// per-cell flags (so e.g. `WRAPLINE` survives a clear-to-end-of-line).
398    #[inline]
399    pub fn set_bg_palette(&mut self, idx: u8) {
400        let preserved = self.0 & CELL_FLAGS_MASK;
401        self.0 = preserved
402            | ((ContentTag::BgPalette as u64) << CONTENT_TAG_SHIFT)
403            | ((idx as u64) << BG_PALETTE_SHIFT);
404    }
405
406    /// Convert this cell into a bg-only cell holding an RGB background.
407    #[inline]
408    pub fn set_bg_rgb(&mut self, r: u8, g: u8, b: u8) {
409        let preserved = self.0 & CELL_FLAGS_MASK;
410        self.0 = preserved
411            | ((ContentTag::BgRgb as u64) << CONTENT_TAG_SHIFT)
412            | ((r as u64) << BG_RGB_R_SHIFT)
413            | ((g as u64) << BG_RGB_G_SHIFT)
414            | ((b as u64) << BG_RGB_B_SHIFT);
415    }
416
417    /// Read the inline-encoded palette index. Only meaningful when
418    /// `content_tag() == BgPalette` — call after the tag check.
419    #[inline]
420    pub fn bg_palette_index(self) -> u8 {
421        ((self.0 & BG_PALETTE_MASK) >> BG_PALETTE_SHIFT) as u8
422    }
423
424    /// Read the inline-encoded RGB. Only meaningful when
425    /// `content_tag() == BgRgb`.
426    #[inline]
427    pub fn bg_rgb(self) -> (u8, u8, u8) {
428        (
429            ((self.0 >> BG_RGB_R_SHIFT) & 0xFF) as u8,
430            ((self.0 >> BG_RGB_G_SHIFT) & 0xFF) as u8,
431            ((self.0 >> BG_RGB_B_SHIFT) & 0xFF) as u8,
432        )
433    }
434
435    /// True if this cell encodes its background inline (no text content,
436    /// no style table lookup needed). Used by the renderer hot loop.
437    #[inline]
438    pub fn is_bg_only(self) -> bool {
439        !matches!(self.content_tag(), ContentTag::Codepoint)
440    }
441
442    /// Clear all per-cell state. Used by `clear_wide` and similar.
443    #[inline]
444    pub fn clear(&mut self) {
445        *self = Square(0);
446    }
447
448    /// Reset the cell using a template. Preserves the template's style id;
449    /// drops any extras the cell currently has (caller must free the slot).
450    #[inline]
451    pub fn reset(&mut self, template: Square) {
452        let new = Square(0).with_style_id(template.style_id());
453        *self = new;
454    }
455
456    /// Builder helper for tests.
457    #[inline]
458    pub fn with_style_id(mut self, id: StyleId) -> Self {
459        self.set_style_id(id);
460        self
461    }
462
463    #[inline]
464    pub fn is_default(self) -> bool {
465        self.0 == 0
466    }
467
468    #[inline]
469    pub fn is_wide(self) -> bool {
470        matches!(self.wide(), Wide::Wide)
471    }
472
473    #[inline]
474    pub fn is_spacer(self) -> bool {
475        matches!(self.wide(), Wide::Spacer)
476    }
477
478    #[inline]
479    pub fn is_leading_spacer(self) -> bool {
480        matches!(self.wide(), Wide::LeadingSpacer)
481    }
482
483    #[inline]
484    pub fn wrapline(self) -> bool {
485        self.contains_cell_flag(CellFlags::WRAPLINE)
486    }
487
488    #[inline]
489    pub fn set_wrapline(&mut self, on: bool) {
490        if on {
491            self.insert_cell_flag(CellFlags::WRAPLINE);
492        } else {
493            self.remove_cell_flag(CellFlags::WRAPLINE);
494        }
495    }
496
497    #[inline]
498    pub fn has_extras(self) -> bool {
499        self.extras_id_checked().is_some()
500    }
501
502    #[inline]
503    pub fn has_grapheme(self) -> bool {
504        self.contains_cell_flag(CellFlags::GRAPHEME)
505    }
506
507    #[inline]
508    pub fn has_hyperlink(self) -> bool {
509        self.contains_cell_flag(CellFlags::HYPERLINK)
510    }
511}
512
513impl GridSquare for Square {
514    #[inline]
515    fn carries_style(&self) -> bool {
516        matches!(self.style_id_checked(), Some(id) if id != DEFAULT_STYLE_ID)
517    }
518
519    #[inline]
520    fn is_empty(&self) -> bool {
521        if self.0 == 0 {
522            return true;
523        }
524        // A cell with an inline bg is NOT empty even if everything else
525        // looks default.
526        if self.is_bg_only() {
527            return false;
528        }
529        (self.c() == '\0' || self.c() == '\t')
530            && self.style_id() == DEFAULT_STYLE_ID
531            && self.extras_id().is_none()
532            && !self.contains_cell_flag(CellFlags::WRAPLINE)
533            && matches!(self.wide(), Wide::Narrow)
534    }
535
536    #[inline]
537    fn reset(&mut self, template: &Self) {
538        let style_id = template.style_id();
539        *self = Square(0).with_style_id(style_id);
540    }
541}
542
543pub trait LineLength {
544    /// Calculate the occupied TEXTUAL line length: up to the last cell
545    /// carrying a character or extras. Deliberately narrower than
546    /// `GridSquare::is_empty`, which also counts bg-only cells and
547    /// styled blanks as occupied; this feeds selection/copy, where
548    /// colored blanks contribute no text.
549    fn line_length(&self) -> Column;
550}
551
552impl LineLength for Row<Square> {
553    fn line_length(&self) -> Column {
554        let mut length = Column(0);
555
556        if self[Column(self.len() - 1)].wrapline() {
557            return Column(self.len());
558        }
559
560        for (index, cell) in self[..].iter().rev().enumerate() {
561            if cell.c() != '\0' || cell.has_extras() {
562                length = Column(self.len() - index);
563                break;
564            }
565        }
566
567        length
568    }
569}
570
571pub trait ResetDiscriminant<T> {
572    /// Value based on which equality for the reset will be determined.
573    fn discriminant(&self) -> T;
574}
575
576impl<T: Copy> ResetDiscriminant<T> for T {
577    fn discriminant(&self) -> T {
578        *self
579    }
580}
581
582impl ResetDiscriminant<StyleId> for Square {
583    fn discriminant(&self) -> StyleId {
584        self.style_id()
585    }
586}
587
588#[cfg(test)]
589mod tests {
590    use super::*;
591
592    use std::mem;
593
594    use crate::crosswords::grid::row::Row;
595    use crate::crosswords::pos::Column;
596
597    #[test]
598    fn line_length_ignores_bg_only_cells_regardless_of_color() {
599        // A bg-only cell's upper bits hold color channels where a
600        // Codepoint cell keeps its extras id; line_length must not read
601        // them as one (a nonzero blue channel used to count the cell as
602        // text while zero dropped it). Bg-only cells carry no text, so
603        // they never extend the textual line length.
604        let mut row: Row<Square> = Row::new(4);
605        row[Column(1)].set_bg_rgb(10, 20, 0);
606        assert_eq!(row.line_length(), Column(0));
607
608        let mut row: Row<Square> = Row::new(4);
609        row[Column(1)].set_bg_rgb(10, 20, 30);
610        assert_eq!(row.line_length(), Column(0));
611
612        // Text after a bg-only cell still counts normally.
613        let mut row: Row<Square> = Row::new(4);
614        row[Column(0)].set_bg_rgb(10, 20, 30);
615        row[Column(1)] = Square::from_char('x');
616        assert_eq!(row.line_length(), Column(2));
617    }
618
619    #[test]
620    fn checked_id_accessors_reject_bg_only_cells() {
621        let mut sq = Square::default();
622        sq.set_style_id(7);
623        assert_eq!(sq.style_id_checked(), Some(7));
624
625        let mut bg = Square::default();
626        bg.set_bg_rgb(1, 2, 3);
627        assert_eq!(bg.style_id_checked(), None);
628        assert_eq!(bg.extras_id_checked(), None);
629        assert!(!bg.has_extras());
630    }
631
632    #[test]
633    fn square_is_eight_bytes() {
634        // The whole point of this rewrite.
635        assert_eq!(mem::size_of::<Square>(), 8);
636    }
637
638    #[test]
639    fn codepoint_round_trip() {
640        let mut s = Square(0);
641        s.set_c('🦀');
642        assert_eq!(s.c(), '🦀');
643        s.set_c('a');
644        assert_eq!(s.c(), 'a');
645        s.set_c('\0');
646        assert_eq!(s.c(), '\0');
647    }
648
649    #[test]
650    fn style_id_round_trip() {
651        let mut s = Square(0);
652        s.set_style_id(42);
653        assert_eq!(s.style_id(), 42);
654        s.set_style_id(0xFFFF);
655        assert_eq!(s.style_id(), 0xFFFF);
656    }
657
658    #[test]
659    fn extras_id_round_trip() {
660        let mut s = Square(0);
661        assert_eq!(s.extras_id(), None);
662        s.set_extras_id(Some(7));
663        assert_eq!(s.extras_id(), Some(7));
664        s.set_extras_id(None);
665        assert_eq!(s.extras_id(), None);
666    }
667
668    #[test]
669    fn wide_round_trip() {
670        let mut s = Square(0);
671        for w in [Wide::Narrow, Wide::Wide, Wide::Spacer, Wide::LeadingSpacer] {
672            s.set_wide(w);
673            assert_eq!(s.wide(), w);
674        }
675    }
676
677    #[test]
678    fn cell_flags_round_trip() {
679        let mut s = Square(0);
680        s.insert_cell_flag(CellFlags::WRAPLINE | CellFlags::GRAPHEME);
681        assert!(s.wrapline());
682        assert!(s.has_grapheme());
683        assert!(!s.has_hyperlink());
684        s.remove_cell_flag(CellFlags::WRAPLINE);
685        assert!(!s.wrapline());
686        assert!(s.has_grapheme());
687    }
688
689    #[test]
690    fn fields_are_independent() {
691        let mut s = Square(0);
692        s.set_c('Z');
693        s.set_style_id(0x1234);
694        s.set_extras_id(Some(0x5678));
695        s.set_wide(Wide::Wide);
696        s.insert_cell_flag(CellFlags::WRAPLINE);
697        assert_eq!(s.c(), 'Z');
698        assert_eq!(s.style_id(), 0x1234);
699        assert_eq!(s.extras_id(), Some(0x5678));
700        assert_eq!(s.wide(), Wide::Wide);
701        assert!(s.wrapline());
702    }
703
704    #[test]
705    fn bg_palette_round_trip() {
706        let mut s = Square(0);
707        s.set_bg_palette(42);
708        assert_eq!(s.content_tag(), ContentTag::BgPalette);
709        assert!(s.is_bg_only());
710        assert_eq!(s.bg_palette_index(), 42);
711        // bg-only cells have no codepoint
712        assert_eq!(s.c(), '\0');
713        // NOTE: `style_id()` and `extras_id()` are intentionally
714        // branchless on the bg-only/codepoint discriminant — they
715        // read raw bits regardless of content_tag. For a bg-only
716        // cell those bits hold the bg color encoding, not a style/
717        // extras id. Production code (the renderer hot loop) checks
718        // `content_tag()` first, so it never asks `style_id()` of a
719        // bg-only cell. We don't assert `style_id() == DEFAULT_STYLE_ID`
720        // here for that reason. (See comments on `Square::style_id`.)
721    }
722
723    #[test]
724    fn bg_rgb_round_trip() {
725        let mut s = Square(0);
726        s.set_bg_rgb(0x12, 0x34, 0x56);
727        assert_eq!(s.content_tag(), ContentTag::BgRgb);
728        assert!(s.is_bg_only());
729        assert_eq!(s.bg_rgb(), (0x12, 0x34, 0x56));
730        assert_eq!(s.c(), '\0');
731        // Same caveat as `bg_palette_round_trip`: branchless
732        // accessors read raw bits, so `style_id()` of a BgRgb cell
733        // returns garbage. The renderer always checks `content_tag()`
734        // first.
735    }
736
737    #[test]
738    fn bg_only_preserves_wrapline() {
739        let mut s = Square(0);
740        s.set_wrapline(true);
741        s.set_bg_rgb(1, 2, 3);
742        assert!(s.wrapline());
743        assert_eq!(s.bg_rgb(), (1, 2, 3));
744    }
745
746    #[test]
747    fn bg_only_cells_are_not_empty() {
748        let mut s = Square(0);
749        s.set_bg_palette(7);
750        assert!(!<Square as crate::crosswords::grid::GridSquare>::is_empty(
751            &s
752        ));
753    }
754
755    #[test]
756    fn line_length_works() {
757        let mut row = Row::<Square>::new(10);
758        row[Column(5)].set_c('a');
759        assert_eq!(row.line_length(), Column(6));
760    }
761
762    #[test]
763    fn line_length_works_with_wrapline() {
764        let mut row = Row::<Square>::new(10);
765        row[Column(9)].set_wrapline(true);
766        assert_eq!(row.line_length(), Column(10));
767    }
768}