Skip to main content

ftui_render/
cell.rs

1#![forbid(unsafe_code)]
2
3//! Cell types and invariants.
4//!
5//! The `Cell` is the fundamental unit of the terminal grid. Each cell occupies
6//! exactly **16 bytes** to ensure optimal cache utilization (4 cells per 64-byte
7//! cache line) and enable fast SIMD comparisons.
8//!
9//! # Layout (16 bytes, non-negotiable)
10//!
11//! ```text
12//! Cell {
13//!     content: CellContent,  // 4 bytes - char or GraphemeId
14//!     fg: PackedRgba,        // 4 bytes - foreground color
15//!     bg: PackedRgba,        // 4 bytes - background color
16//!     attrs: CellAttrs,      // 4 bytes - style flags + link ID
17//! }
18//! ```
19//!
20//! # Why 16 Bytes?
21//!
22//! - 4 cells per 64-byte cache line (perfect fit)
23//! - Single 128-bit SIMD comparison
24//! - No heap allocation for 99% of cells
25//! - 24 bytes wastes cache, 32 bytes doubles bandwidth
26
27use crate::char_width;
28
29/// Grapheme ID: reference to an interned string in [`GraphemePool`].
30///
31/// # Layout
32///
33/// ```text
34/// [30-27: width (4 bits)][26-16: generation (11 bits)][15-0: pool slot (16 bits)]
35/// ```
36///
37/// # Capacity
38///
39/// - Pool slots: 65,536 (16 bits = 64K entries)
40/// - Generation: 2048 versions (11 bits) for stale access detection
41/// - Width range: 0-15 (4 bits)
42///
43/// # Design Rationale
44///
45/// - 16 bits for slot (64K) is sufficient for any single frame's unique graphemes.
46/// - 11 bits for generation allows detecting access to reused slots (fixing the ABA problem).
47/// - 4 bits for width allows display widths 0-15.
48/// - Total 31 bits leaves bit 31 for `CellContent` type discrimination.
49#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
50#[repr(transparent)]
51pub struct GraphemeId(u32);
52
53impl GraphemeId {
54    /// Maximum slot index (16 bits).
55    pub const MAX_SLOT: u32 = 0xFFFF;
56
57    /// Maximum width (4 bits).
58    pub const MAX_WIDTH: u8 = 15;
59
60    /// Maximum generation (11 bits).
61    pub const MAX_GENERATION: u16 = 2047;
62
63    /// Create a new `GraphemeId` from slot index, generation, and display width.
64    ///
65    /// # Panics
66    ///
67    /// Panics in debug mode if `slot > MAX_SLOT` or `width > MAX_WIDTH`.
68    #[inline]
69    pub const fn new(slot: u32, generation: u16, width: u8) -> Self {
70        debug_assert!(slot <= Self::MAX_SLOT, "slot overflow");
71        debug_assert!(generation <= Self::MAX_GENERATION, "generation overflow");
72        debug_assert!(width <= Self::MAX_WIDTH, "width overflow");
73        Self(
74            (slot & Self::MAX_SLOT)
75                | (((generation as u32) & 0x7FF) << 16)
76                | ((width as u32) << 27),
77        )
78    }
79
80    /// Extract the pool slot index (0-64K).
81    #[inline]
82    pub const fn slot(self) -> usize {
83        (self.0 & Self::MAX_SLOT) as usize
84    }
85
86    /// Extract the generation counter (0-2047).
87    #[inline]
88    pub const fn generation(self) -> u16 {
89        ((self.0 >> 16) & 0x7FF) as u16
90    }
91
92    /// Extract the display width (0-15).
93    #[inline]
94    pub const fn width(self) -> usize {
95        ((self.0 >> 27) & 0x0F) as usize
96    }
97
98    /// Raw u32 value for storage in `CellContent`.
99    #[inline]
100    pub const fn raw(self) -> u32 {
101        self.0
102    }
103
104    /// Reconstruct from a raw u32.
105    #[inline]
106    pub const fn from_raw(raw: u32) -> Self {
107        Self(raw)
108    }
109}
110
111impl core::fmt::Debug for GraphemeId {
112    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
113        f.debug_struct("GraphemeId")
114            .field("slot", &self.slot())
115            .field("gen", &self.generation())
116            .field("width", &self.width())
117            .finish()
118    }
119}
120
121/// Cell content: either a direct Unicode char or a reference to a grapheme cluster.
122///
123/// # Encoding Scheme (4 bytes)
124///
125/// ```text
126/// Bit 31 (type discriminator):
127///   0: Direct char (bits 0-20 contain Unicode scalar value, max U+10FFFF)
128///   1: GraphemeId reference (bits 0-30 contain slot + width)
129/// ```
130///
131/// This allows:
132/// - 99% of cells (ASCII/BMP) to be stored without heap allocation
133/// - Complex graphemes (emoji, ZWJ sequences) stored in pool
134///
135/// # Special Values
136///
137/// - `EMPTY` (0x0): Empty cell, width 0
138/// - `CONTINUATION` (0x1): Placeholder for wide character continuation
139#[derive(Clone, Copy, PartialEq, Eq, Hash)]
140#[repr(transparent)]
141pub struct CellContent(u32);
142
143impl CellContent {
144    /// Empty cell content (no character).
145    pub const EMPTY: Self = Self(0);
146
147    /// Continuation marker for wide characters.
148    ///
149    /// When a character has display width > 1, subsequent cells are filled
150    /// with this marker to indicate they are part of the previous character.
151    ///
152    /// Value is `0x7FFF_FFFF` (max i32), which is outside valid Unicode scalar
153    /// range (0..0x10FFFF) but fits in 31 bits (Direct Char mode).
154    pub const CONTINUATION: Self = Self(0x7FFF_FFFF);
155
156    /// Create content from a single Unicode character.
157    ///
158    /// For characters with display width > 1, subsequent cells should be
159    /// filled with `CONTINUATION`.
160    ///
161    /// # Tab Handling
162    ///
163    /// The tab character `\t` is normalized to a space. This prevents cursor
164    /// desynchronization, as the `Presenter` cannot predict terminal tab stops.
165    #[inline]
166    pub const fn from_char(c: char) -> Self {
167        if c == '\t' {
168            Self(' ' as u32)
169        } else {
170            Self(c as u32)
171        }
172    }
173
174    /// Create content from a grapheme ID (for multi-codepoint clusters).
175    ///
176    /// The grapheme ID references an entry in the `GraphemePool`.
177    #[inline]
178    pub const fn from_grapheme(id: GraphemeId) -> Self {
179        Self(0x8000_0000 | id.raw())
180    }
181
182    /// Check if this content is a grapheme reference (vs direct char).
183    #[inline]
184    pub const fn is_grapheme(self) -> bool {
185        self.0 & 0x8000_0000 != 0
186    }
187
188    /// Check if this is a continuation cell (part of a wide character).
189    #[inline]
190    pub const fn is_continuation(self) -> bool {
191        self.0 == Self::CONTINUATION.0
192    }
193
194    /// Check if this cell is empty.
195    #[inline]
196    pub const fn is_empty(self) -> bool {
197        self.0 == Self::EMPTY.0
198    }
199
200    /// Check if this content is the default value.
201    ///
202    /// This is equivalent to `is_empty()` and primarily exists for readability in tests.
203    #[inline]
204    pub const fn is_default(self) -> bool {
205        self.0 == Self::EMPTY.0
206    }
207
208    /// Extract the character if this is a direct char (not a grapheme).
209    ///
210    /// Returns `None` if this is empty, continuation, or a grapheme reference.
211    #[inline]
212    pub fn as_char(self) -> Option<char> {
213        if self.is_grapheme() || self.0 == Self::EMPTY.0 || self.0 == Self::CONTINUATION.0 {
214            None
215        } else {
216            char::from_u32(self.0)
217        }
218    }
219
220    /// Extract the grapheme ID if this is a grapheme reference.
221    ///
222    /// Returns `None` if this is a direct char.
223    #[inline]
224    pub const fn grapheme_id(self) -> Option<GraphemeId> {
225        if self.is_grapheme() {
226            Some(GraphemeId::from_raw(self.0 & !0x8000_0000))
227        } else {
228            None
229        }
230    }
231
232    /// Get the display width of this content.
233    ///
234    /// - Empty: 0
235    /// - Continuation: 0
236    /// - Grapheme: width embedded in GraphemeId
237    /// - Char: requires external width lookup (returns 1 as default for ASCII)
238    ///
239    /// Note: For accurate char width, use the unicode-display-width-based
240    /// helpers in this crate. This method provides a fast path for known cases.
241    #[inline]
242    pub const fn width_hint(self) -> usize {
243        if self.is_empty() || self.is_continuation() {
244            0
245        } else if self.is_grapheme() {
246            ((self.0 >> 27) & 0x0F) as usize
247        } else {
248            // For direct chars, assume width 1 (fast path for ASCII)
249            // Callers should use unicode-width for accurate measurement
250            1
251        }
252    }
253
254    /// Get the display width of this content with Unicode width semantics.
255    ///
256    /// This is the accurate (but slower) width computation for direct chars.
257    #[inline]
258    pub fn width(self) -> usize {
259        if self.is_empty() || self.is_continuation() {
260            0
261        } else if self.is_grapheme() {
262            ((self.0 >> 27) & 0x0F) as usize
263        } else {
264            let Some(c) = self.as_char() else {
265                return 1;
266            };
267            char_width(c)
268        }
269    }
270
271    /// Raw u32 value.
272    #[inline]
273    pub const fn raw(self) -> u32 {
274        self.0
275    }
276}
277
278impl Default for CellContent {
279    fn default() -> Self {
280        Self::EMPTY
281    }
282}
283
284impl core::fmt::Debug for CellContent {
285    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
286        if self.is_empty() {
287            write!(f, "CellContent::EMPTY")
288        } else if self.is_continuation() {
289            write!(f, "CellContent::CONTINUATION")
290        } else if let Some(c) = self.as_char() {
291            write!(f, "CellContent::Char({c:?})")
292        } else if let Some(id) = self.grapheme_id() {
293            write!(f, "CellContent::Grapheme({id:?})")
294        } else {
295            write!(f, "CellContent(0x{:08x})", self.0)
296        }
297    }
298}
299
300/// A single terminal cell (16 bytes).
301///
302/// # Layout
303///
304/// ```text
305/// #[repr(C, align(16))]
306/// Cell {
307///     content: CellContent,  // 4 bytes
308///     fg: PackedRgba,        // 4 bytes
309///     bg: PackedRgba,        // 4 bytes
310///     attrs: CellAttrs,      // 4 bytes
311/// }
312/// ```
313///
314/// # Invariants
315///
316/// - Size is exactly 16 bytes (verified by compile-time assert)
317/// - All fields are valid (no uninitialized memory)
318/// - Continuation cells should not have meaningful fg/bg (they inherit from parent)
319///
320/// # Default
321///
322/// The default cell is empty with transparent background, white foreground,
323/// and no style attributes.
324#[derive(Clone, Copy, PartialEq, Eq)]
325#[repr(C, align(16))]
326pub struct Cell {
327    /// Character or grapheme content.
328    pub content: CellContent,
329    /// Foreground color.
330    pub fg: PackedRgba,
331    /// Background color.
332    pub bg: PackedRgba,
333    /// Style flags and hyperlink ID.
334    pub attrs: CellAttrs,
335}
336
337// Compile-time size check
338const _: () = assert!(core::mem::size_of::<Cell>() == 16);
339
340impl Cell {
341    /// A continuation cell (placeholder for wide characters).
342    ///
343    /// When a character has display width > 1, subsequent cells are filled
344    /// with this to indicate they are "owned" by the previous cell.
345    pub const CONTINUATION: Self = Self {
346        content: CellContent::CONTINUATION,
347        fg: PackedRgba::TRANSPARENT,
348        bg: PackedRgba::TRANSPARENT,
349        attrs: CellAttrs::NONE,
350    };
351
352    /// Create a new cell with the given content and default colors.
353    #[inline]
354    pub const fn new(content: CellContent) -> Self {
355        Self {
356            content,
357            fg: PackedRgba::WHITE,
358            bg: PackedRgba::TRANSPARENT,
359            attrs: CellAttrs::NONE,
360        }
361    }
362
363    /// Create a cell from a single character.
364    #[inline]
365    pub const fn from_char(c: char) -> Self {
366        Self::new(CellContent::from_char(c))
367    }
368
369    /// Check if this is a continuation cell.
370    #[inline]
371    pub const fn is_continuation(&self) -> bool {
372        self.content.is_continuation()
373    }
374
375    /// Check if this cell is empty.
376    #[inline]
377    pub const fn is_empty(&self) -> bool {
378        self.content.is_empty()
379    }
380
381    /// Get the display width hint for this cell.
382    ///
383    /// See [`CellContent::width_hint`] for details.
384    #[inline]
385    pub const fn width_hint(&self) -> usize {
386        self.content.width_hint()
387    }
388
389    /// Bitwise equality comparison (fast path for diffing).
390    ///
391    /// Uses bitwise AND (`&`) instead of short-circuit AND (`&&`) so all
392    /// four u32 comparisons are always evaluated. This avoids branch
393    /// mispredictions in tight loops and allows LLVM to lower the check
394    /// to a single 128-bit SIMD compare on supported targets.
395    #[inline(always)]
396    pub fn bits_eq(&self, other: &Self) -> bool {
397        (self.content.raw() == other.content.raw())
398            & (self.fg == other.fg)
399            & (self.bg == other.bg)
400            & (self.attrs == other.attrs)
401    }
402
403    /// Set the cell content to a character, preserving other fields.
404    #[inline]
405    #[must_use]
406    pub const fn with_char(mut self, c: char) -> Self {
407        self.content = CellContent::from_char(c);
408        self
409    }
410
411    /// Set the foreground color.
412    #[inline]
413    #[must_use]
414    pub const fn with_fg(mut self, fg: PackedRgba) -> Self {
415        self.fg = fg;
416        self
417    }
418
419    /// Set the background color.
420    #[inline]
421    #[must_use]
422    pub const fn with_bg(mut self, bg: PackedRgba) -> Self {
423        self.bg = bg;
424        self
425    }
426
427    /// Set the style attributes.
428    #[inline]
429    #[must_use]
430    pub const fn with_attrs(mut self, attrs: CellAttrs) -> Self {
431        self.attrs = attrs;
432        self
433    }
434}
435impl Default for Cell {
436    fn default() -> Self {
437        Self {
438            content: CellContent::EMPTY,
439            fg: PackedRgba::WHITE,
440            bg: PackedRgba::TRANSPARENT,
441            attrs: CellAttrs::NONE,
442        }
443    }
444}
445
446impl core::fmt::Debug for Cell {
447    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
448        f.debug_struct("Cell")
449            .field("content", &self.content)
450            .field("fg", &self.fg)
451            .field("bg", &self.bg)
452            .field("attrs", &self.attrs)
453            .finish()
454    }
455}
456
457/// A compact RGBA color.
458///
459/// - **Size:** 4 bytes (fits within the `Cell` 16-byte budget).
460/// - **Layout:** `0xRRGGBBAA` (R in bits 31..24, A in bits 7..0).
461///
462/// Notes
463/// -----
464/// This is **straight alpha** storage (RGB channels are not pre-multiplied).
465/// Compositing uses Porter-Duff **SourceOver** (`src over dst`).
466#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
467#[repr(transparent)]
468pub struct PackedRgba(pub u32);
469
470impl PackedRgba {
471    /// Fully transparent (alpha = 0).
472    pub const TRANSPARENT: Self = Self(0);
473    /// Opaque black.
474    pub const BLACK: Self = Self::rgb(0, 0, 0);
475    /// Opaque white.
476    pub const WHITE: Self = Self::rgb(255, 255, 255);
477    /// Opaque red.
478    pub const RED: Self = Self::rgb(255, 0, 0);
479    /// Opaque green.
480    pub const GREEN: Self = Self::rgb(0, 255, 0);
481    /// Opaque blue.
482    pub const BLUE: Self = Self::rgb(0, 0, 255);
483
484    /// Create an opaque RGB color (alpha = 255).
485    #[inline]
486    pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
487        Self::rgba(r, g, b, 255)
488    }
489
490    /// Create an RGBA color with explicit alpha.
491    #[inline]
492    pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
493        Self(((r as u32) << 24) | ((g as u32) << 16) | ((b as u32) << 8) | (a as u32))
494    }
495
496    /// Red channel.
497    #[inline]
498    pub const fn r(self) -> u8 {
499        (self.0 >> 24) as u8
500    }
501
502    /// Green channel.
503    #[inline]
504    pub const fn g(self) -> u8 {
505        (self.0 >> 16) as u8
506    }
507
508    /// Blue channel.
509    #[inline]
510    pub const fn b(self) -> u8 {
511        (self.0 >> 8) as u8
512    }
513
514    /// Alpha channel.
515    #[inline]
516    pub const fn a(self) -> u8 {
517        self.0 as u8
518    }
519
520    #[inline]
521    const fn div_round_u8(numer: u64, denom: u64) -> u8 {
522        debug_assert!(denom != 0);
523        let v = (numer + (denom / 2)) / denom;
524        if v > 255 { 255 } else { v as u8 }
525    }
526
527    /// Porter-Duff SourceOver: `src over dst`.
528    ///
529    /// Stored as straight alpha, so we compute the exact rational form and round at the end
530    /// (avoids accumulating rounding error across intermediate steps).
531    #[inline]
532    #[must_use]
533    pub fn over(self, dst: Self) -> Self {
534        let s_a = self.a() as u64;
535        if s_a == 255 {
536            return self;
537        }
538        if s_a == 0 {
539            return dst;
540        }
541
542        let d_a = dst.a() as u64;
543        let inv_s_a = 255 - s_a;
544
545        // out_a = s_a + d_a*(1 - s_a)  (all in [0,1], scaled by 255)
546        // We compute numer_a in the "255^2 domain" to keep channels exact:
547        // numer_a = 255*s_a + d_a*(255 - s_a)
548        // out_a_u8 = round(numer_a / 255)
549        let numer_a = 255 * s_a + d_a * inv_s_a;
550        if numer_a == 0 {
551            return Self::TRANSPARENT;
552        }
553
554        let out_a = Self::div_round_u8(numer_a, 255);
555
556        // For straight alpha, the exact rational (scaled to [0,255]) is:
557        // out_c_u8 = round( (src_c*s_a*255 + dst_c*d_a*(255 - s_a)) / numer_a )
558        let r = Self::div_round_u8(
559            (self.r() as u64) * s_a * 255 + (dst.r() as u64) * d_a * inv_s_a,
560            numer_a,
561        );
562        let g = Self::div_round_u8(
563            (self.g() as u64) * s_a * 255 + (dst.g() as u64) * d_a * inv_s_a,
564            numer_a,
565        );
566        let b = Self::div_round_u8(
567            (self.b() as u64) * s_a * 255 + (dst.b() as u64) * d_a * inv_s_a,
568            numer_a,
569        );
570
571        Self::rgba(r, g, b, out_a)
572    }
573
574    /// Apply uniform opacity in `[0.0, 1.0]` by scaling alpha.
575    #[inline]
576    #[must_use]
577    pub fn with_opacity(self, opacity: f32) -> Self {
578        let opacity = opacity.clamp(0.0, 1.0);
579        let a = ((self.a() as f32) * opacity).round().clamp(0.0, 255.0) as u8;
580        Self::rgba(self.r(), self.g(), self.b(), a)
581    }
582}
583
584bitflags::bitflags! {
585    /// 8-bit cell style flags.
586    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
587    pub struct StyleFlags: u8 {
588        /// Bold / increased intensity.
589        const BOLD          = 0b0000_0001;
590        /// Dim / decreased intensity.
591        const DIM           = 0b0000_0010;
592        /// Italic text.
593        const ITALIC        = 0b0000_0100;
594        /// Underlined text.
595        const UNDERLINE     = 0b0000_1000;
596        /// Blinking text.
597        const BLINK         = 0b0001_0000;
598        /// Reverse video (swap fg/bg).
599        const REVERSE       = 0b0010_0000;
600        /// Strikethrough text.
601        const STRIKETHROUGH = 0b0100_0000;
602        /// Hidden / invisible text.
603        const HIDDEN        = 0b1000_0000;
604    }
605}
606
607/// Packed cell attributes:
608/// - bits 31..24: `StyleFlags` (8 bits)
609/// - bits 23..0: `link_id` (24 bits)
610#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
611#[repr(transparent)]
612pub struct CellAttrs(u32);
613
614impl CellAttrs {
615    /// No attributes or link.
616    pub const NONE: Self = Self(0);
617
618    /// Sentinel value for "no hyperlink".
619    pub const LINK_ID_NONE: u32 = 0;
620    /// Maximum link ID (24-bit range).
621    pub const LINK_ID_MAX: u32 = 0x00FF_FFFE;
622
623    /// Create attributes from flags and a hyperlink ID.
624    #[inline]
625    pub fn new(flags: StyleFlags, link_id: u32) -> Self {
626        debug_assert!(
627            link_id <= Self::LINK_ID_MAX,
628            "link_id overflow: {link_id} (max={})",
629            Self::LINK_ID_MAX
630        );
631        Self(((flags.bits() as u32) << 24) | (link_id & 0x00FF_FFFF))
632    }
633
634    /// Extract the style flags.
635    #[inline]
636    pub fn flags(self) -> StyleFlags {
637        StyleFlags::from_bits_truncate((self.0 >> 24) as u8)
638    }
639
640    /// Extract the hyperlink ID.
641    #[inline]
642    pub fn link_id(self) -> u32 {
643        self.0 & 0x00FF_FFFF
644    }
645
646    /// Return a copy with different style flags.
647    #[inline]
648    #[must_use]
649    pub fn with_flags(self, flags: StyleFlags) -> Self {
650        Self((self.0 & 0x00FF_FFFF) | ((flags.bits() as u32) << 24))
651    }
652
653    /// Return a copy with a different hyperlink ID.
654    #[inline]
655    #[must_use]
656    pub fn with_link(self, link_id: u32) -> Self {
657        debug_assert!(
658            link_id <= Self::LINK_ID_MAX,
659            "link_id overflow: {link_id} (max={})",
660            Self::LINK_ID_MAX
661        );
662        Self((self.0 & 0xFF00_0000) | (link_id & 0x00FF_FFFF))
663    }
664
665    /// Return a copy with additional style flags OR-ed in (preserving existing flags and link ID).
666    ///
667    /// Unlike [`with_flags`](Self::with_flags) which *replaces* all flags, this method
668    /// *merges* the new flags on top so that existing attributes are preserved.
669    #[inline]
670    #[must_use]
671    pub fn merged_flags(self, extra: StyleFlags) -> Self {
672        let combined = self.flags().union(extra);
673        Self((self.0 & 0x00FF_FFFF) | ((combined.bits() as u32) << 24))
674    }
675
676    /// Check whether a specific flag is set.
677    #[inline]
678    pub fn has_flag(self, flag: StyleFlags) -> bool {
679        self.flags().contains(flag)
680    }
681}
682
683#[cfg(test)]
684mod tests {
685    use super::{Cell, CellAttrs, CellContent, GraphemeId, PackedRgba, StyleFlags};
686
687    fn reference_over(src: PackedRgba, dst: PackedRgba) -> PackedRgba {
688        let sr = src.r() as f64 / 255.0;
689        let sg = src.g() as f64 / 255.0;
690        let sb = src.b() as f64 / 255.0;
691        let sa = src.a() as f64 / 255.0;
692
693        let dr = dst.r() as f64 / 255.0;
694        let dg = dst.g() as f64 / 255.0;
695        let db = dst.b() as f64 / 255.0;
696        let da = dst.a() as f64 / 255.0;
697
698        let out_a = sa + da * (1.0 - sa);
699        if out_a <= 0.0 {
700            return PackedRgba::TRANSPARENT;
701        }
702
703        let out_r = (sr * sa + dr * da * (1.0 - sa)) / out_a;
704        let out_g = (sg * sa + dg * da * (1.0 - sa)) / out_a;
705        let out_b = (sb * sa + db * da * (1.0 - sa)) / out_a;
706
707        let to_u8 = |x: f64| -> u8 { (x * 255.0).round().clamp(0.0, 255.0) as u8 };
708        PackedRgba::rgba(to_u8(out_r), to_u8(out_g), to_u8(out_b), to_u8(out_a))
709    }
710
711    #[test]
712    fn packed_rgba_is_4_bytes() {
713        assert_eq!(core::mem::size_of::<PackedRgba>(), 4);
714    }
715
716    #[test]
717    fn rgb_sets_alpha_to_255() {
718        let c = PackedRgba::rgb(1, 2, 3);
719        assert_eq!(c.r(), 1);
720        assert_eq!(c.g(), 2);
721        assert_eq!(c.b(), 3);
722        assert_eq!(c.a(), 255);
723    }
724
725    #[test]
726    fn rgba_round_trips_components() {
727        let c = PackedRgba::rgba(10, 20, 30, 40);
728        assert_eq!(c.r(), 10);
729        assert_eq!(c.g(), 20);
730        assert_eq!(c.b(), 30);
731        assert_eq!(c.a(), 40);
732    }
733
734    #[test]
735    fn over_with_opaque_src_returns_src() {
736        let src = PackedRgba::rgba(1, 2, 3, 255);
737        let dst = PackedRgba::rgba(9, 8, 7, 200);
738        assert_eq!(src.over(dst), src);
739    }
740
741    #[test]
742    fn over_with_transparent_src_returns_dst() {
743        let src = PackedRgba::TRANSPARENT;
744        let dst = PackedRgba::rgba(9, 8, 7, 200);
745        assert_eq!(src.over(dst), dst);
746    }
747
748    #[test]
749    fn over_blends_correctly_for_half_alpha_over_opaque() {
750        // 50% red over opaque blue -> purple-ish, and resulting alpha stays opaque.
751        let src = PackedRgba::rgba(255, 0, 0, 128);
752        let dst = PackedRgba::rgba(0, 0, 255, 255);
753        assert_eq!(src.over(dst), PackedRgba::rgba(128, 0, 127, 255));
754    }
755
756    #[test]
757    fn over_matches_reference_for_partial_alpha_cases() {
758        let cases = [
759            (
760                PackedRgba::rgba(200, 10, 10, 64),
761                PackedRgba::rgba(10, 200, 10, 128),
762            ),
763            (
764                PackedRgba::rgba(1, 2, 3, 1),
765                PackedRgba::rgba(250, 251, 252, 254),
766            ),
767            (
768                PackedRgba::rgba(100, 0, 200, 200),
769                PackedRgba::rgba(0, 120, 30, 50),
770            ),
771        ];
772
773        for (src, dst) in cases {
774            assert_eq!(src.over(dst), reference_over(src, dst));
775        }
776    }
777
778    #[test]
779    fn with_opacity_scales_alpha() {
780        let c = PackedRgba::rgba(10, 20, 30, 255);
781        assert_eq!(c.with_opacity(0.5).a(), 128);
782        assert_eq!(c.with_opacity(-1.0).a(), 0);
783        assert_eq!(c.with_opacity(2.0).a(), 255);
784    }
785
786    #[test]
787    fn cell_attrs_is_4_bytes() {
788        assert_eq!(core::mem::size_of::<CellAttrs>(), 4);
789    }
790
791    #[test]
792    fn cell_attrs_none_has_no_flags_and_no_link() {
793        assert!(CellAttrs::NONE.flags().is_empty());
794        assert_eq!(CellAttrs::NONE.link_id(), 0);
795    }
796
797    #[test]
798    fn cell_attrs_new_stores_flags_and_link() {
799        let flags = StyleFlags::BOLD | StyleFlags::ITALIC;
800        let a = CellAttrs::new(flags, 42);
801        assert_eq!(a.flags(), flags);
802        assert_eq!(a.link_id(), 42);
803    }
804
805    #[test]
806    fn cell_attrs_with_flags_preserves_link_id() {
807        let a = CellAttrs::new(StyleFlags::BOLD, 123);
808        let b = a.with_flags(StyleFlags::UNDERLINE);
809        assert_eq!(b.flags(), StyleFlags::UNDERLINE);
810        assert_eq!(b.link_id(), 123);
811    }
812
813    #[test]
814    fn cell_attrs_merged_flags_ors_without_clearing() {
815        let a = CellAttrs::new(StyleFlags::BOLD, 42);
816        let b = a.merged_flags(StyleFlags::ITALIC);
817        assert_eq!(b.flags(), StyleFlags::BOLD | StyleFlags::ITALIC);
818        assert_eq!(b.link_id(), 42, "link_id must be preserved");
819    }
820
821    #[test]
822    fn cell_attrs_merged_flags_noop_for_empty() {
823        let a = CellAttrs::new(StyleFlags::BOLD, 7);
824        let b = a.merged_flags(StyleFlags::empty());
825        assert_eq!(b.flags(), StyleFlags::BOLD);
826        assert_eq!(b.link_id(), 7);
827    }
828
829    #[test]
830    fn cell_attrs_with_link_preserves_flags() {
831        let a = CellAttrs::new(StyleFlags::BOLD | StyleFlags::ITALIC, 1);
832        let b = a.with_link(999);
833        assert_eq!(b.flags(), StyleFlags::BOLD | StyleFlags::ITALIC);
834        assert_eq!(b.link_id(), 999);
835    }
836
837    #[test]
838    fn cell_attrs_flag_combinations_work() {
839        let flags = StyleFlags::BOLD | StyleFlags::ITALIC;
840        let a = CellAttrs::new(flags, 0);
841        assert!(a.has_flag(StyleFlags::BOLD));
842        assert!(a.has_flag(StyleFlags::ITALIC));
843        assert!(!a.has_flag(StyleFlags::UNDERLINE));
844    }
845
846    #[test]
847    fn cell_attrs_link_id_max_boundary() {
848        let a = CellAttrs::new(StyleFlags::empty(), CellAttrs::LINK_ID_MAX);
849        assert_eq!(a.link_id(), CellAttrs::LINK_ID_MAX);
850    }
851
852    // ====== GraphemeId tests ======
853
854    #[test]
855    fn grapheme_id_is_4_bytes() {
856        assert_eq!(core::mem::size_of::<GraphemeId>(), 4);
857    }
858
859    #[test]
860    fn grapheme_id_encoding_roundtrip() {
861        let id = GraphemeId::new(12345, 42, 2);
862        assert_eq!(id.slot(), 12345);
863        assert_eq!(id.generation(), 42);
864        assert_eq!(id.width(), 2);
865    }
866
867    #[test]
868    fn grapheme_id_max_values() {
869        let id = GraphemeId::new(
870            GraphemeId::MAX_SLOT,
871            GraphemeId::MAX_GENERATION,
872            GraphemeId::MAX_WIDTH,
873        );
874        assert_eq!(id.slot(), 0xFFFF);
875        assert_eq!(id.generation(), GraphemeId::MAX_GENERATION);
876        assert_eq!(id.width(), GraphemeId::MAX_WIDTH as usize);
877    }
878
879    #[test]
880    fn grapheme_id_zero_values() {
881        let id = GraphemeId::new(0, 0, 0);
882        assert_eq!(id.slot(), 0);
883        assert_eq!(id.generation(), 0);
884        assert_eq!(id.width(), 0);
885    }
886
887    #[test]
888    fn grapheme_id_raw_roundtrip() {
889        let id = GraphemeId::new(999, 128, 5);
890        let raw = id.raw();
891        let restored = GraphemeId::from_raw(raw);
892        assert_eq!(restored.slot(), 999);
893        assert_eq!(restored.generation(), 128);
894        assert_eq!(restored.width(), 5);
895    }
896
897    // ====== CellContent tests ======
898
899    #[test]
900    fn cell_content_is_4_bytes() {
901        assert_eq!(core::mem::size_of::<CellContent>(), 4);
902    }
903
904    #[test]
905    fn cell_content_empty_properties() {
906        assert!(CellContent::EMPTY.is_empty());
907        assert!(!CellContent::EMPTY.is_continuation());
908        assert!(!CellContent::EMPTY.is_grapheme());
909        assert_eq!(CellContent::EMPTY.width_hint(), 0);
910    }
911
912    #[test]
913    fn cell_content_continuation_properties() {
914        assert!(CellContent::CONTINUATION.is_continuation());
915        assert!(!CellContent::CONTINUATION.is_empty());
916        assert!(!CellContent::CONTINUATION.is_grapheme());
917        assert_eq!(CellContent::CONTINUATION.width_hint(), 0);
918    }
919
920    #[test]
921    fn cell_content_from_char_ascii() {
922        let c = CellContent::from_char('A');
923        assert!(!c.is_grapheme());
924        assert!(!c.is_empty());
925        assert!(!c.is_continuation());
926        assert_eq!(c.as_char(), Some('A'));
927        assert_eq!(c.width_hint(), 1);
928    }
929
930    #[test]
931    fn cell_content_from_char_unicode() {
932        // BMP character
933        let c = CellContent::from_char('日');
934        assert_eq!(c.as_char(), Some('日'));
935        assert!(!c.is_grapheme());
936
937        // Supplementary plane character (emoji)
938        let c2 = CellContent::from_char('🎉');
939        assert_eq!(c2.as_char(), Some('🎉'));
940        assert!(!c2.is_grapheme());
941    }
942
943    #[test]
944    fn cell_content_from_grapheme() {
945        let id = GraphemeId::new(42, 0, 2);
946        let c = CellContent::from_grapheme(id);
947
948        assert!(c.is_grapheme());
949        assert!(!c.is_empty());
950        assert!(!c.is_continuation());
951        assert_eq!(c.grapheme_id(), Some(id));
952        assert_eq!(c.as_char(), None);
953        assert_eq!(c.width_hint(), 2);
954    }
955
956    #[test]
957    fn cell_content_width_for_chars() {
958        let ascii = CellContent::from_char('A');
959        assert_eq!(ascii.width(), 1);
960
961        let wide = CellContent::from_char('日');
962        assert_eq!(wide.width(), 2);
963
964        let emoji = CellContent::from_char('🎉');
965        assert_eq!(emoji.width(), 2);
966
967        // Unicode East Asian Width properties:
968        // - '⚡' (U+26A1) is Wide → always width 2
969        // - '⚙' (U+2699) is Neutral → 1 (non-CJK) or 2 (CJK)
970        // - '❤' (U+2764) is Neutral → 1 (non-CJK) or 2 (CJK)
971        let bolt = CellContent::from_char('⚡');
972        assert_eq!(bolt.width(), 2, "bolt is Wide, always width 2");
973
974        // Neutral-width characters: width depends on CJK mode
975        let gear = CellContent::from_char('⚙');
976        let heart = CellContent::from_char('❤');
977        assert!(
978            [1, 2].contains(&gear.width()),
979            "gear should be 1 (non-CJK) or 2 (CJK), got {}",
980            gear.width()
981        );
982        assert_eq!(
983            gear.width(),
984            heart.width(),
985            "gear and heart should have same width (both Neutral)"
986        );
987    }
988
989    #[test]
990    fn cell_content_width_for_grapheme() {
991        let id = GraphemeId::new(7, 0, 3);
992        let c = CellContent::from_grapheme(id);
993        assert_eq!(c.width(), 3);
994    }
995
996    #[test]
997    fn cell_content_width_empty_is_zero() {
998        assert_eq!(CellContent::EMPTY.width(), 0);
999        assert_eq!(CellContent::CONTINUATION.width(), 0);
1000    }
1001
1002    #[test]
1003    fn cell_content_grapheme_discriminator_bit() {
1004        // Chars should have bit 31 = 0
1005        let char_content = CellContent::from_char('X');
1006        assert_eq!(char_content.raw() & 0x8000_0000, 0);
1007
1008        // Graphemes should have bit 31 = 1
1009        let grapheme_content = CellContent::from_grapheme(GraphemeId::new(1, 0, 1));
1010        assert_ne!(grapheme_content.raw() & 0x8000_0000, 0);
1011    }
1012
1013    // ====== Cell tests ======
1014
1015    #[test]
1016    fn cell_is_16_bytes() {
1017        assert_eq!(core::mem::size_of::<Cell>(), 16);
1018    }
1019
1020    #[test]
1021    fn cell_alignment_is_16() {
1022        assert_eq!(core::mem::align_of::<Cell>(), 16);
1023    }
1024
1025    #[test]
1026    fn cell_default_properties() {
1027        let cell = Cell::default();
1028        assert!(cell.is_empty());
1029        assert!(!cell.is_continuation());
1030        assert_eq!(cell.fg, PackedRgba::WHITE);
1031        assert_eq!(cell.bg, PackedRgba::TRANSPARENT);
1032        assert_eq!(cell.attrs, CellAttrs::NONE);
1033    }
1034
1035    #[test]
1036    fn cell_continuation_constant() {
1037        assert!(Cell::CONTINUATION.is_continuation());
1038        assert!(!Cell::CONTINUATION.is_empty());
1039    }
1040
1041    #[test]
1042    fn cell_from_char() {
1043        let cell = Cell::from_char('X');
1044        assert_eq!(cell.content.as_char(), Some('X'));
1045        assert_eq!(cell.fg, PackedRgba::WHITE);
1046        assert_eq!(cell.bg, PackedRgba::TRANSPARENT);
1047    }
1048
1049    #[test]
1050    fn cell_builder_methods() {
1051        let cell = Cell::from_char('A')
1052            .with_fg(PackedRgba::rgb(255, 0, 0))
1053            .with_bg(PackedRgba::rgb(0, 0, 255))
1054            .with_attrs(CellAttrs::new(StyleFlags::BOLD, 0));
1055
1056        assert_eq!(cell.content.as_char(), Some('A'));
1057        assert_eq!(cell.fg, PackedRgba::rgb(255, 0, 0));
1058        assert_eq!(cell.bg, PackedRgba::rgb(0, 0, 255));
1059        assert!(cell.attrs.has_flag(StyleFlags::BOLD));
1060    }
1061
1062    #[test]
1063    fn cell_bits_eq_same_cells() {
1064        let cell1 = Cell::from_char('X').with_fg(PackedRgba::rgb(1, 2, 3));
1065        let cell2 = Cell::from_char('X').with_fg(PackedRgba::rgb(1, 2, 3));
1066        assert!(cell1.bits_eq(&cell2));
1067    }
1068
1069    #[test]
1070    fn cell_bits_eq_different_cells() {
1071        let cell1 = Cell::from_char('X');
1072        let cell2 = Cell::from_char('Y');
1073        assert!(!cell1.bits_eq(&cell2));
1074
1075        let cell3 = Cell::from_char('X').with_fg(PackedRgba::rgb(1, 2, 3));
1076        assert!(!cell1.bits_eq(&cell3));
1077    }
1078
1079    #[test]
1080    fn cell_width_hint() {
1081        let empty = Cell::default();
1082        assert_eq!(empty.width_hint(), 0);
1083
1084        let cont = Cell::CONTINUATION;
1085        assert_eq!(cont.width_hint(), 0);
1086
1087        let ascii = Cell::from_char('A');
1088        assert_eq!(ascii.width_hint(), 1);
1089    }
1090
1091    // Property tests moved to top-level `cell_proptests` module for edition 2024 compat.
1092
1093    // ====== PackedRgba extended coverage ======
1094
1095    #[test]
1096    fn packed_rgba_named_constants() {
1097        assert_eq!(PackedRgba::TRANSPARENT, PackedRgba(0));
1098        assert_eq!(PackedRgba::TRANSPARENT.a(), 0);
1099
1100        assert_eq!(PackedRgba::BLACK.r(), 0);
1101        assert_eq!(PackedRgba::BLACK.g(), 0);
1102        assert_eq!(PackedRgba::BLACK.b(), 0);
1103        assert_eq!(PackedRgba::BLACK.a(), 255);
1104
1105        assert_eq!(PackedRgba::WHITE.r(), 255);
1106        assert_eq!(PackedRgba::WHITE.g(), 255);
1107        assert_eq!(PackedRgba::WHITE.b(), 255);
1108        assert_eq!(PackedRgba::WHITE.a(), 255);
1109
1110        assert_eq!(PackedRgba::RED, PackedRgba::rgb(255, 0, 0));
1111        assert_eq!(PackedRgba::GREEN, PackedRgba::rgb(0, 255, 0));
1112        assert_eq!(PackedRgba::BLUE, PackedRgba::rgb(0, 0, 255));
1113    }
1114
1115    #[test]
1116    fn packed_rgba_default_is_transparent() {
1117        assert_eq!(PackedRgba::default(), PackedRgba::TRANSPARENT);
1118    }
1119
1120    #[test]
1121    fn over_both_transparent_returns_transparent() {
1122        // Exercises numer_a == 0 branch (line 508)
1123        let result = PackedRgba::TRANSPARENT.over(PackedRgba::TRANSPARENT);
1124        assert_eq!(result, PackedRgba::TRANSPARENT);
1125    }
1126
1127    #[test]
1128    fn over_partial_alpha_over_transparent_dst() {
1129        // d_a == 0 path: src partial alpha over fully transparent
1130        let src = PackedRgba::rgba(200, 100, 50, 128);
1131        let result = src.over(PackedRgba::TRANSPARENT);
1132        // Output alpha = src_alpha (dst contributes nothing)
1133        assert_eq!(result.a(), 128);
1134        // Colors should be src colors since dst has no contribution
1135        assert_eq!(result.r(), 200);
1136        assert_eq!(result.g(), 100);
1137        assert_eq!(result.b(), 50);
1138    }
1139
1140    #[test]
1141    fn over_very_low_alpha() {
1142        // Near-transparent source (alpha=1) over opaque destination
1143        let src = PackedRgba::rgba(255, 0, 0, 1);
1144        let dst = PackedRgba::rgba(0, 0, 255, 255);
1145        let result = src.over(dst);
1146        // Result should be very close to dst
1147        assert_eq!(result.a(), 255);
1148        assert!(result.b() > 250, "b={} should be near 255", result.b());
1149        assert!(result.r() < 5, "r={} should be near 0", result.r());
1150    }
1151
1152    #[test]
1153    fn with_opacity_exact_zero() {
1154        let c = PackedRgba::rgba(10, 20, 30, 200);
1155        let result = c.with_opacity(0.0);
1156        assert_eq!(result.a(), 0);
1157        assert_eq!(result.r(), 10); // RGB preserved
1158        assert_eq!(result.g(), 20);
1159        assert_eq!(result.b(), 30);
1160    }
1161
1162    #[test]
1163    fn with_opacity_exact_one() {
1164        let c = PackedRgba::rgba(10, 20, 30, 200);
1165        let result = c.with_opacity(1.0);
1166        assert_eq!(result.a(), 200); // Alpha unchanged
1167        assert_eq!(result.r(), 10);
1168    }
1169
1170    #[test]
1171    fn with_opacity_preserves_rgb() {
1172        let c = PackedRgba::rgba(42, 84, 168, 255);
1173        let result = c.with_opacity(0.25);
1174        assert_eq!(result.r(), 42);
1175        assert_eq!(result.g(), 84);
1176        assert_eq!(result.b(), 168);
1177        assert_eq!(result.a(), 64); // 255 * 0.25 = 63.75 → 64
1178    }
1179
1180    // ====== CellContent extended coverage ======
1181
1182    #[test]
1183    fn cell_content_as_char_none_for_empty() {
1184        assert_eq!(CellContent::EMPTY.as_char(), None);
1185    }
1186
1187    #[test]
1188    fn cell_content_as_char_none_for_continuation() {
1189        assert_eq!(CellContent::CONTINUATION.as_char(), None);
1190    }
1191
1192    #[test]
1193    fn cell_content_as_char_none_for_grapheme() {
1194        let id = GraphemeId::new(1, 2, 1);
1195        let c = CellContent::from_grapheme(id);
1196        assert_eq!(c.as_char(), None);
1197    }
1198
1199    #[test]
1200    fn cell_content_grapheme_id_none_for_char() {
1201        let c = CellContent::from_char('A');
1202        assert_eq!(c.grapheme_id(), None);
1203    }
1204
1205    #[test]
1206    fn cell_content_grapheme_id_none_for_empty() {
1207        assert_eq!(CellContent::EMPTY.grapheme_id(), None);
1208    }
1209
1210    #[test]
1211    fn cell_content_width_control_chars() {
1212        // Control characters have width 0, except tab/newline/CR which are 1 cell
1213        // Note: NUL (0x00) is CellContent::EMPTY, so test with other controls
1214        let tab = CellContent::from_char('\t');
1215        assert_eq!(tab.width(), 1);
1216
1217        let bel = CellContent::from_char('\x07');
1218        assert_eq!(bel.width(), 0);
1219    }
1220
1221    #[test]
1222    fn cell_content_width_hint_always_1_for_chars() {
1223        // width_hint is the fast path that always returns 1 for non-special chars
1224        let wide = CellContent::from_char('日');
1225        assert_eq!(wide.width_hint(), 1); // fast path says 1
1226        assert_eq!(wide.width(), 2); // accurate path says 2
1227    }
1228
1229    #[test]
1230    fn cell_content_default_is_empty() {
1231        assert_eq!(CellContent::default(), CellContent::EMPTY);
1232    }
1233
1234    #[test]
1235    fn cell_content_debug_empty() {
1236        let s = format!("{:?}", CellContent::EMPTY);
1237        assert_eq!(s, "CellContent::EMPTY");
1238    }
1239
1240    #[test]
1241    fn cell_content_debug_continuation() {
1242        let s = format!("{:?}", CellContent::CONTINUATION);
1243        assert_eq!(s, "CellContent::CONTINUATION");
1244    }
1245
1246    #[test]
1247    fn cell_content_debug_char() {
1248        let s = format!("{:?}", CellContent::from_char('X'));
1249        assert!(s.starts_with("CellContent::Char("), "got: {s}");
1250    }
1251
1252    #[test]
1253    fn cell_content_debug_grapheme() {
1254        let id = GraphemeId::new(1, 2, 1);
1255        let s = format!("{:?}", CellContent::from_grapheme(id));
1256        assert!(s.starts_with("CellContent::Grapheme("), "got: {s}");
1257    }
1258
1259    #[test]
1260    fn cell_content_raw_value() {
1261        let c = CellContent::from_char('A');
1262        assert_eq!(c.raw(), 'A' as u32);
1263
1264        let g = CellContent::from_grapheme(GraphemeId::new(5, 2, 1));
1265        assert_ne!(g.raw() & 0x8000_0000, 0);
1266    }
1267
1268    // ====== CellAttrs extended coverage ======
1269
1270    #[test]
1271    fn cell_attrs_default_is_none() {
1272        assert_eq!(CellAttrs::default(), CellAttrs::NONE);
1273    }
1274
1275    #[test]
1276    fn cell_attrs_each_flag_isolated() {
1277        let all_flags = [
1278            StyleFlags::BOLD,
1279            StyleFlags::DIM,
1280            StyleFlags::ITALIC,
1281            StyleFlags::UNDERLINE,
1282            StyleFlags::BLINK,
1283            StyleFlags::REVERSE,
1284            StyleFlags::STRIKETHROUGH,
1285            StyleFlags::HIDDEN,
1286        ];
1287
1288        for &flag in &all_flags {
1289            let a = CellAttrs::new(flag, 0);
1290            assert!(a.has_flag(flag), "flag {:?} should be set", flag);
1291
1292            // Verify no other flags are set
1293            for &other in &all_flags {
1294                if other != flag {
1295                    assert!(
1296                        !a.has_flag(other),
1297                        "flag {:?} should NOT be set when only {:?} is",
1298                        other,
1299                        flag
1300                    );
1301                }
1302            }
1303        }
1304    }
1305
1306    #[test]
1307    fn cell_attrs_all_flags_combined() {
1308        let all = StyleFlags::BOLD
1309            | StyleFlags::DIM
1310            | StyleFlags::ITALIC
1311            | StyleFlags::UNDERLINE
1312            | StyleFlags::BLINK
1313            | StyleFlags::REVERSE
1314            | StyleFlags::STRIKETHROUGH
1315            | StyleFlags::HIDDEN;
1316        let a = CellAttrs::new(all, 42);
1317        assert_eq!(a.flags(), all);
1318        assert!(a.has_flag(StyleFlags::BOLD));
1319        assert!(a.has_flag(StyleFlags::HIDDEN));
1320        assert_eq!(a.link_id(), 42);
1321    }
1322
1323    #[test]
1324    fn cell_attrs_link_id_zero() {
1325        let a = CellAttrs::new(StyleFlags::BOLD, CellAttrs::LINK_ID_NONE);
1326        assert_eq!(a.link_id(), 0);
1327        assert!(a.has_flag(StyleFlags::BOLD));
1328    }
1329
1330    #[test]
1331    fn cell_attrs_with_link_to_none() {
1332        let a = CellAttrs::new(StyleFlags::ITALIC, 500);
1333        let b = a.with_link(CellAttrs::LINK_ID_NONE);
1334        assert_eq!(b.link_id(), 0);
1335        assert!(b.has_flag(StyleFlags::ITALIC));
1336    }
1337
1338    #[test]
1339    fn cell_attrs_with_flags_to_empty() {
1340        let a = CellAttrs::new(StyleFlags::BOLD | StyleFlags::ITALIC, 123);
1341        let b = a.with_flags(StyleFlags::empty());
1342        assert!(b.flags().is_empty());
1343        assert_eq!(b.link_id(), 123);
1344    }
1345
1346    // ====== Cell extended coverage ======
1347
1348    #[test]
1349    fn cell_bits_eq_detects_bg_difference() {
1350        let cell1 = Cell::from_char('X');
1351        let cell2 = Cell::from_char('X').with_bg(PackedRgba::RED);
1352        assert!(!cell1.bits_eq(&cell2));
1353    }
1354
1355    #[test]
1356    fn cell_bits_eq_detects_attrs_difference() {
1357        let cell1 = Cell::from_char('X');
1358        let cell2 = Cell::from_char('X').with_attrs(CellAttrs::new(StyleFlags::BOLD, 0));
1359        assert!(!cell1.bits_eq(&cell2));
1360    }
1361
1362    #[test]
1363    fn cell_with_char_preserves_colors_and_attrs() {
1364        let cell = Cell::from_char('A')
1365            .with_fg(PackedRgba::RED)
1366            .with_bg(PackedRgba::BLUE)
1367            .with_attrs(CellAttrs::new(StyleFlags::BOLD, 42));
1368
1369        let updated = cell.with_char('Z');
1370        assert_eq!(updated.content.as_char(), Some('Z'));
1371        assert_eq!(updated.fg, PackedRgba::RED);
1372        assert_eq!(updated.bg, PackedRgba::BLUE);
1373        assert!(updated.attrs.has_flag(StyleFlags::BOLD));
1374        assert_eq!(updated.attrs.link_id(), 42);
1375    }
1376
1377    #[test]
1378    fn cell_new_vs_from_char() {
1379        let a = Cell::new(CellContent::from_char('A'));
1380        let b = Cell::from_char('A');
1381        assert!(a.bits_eq(&b));
1382    }
1383
1384    #[test]
1385    fn cell_continuation_has_transparent_colors() {
1386        assert_eq!(Cell::CONTINUATION.fg, PackedRgba::TRANSPARENT);
1387        assert_eq!(Cell::CONTINUATION.bg, PackedRgba::TRANSPARENT);
1388        assert_eq!(Cell::CONTINUATION.attrs, CellAttrs::NONE);
1389    }
1390
1391    #[test]
1392    fn cell_debug_format() {
1393        let cell = Cell::from_char('A');
1394        let s = format!("{:?}", cell);
1395        assert!(s.contains("Cell"), "got: {s}");
1396        assert!(s.contains("content"), "got: {s}");
1397        assert!(s.contains("fg"), "got: {s}");
1398        assert!(s.contains("bg"), "got: {s}");
1399        assert!(s.contains("attrs"), "got: {s}");
1400    }
1401
1402    #[test]
1403    fn cell_is_empty_for_various() {
1404        assert!(Cell::default().is_empty());
1405        assert!(!Cell::from_char('A').is_empty());
1406        assert!(!Cell::CONTINUATION.is_empty());
1407    }
1408
1409    #[test]
1410    fn cell_is_continuation_for_various() {
1411        assert!(!Cell::default().is_continuation());
1412        assert!(!Cell::from_char('A').is_continuation());
1413        assert!(Cell::CONTINUATION.is_continuation());
1414    }
1415
1416    #[test]
1417    fn cell_width_hint_for_grapheme() {
1418        let id = GraphemeId::new(100, 0, 3);
1419        let cell = Cell::new(CellContent::from_grapheme(id));
1420        assert_eq!(cell.width_hint(), 3);
1421    }
1422
1423    // ====== GraphemeId extended coverage ======
1424
1425    #[test]
1426    fn grapheme_id_default() {
1427        let id = GraphemeId::default();
1428        assert_eq!(id.slot(), 0);
1429        assert_eq!(id.generation(), 0);
1430        assert_eq!(id.width(), 0);
1431    }
1432
1433    #[test]
1434    fn grapheme_id_debug_format() {
1435        let id = GraphemeId::new(42, 5, 2);
1436        let s = format!("{:?}", id);
1437        assert!(s.contains("GraphemeId"), "got: {s}");
1438        assert!(s.contains("42"), "got: {s}");
1439        assert!(s.contains("5"), "got: {s}");
1440        assert!(s.contains("2"), "got: {s}");
1441    }
1442
1443    #[test]
1444    fn grapheme_id_width_isolated_from_slot() {
1445        // Verify slot bits don't leak into width field
1446        let id = GraphemeId::new(GraphemeId::MAX_SLOT, 0, 0);
1447        assert_eq!(id.width(), 0);
1448        assert_eq!(id.slot(), 0xFFFF);
1449
1450        let id2 = GraphemeId::new(0, 0, GraphemeId::MAX_WIDTH);
1451        assert_eq!(id2.slot(), 0);
1452        assert_eq!(id2.width(), GraphemeId::MAX_WIDTH as usize);
1453    }
1454
1455    // ====== StyleFlags coverage ======
1456
1457    #[test]
1458    fn style_flags_empty_has_no_bits() {
1459        assert!(StyleFlags::empty().is_empty());
1460        assert_eq!(StyleFlags::empty().bits(), 0);
1461    }
1462
1463    #[test]
1464    fn style_flags_all_has_all_bits() {
1465        let all = StyleFlags::all();
1466        assert!(all.contains(StyleFlags::BOLD));
1467        assert!(all.contains(StyleFlags::DIM));
1468        assert!(all.contains(StyleFlags::ITALIC));
1469        assert!(all.contains(StyleFlags::UNDERLINE));
1470        assert!(all.contains(StyleFlags::BLINK));
1471        assert!(all.contains(StyleFlags::REVERSE));
1472        assert!(all.contains(StyleFlags::STRIKETHROUGH));
1473        assert!(all.contains(StyleFlags::HIDDEN));
1474    }
1475
1476    #[test]
1477    fn style_flags_union_and_intersection() {
1478        let a = StyleFlags::BOLD | StyleFlags::ITALIC;
1479        let b = StyleFlags::ITALIC | StyleFlags::UNDERLINE;
1480        assert_eq!(
1481            a | b,
1482            StyleFlags::BOLD | StyleFlags::ITALIC | StyleFlags::UNDERLINE
1483        );
1484        assert_eq!(a & b, StyleFlags::ITALIC);
1485    }
1486
1487    #[test]
1488    fn style_flags_from_bits_truncate() {
1489        // 0xFF should give all flags
1490        let all = StyleFlags::from_bits_truncate(0xFF);
1491        assert_eq!(all, StyleFlags::all());
1492
1493        // 0x00 should give empty
1494        let none = StyleFlags::from_bits_truncate(0x00);
1495        assert!(none.is_empty());
1496    }
1497
1498    // ====== Edge-case tests (bd-35ddr) ======
1499
1500    // -- PackedRgba edge cases --
1501
1502    #[test]
1503    fn over_not_commutative() {
1504        let red_half = PackedRgba::rgba(255, 0, 0, 128);
1505        let blue_half = PackedRgba::rgba(0, 0, 255, 128);
1506        let a_over_b = red_half.over(blue_half);
1507        let b_over_a = blue_half.over(red_half);
1508        // Porter-Duff SourceOver is NOT commutative
1509        assert_ne!(a_over_b, b_over_a);
1510    }
1511
1512    #[test]
1513    fn over_opaque_self_compositing_is_idempotent() {
1514        let c = PackedRgba::rgba(42, 84, 168, 255);
1515        assert_eq!(c.over(c), c);
1516    }
1517
1518    #[test]
1519    fn over_near_opaque_src_alpha_254() {
1520        // Non-trivial branch: alpha=254 takes the blending path, not the early-out
1521        let src = PackedRgba::rgba(255, 0, 0, 254);
1522        let dst = PackedRgba::rgba(0, 0, 255, 255);
1523        let result = src.over(dst);
1524        assert_eq!(result.a(), 255);
1525        // Red should dominate but not quite 255
1526        assert!(result.r() >= 253, "r={}", result.r());
1527        assert!(result.b() <= 2, "b={}", result.b());
1528    }
1529
1530    #[test]
1531    fn over_both_partial_alpha_symmetric_colors() {
1532        // 128-alpha red over 128-alpha red — output alpha should be ~192
1533        let c = PackedRgba::rgba(200, 100, 50, 128);
1534        let result = c.over(c);
1535        let ref_result = reference_over(c, c);
1536        assert_eq!(result, ref_result);
1537        // Alpha: 128 + 128*(1-128/255) = 128 + 128*0.498 ≈ 192
1538        assert!(result.a() >= 190 && result.a() <= 194, "a={}", result.a());
1539    }
1540
1541    #[test]
1542    fn over_both_alpha_1_minimal() {
1543        let src = PackedRgba::rgba(255, 255, 255, 1);
1544        let dst = PackedRgba::rgba(0, 0, 0, 1);
1545        let result = src.over(dst);
1546        let ref_result = reference_over(src, dst);
1547        assert_eq!(result, ref_result);
1548        // Output alpha should be ~2 (very transparent)
1549        assert!(result.a() <= 3, "a={}", result.a());
1550    }
1551
1552    #[test]
1553    fn over_white_alpha_0_over_opaque_is_dst() {
1554        // White with alpha=0 should be treated as transparent
1555        let src = PackedRgba::rgba(255, 255, 255, 0);
1556        let dst = PackedRgba::rgba(100, 50, 25, 255);
1557        assert_eq!(src.over(dst), dst);
1558    }
1559
1560    #[test]
1561    fn with_opacity_nan_clamps_to_zero() {
1562        let c = PackedRgba::rgba(10, 20, 30, 200);
1563        let result = c.with_opacity(f32::NAN);
1564        // NaN.clamp(0.0, 1.0) returns... let's verify behavior
1565        // In Rust, NaN.clamp returns NaN, and NaN * 200 = NaN, then NaN.round() = NaN
1566        // NaN as u8 = 0 in Rust
1567        assert_eq!(result.r(), 10);
1568        assert_eq!(result.g(), 20);
1569        assert_eq!(result.b(), 30);
1570    }
1571
1572    #[test]
1573    fn with_opacity_negative_infinity_clamps_to_zero() {
1574        let c = PackedRgba::rgba(10, 20, 30, 200);
1575        let result = c.with_opacity(f32::NEG_INFINITY);
1576        assert_eq!(result.a(), 0);
1577    }
1578
1579    #[test]
1580    fn with_opacity_positive_infinity_clamps_to_original() {
1581        let c = PackedRgba::rgba(10, 20, 30, 200);
1582        let result = c.with_opacity(f32::INFINITY);
1583        assert_eq!(result.a(), 200);
1584    }
1585
1586    #[test]
1587    fn with_opacity_on_transparent_stays_transparent() {
1588        let c = PackedRgba::TRANSPARENT;
1589        assert_eq!(c.with_opacity(0.5).a(), 0);
1590        assert_eq!(c.with_opacity(1.0).a(), 0);
1591    }
1592
1593    #[test]
1594    fn packed_rgba_extreme_channel_values() {
1595        let all_max = PackedRgba::rgba(255, 255, 255, 255);
1596        assert_eq!(all_max.r(), 255);
1597        assert_eq!(all_max.g(), 255);
1598        assert_eq!(all_max.b(), 255);
1599        assert_eq!(all_max.a(), 255);
1600
1601        let all_zero = PackedRgba::rgba(0, 0, 0, 0);
1602        assert_eq!(all_zero, PackedRgba::TRANSPARENT);
1603    }
1604
1605    #[test]
1606    fn packed_rgba_hash_differs_for_different_values() {
1607        use std::collections::HashSet;
1608        let mut set = HashSet::new();
1609        set.insert(PackedRgba::RED);
1610        set.insert(PackedRgba::GREEN);
1611        set.insert(PackedRgba::BLUE);
1612        set.insert(PackedRgba::RED); // duplicate
1613        assert_eq!(set.len(), 3);
1614    }
1615
1616    #[test]
1617    fn packed_rgba_channel_isolation() {
1618        // Changing one channel should not affect others
1619        let base = PackedRgba::rgba(10, 20, 30, 40);
1620        let different_r = PackedRgba::rgba(99, 20, 30, 40);
1621        assert_ne!(base, different_r);
1622        assert_eq!(base.g(), different_r.g());
1623        assert_eq!(base.b(), different_r.b());
1624        assert_eq!(base.a(), different_r.a());
1625    }
1626
1627    // -- CellContent edge cases --
1628
1629    #[test]
1630    fn cell_content_nul_char_equals_empty() {
1631        // '\0' as u32 == 0, same as EMPTY.raw()
1632        let nul = CellContent::from_char('\0');
1633        assert_eq!(nul.raw(), CellContent::EMPTY.raw());
1634        assert!(nul.is_empty());
1635        assert_eq!(nul.as_char(), None); // Empty is filtered out by as_char
1636    }
1637
1638    #[test]
1639    fn cell_content_soh_char_is_not_continuation() {
1640        let soh = CellContent::from_char('\x01');
1641        assert_eq!(soh.raw(), 1);
1642        assert!(!soh.is_empty());
1643        assert!(!soh.is_continuation());
1644        assert_eq!(soh.as_char(), Some('\x01'));
1645    }
1646
1647    #[test]
1648    fn cell_content_max_unicode_codepoint() {
1649        let max = CellContent::from_char('\u{10FFFF}');
1650        assert_eq!(max.as_char(), Some('\u{10FFFF}'));
1651        assert!(!max.is_grapheme());
1652        // U+10FFFF is a noncharacter, should have width 1 (fast path)
1653        assert_eq!(max.width_hint(), 1);
1654    }
1655
1656    #[test]
1657    fn cell_content_bmp_boundary_chars() {
1658        // Last BMP char before surrogates (U+D7FF)
1659        let last_before_surrogates = CellContent::from_char('\u{D7FF}');
1660        assert_eq!(last_before_surrogates.as_char(), Some('\u{D7FF}'));
1661
1662        // First char after surrogates (U+E000)
1663        let first_after_surrogates = CellContent::from_char('\u{E000}');
1664        assert_eq!(first_after_surrogates.as_char(), Some('\u{E000}'));
1665
1666        // First supplementary char (U+10000)
1667        let supplementary = CellContent::from_char('\u{10000}');
1668        assert_eq!(supplementary.as_char(), Some('\u{10000}'));
1669        assert!(!supplementary.is_grapheme()); // bit 31 is NOT set (U+10000 = 0x10000)
1670    }
1671
1672    #[test]
1673    fn cell_content_grapheme_with_zero_width() {
1674        let id = GraphemeId::new(42, 0, 0);
1675        let c = CellContent::from_grapheme(id);
1676        assert_eq!(c.width_hint(), 0);
1677        assert_eq!(c.width(), 0);
1678        assert!(c.is_grapheme());
1679    }
1680
1681    #[test]
1682    fn cell_content_grapheme_with_max_width() {
1683        let id = GraphemeId::new(1, 0, GraphemeId::MAX_WIDTH);
1684        let c = CellContent::from_grapheme(id);
1685        assert_eq!(c.width_hint(), GraphemeId::MAX_WIDTH as usize);
1686        assert_eq!(c.width(), GraphemeId::MAX_WIDTH as usize);
1687    }
1688
1689    #[test]
1690    fn cell_content_continuation_value_is_max_i31() {
1691        // CONTINUATION = 0x7FFF_FFFF — max value with bit 31 clear
1692        assert_eq!(CellContent::CONTINUATION.raw(), 0x7FFF_FFFF);
1693        assert!(!CellContent::CONTINUATION.is_grapheme()); // bit 31 = 0
1694        assert!(CellContent::CONTINUATION.is_continuation());
1695    }
1696
1697    #[test]
1698    fn cell_content_empty_and_continuation_are_distinct() {
1699        assert_ne!(CellContent::EMPTY, CellContent::CONTINUATION);
1700        assert!(CellContent::EMPTY.is_empty());
1701        assert!(!CellContent::EMPTY.is_continuation());
1702        assert!(!CellContent::CONTINUATION.is_empty());
1703        assert!(CellContent::CONTINUATION.is_continuation());
1704    }
1705
1706    #[test]
1707    fn cell_content_grapheme_id_strips_high_bit() {
1708        let id = GraphemeId::new(
1709            GraphemeId::MAX_SLOT,
1710            GraphemeId::MAX_GENERATION,
1711            GraphemeId::MAX_WIDTH,
1712        );
1713        let c = CellContent::from_grapheme(id);
1714        let extracted = c.grapheme_id().unwrap();
1715        assert_eq!(extracted.slot(), id.slot());
1716        assert_eq!(extracted.generation(), id.generation());
1717        assert_eq!(extracted.width(), id.width());
1718    }
1719
1720    // -- GraphemeId edge cases --
1721
1722    #[test]
1723    fn grapheme_id_slot_one_width_one() {
1724        let id = GraphemeId::new(1, 0, 1);
1725        assert_eq!(id.slot(), 1);
1726        assert_eq!(id.width(), 1);
1727    }
1728
1729    #[test]
1730    fn grapheme_id_hash_eq_consistency() {
1731        use std::collections::HashSet;
1732        let a = GraphemeId::new(42, 0, 2);
1733        let b = GraphemeId::new(42, 0, 2);
1734        let c = GraphemeId::new(42, 0, 3);
1735        let d = GraphemeId::new(42, 1, 2);
1736        assert_eq!(a, b);
1737        assert_ne!(a, c);
1738        assert_ne!(a, d);
1739        let mut set = HashSet::new();
1740        set.insert(a);
1741        assert!(set.contains(&b));
1742        assert!(!set.contains(&c));
1743        assert!(!set.contains(&d));
1744    }
1745
1746    #[test]
1747    fn grapheme_id_adjacent_slots_differ() {
1748        let a = GraphemeId::new(0, 0, 1);
1749        let b = GraphemeId::new(1, 0, 1);
1750        assert_ne!(a, b);
1751        assert_ne!(a.slot(), b.slot());
1752        assert_eq!(a.width(), b.width());
1753    }
1754
1755    // -- CellAttrs edge cases --
1756
1757    #[test]
1758    fn cell_attrs_link_id_masks_overflow() {
1759        // In release mode (no debug_assert), overflow is masked to 24 bits
1760        let a = CellAttrs::new(StyleFlags::empty(), 0x00FF_FFFE);
1761        assert_eq!(a.link_id(), 0x00FF_FFFE);
1762    }
1763
1764    #[test]
1765    fn cell_attrs_chained_mutations() {
1766        let a = CellAttrs::new(StyleFlags::BOLD, 100)
1767            .with_flags(StyleFlags::ITALIC)
1768            .with_link(200)
1769            .with_flags(StyleFlags::UNDERLINE | StyleFlags::DIM)
1770            .with_link(300);
1771        assert_eq!(a.flags(), StyleFlags::UNDERLINE | StyleFlags::DIM);
1772        assert_eq!(a.link_id(), 300);
1773    }
1774
1775    #[test]
1776    fn cell_attrs_all_flags_max_link() {
1777        let all_flags = StyleFlags::all();
1778        let a = CellAttrs::new(all_flags, CellAttrs::LINK_ID_MAX);
1779        assert_eq!(a.flags(), all_flags);
1780        assert_eq!(a.link_id(), CellAttrs::LINK_ID_MAX);
1781        // Verify no bit overlap
1782        assert_eq!(a.flags().bits(), 0xFF);
1783        assert_eq!(a.link_id(), 0x00FF_FFFE);
1784    }
1785
1786    #[test]
1787    fn cell_attrs_link_id_none_is_zero() {
1788        assert_eq!(CellAttrs::LINK_ID_NONE, 0);
1789    }
1790
1791    // -- Cell edge cases --
1792
1793    #[test]
1794    fn cell_eq_matches_bits_eq() {
1795        let pairs = [
1796            (Cell::default(), Cell::default()),
1797            (Cell::from_char('A'), Cell::from_char('A')),
1798            (Cell::from_char('A'), Cell::from_char('B')),
1799            (Cell::CONTINUATION, Cell::CONTINUATION),
1800            (
1801                Cell::from_char('X').with_fg(PackedRgba::RED),
1802                Cell::from_char('X').with_fg(PackedRgba::BLUE),
1803            ),
1804        ];
1805        for (a, b) in &pairs {
1806            assert_eq!(
1807                a == b,
1808                a.bits_eq(b),
1809                "PartialEq and bits_eq disagree for {:?} vs {:?}",
1810                a,
1811                b
1812            );
1813        }
1814    }
1815
1816    #[test]
1817    fn cell_from_grapheme_content() {
1818        let id = GraphemeId::new(42, 0, 2);
1819        let cell = Cell::new(CellContent::from_grapheme(id));
1820        assert!(cell.content.is_grapheme());
1821        assert_eq!(cell.width_hint(), 2);
1822        assert!(!cell.is_empty());
1823        assert!(!cell.is_continuation());
1824    }
1825
1826    #[test]
1827    fn cell_with_char_on_continuation() {
1828        let cell = Cell::CONTINUATION.with_char('A');
1829        assert_eq!(cell.content.as_char(), Some('A'));
1830        assert!(!cell.is_continuation());
1831        // Colors from CONTINUATION are preserved
1832        assert_eq!(cell.fg, PackedRgba::TRANSPARENT);
1833        assert_eq!(cell.bg, PackedRgba::TRANSPARENT);
1834    }
1835
1836    #[test]
1837    fn cell_default_bits_eq_self() {
1838        let cell = Cell::default();
1839        assert!(cell.bits_eq(&cell));
1840    }
1841
1842    #[test]
1843    fn cell_new_empty_equals_default() {
1844        let a = Cell::new(CellContent::EMPTY);
1845        let b = Cell::default();
1846        assert!(a.bits_eq(&b));
1847    }
1848
1849    #[test]
1850    fn cell_all_builder_methods_chain() {
1851        let cell = Cell::default()
1852            .with_char('Z')
1853            .with_fg(PackedRgba::rgba(1, 2, 3, 4))
1854            .with_bg(PackedRgba::rgba(5, 6, 7, 8))
1855            .with_attrs(CellAttrs::new(
1856                StyleFlags::BOLD | StyleFlags::STRIKETHROUGH,
1857                999,
1858            ));
1859        assert_eq!(cell.content.as_char(), Some('Z'));
1860        assert_eq!(cell.fg.r(), 1);
1861        assert_eq!(cell.bg.a(), 8);
1862        assert!(cell.attrs.has_flag(StyleFlags::BOLD));
1863        assert!(cell.attrs.has_flag(StyleFlags::STRIKETHROUGH));
1864        assert!(!cell.attrs.has_flag(StyleFlags::ITALIC));
1865        assert_eq!(cell.attrs.link_id(), 999);
1866    }
1867
1868    #[test]
1869    fn cell_size_and_alignment_invariants() {
1870        // Non-negotiable: 16 bytes, 16-byte aligned
1871        assert_eq!(core::mem::size_of::<Cell>(), 16);
1872        assert_eq!(core::mem::align_of::<Cell>(), 16);
1873        // 4 cells per 64-byte cache line
1874        assert_eq!(64 / core::mem::size_of::<Cell>(), 4);
1875    }
1876
1877    #[test]
1878    fn cell_content_size_invariant() {
1879        assert_eq!(core::mem::size_of::<CellContent>(), 4);
1880    }
1881
1882    #[test]
1883    fn cell_attrs_size_invariant() {
1884        assert_eq!(core::mem::size_of::<CellAttrs>(), 4);
1885    }
1886
1887    // -- Porter-Duff over() stress tests --
1888
1889    #[test]
1890    fn over_associativity_approximate() {
1891        // Porter-Duff SourceOver is NOT perfectly associative due to rounding,
1892        // but results should be very close (within 1 per channel)
1893        let a = PackedRgba::rgba(200, 50, 100, 128);
1894        let b = PackedRgba::rgba(50, 200, 50, 128);
1895        let c = PackedRgba::rgba(100, 100, 200, 128);
1896
1897        let ab_c = a.over(b).over(c);
1898        let a_bc = a.over(b.over(c));
1899
1900        // Allow ±1 per channel for rounding differences
1901        assert!(
1902            (ab_c.r() as i16 - a_bc.r() as i16).unsigned_abs() <= 1,
1903            "r: {} vs {}",
1904            ab_c.r(),
1905            a_bc.r()
1906        );
1907        assert!(
1908            (ab_c.g() as i16 - a_bc.g() as i16).unsigned_abs() <= 1,
1909            "g: {} vs {}",
1910            ab_c.g(),
1911            a_bc.g()
1912        );
1913        assert!(
1914            (ab_c.b() as i16 - a_bc.b() as i16).unsigned_abs() <= 1,
1915            "b: {} vs {}",
1916            ab_c.b(),
1917            a_bc.b()
1918        );
1919        assert!(
1920            (ab_c.a() as i16 - a_bc.a() as i16).unsigned_abs() <= 1,
1921            "a: {} vs {}",
1922            ab_c.a(),
1923            a_bc.a()
1924        );
1925    }
1926
1927    #[test]
1928    fn over_output_alpha_monotonic_with_src_alpha() {
1929        // Higher src alpha → higher output alpha (or equal)
1930        let dst = PackedRgba::rgba(0, 0, 255, 128);
1931        let mut prev_a = 0u8;
1932        for alpha in (0..=255).step_by(5) {
1933            let src = PackedRgba::rgba(255, 0, 0, alpha);
1934            let result = src.over(dst);
1935            assert!(
1936                result.a() >= prev_a,
1937                "alpha monotonicity violated at src_a={}: result_a={} < prev={}",
1938                alpha,
1939                result.a(),
1940                prev_a
1941            );
1942            prev_a = result.a();
1943        }
1944    }
1945
1946    #[test]
1947    fn over_sweep_matches_reference() {
1948        // Sweep through alpha values and verify each matches the f64 reference
1949        for alpha in (0..=255).step_by(17) {
1950            let src = PackedRgba::rgba(200, 100, 50, alpha);
1951            let dst = PackedRgba::rgba(50, 100, 200, 200);
1952            assert_eq!(
1953                src.over(dst),
1954                reference_over(src, dst),
1955                "mismatch at src_alpha={}",
1956                alpha
1957            );
1958        }
1959    }
1960}
1961
1962/// Property tests for Cell types (bd-10i.13.2).
1963///
1964/// Top-level `#[cfg(test)]` scope: the `proptest!` macro has edition-2024
1965/// compatibility issues when nested inside another test module.
1966#[cfg(test)]
1967mod cell_proptests {
1968    use super::{Cell, CellAttrs, CellContent, GraphemeId, PackedRgba, StyleFlags};
1969    use proptest::prelude::*;
1970
1971    fn arb_packed_rgba() -> impl Strategy<Value = PackedRgba> {
1972        (any::<u8>(), any::<u8>(), any::<u8>(), any::<u8>())
1973            .prop_map(|(r, g, b, a)| PackedRgba::rgba(r, g, b, a))
1974    }
1975
1976    fn arb_grapheme_id() -> impl Strategy<Value = GraphemeId> {
1977        (
1978            0u32..=GraphemeId::MAX_SLOT,
1979            0u16..=GraphemeId::MAX_GENERATION,
1980            0u8..=GraphemeId::MAX_WIDTH,
1981        )
1982            .prop_map(|(slot, generation, width)| GraphemeId::new(slot, generation, width))
1983    }
1984
1985    fn arb_style_flags() -> impl Strategy<Value = StyleFlags> {
1986        any::<u8>().prop_map(StyleFlags::from_bits_truncate)
1987    }
1988
1989    proptest! {
1990        #[test]
1991        fn packed_rgba_roundtrips_all_components(tuple in (any::<u8>(), any::<u8>(), any::<u8>(), any::<u8>())) {
1992            let (r, g, b, a) = tuple;
1993            let c = PackedRgba::rgba(r, g, b, a);
1994            prop_assert_eq!(c.r(), r);
1995            prop_assert_eq!(c.g(), g);
1996            prop_assert_eq!(c.b(), b);
1997            prop_assert_eq!(c.a(), a);
1998        }
1999
2000        #[test]
2001        fn packed_rgba_rgb_always_opaque(tuple in (any::<u8>(), any::<u8>(), any::<u8>())) {
2002            let (r, g, b) = tuple;
2003            let c = PackedRgba::rgb(r, g, b);
2004            prop_assert_eq!(c.a(), 255);
2005            prop_assert_eq!(c.r(), r);
2006            prop_assert_eq!(c.g(), g);
2007            prop_assert_eq!(c.b(), b);
2008        }
2009
2010        #[test]
2011        fn packed_rgba_over_identity_transparent(dst in arb_packed_rgba()) {
2012            // Transparent source leaves destination unchanged
2013            let result = PackedRgba::TRANSPARENT.over(dst);
2014            prop_assert_eq!(result, dst);
2015        }
2016
2017        #[test]
2018        fn packed_rgba_over_identity_opaque(tuple in (any::<u8>(), any::<u8>(), any::<u8>(), arb_packed_rgba())) {
2019            // Fully opaque source replaces destination
2020            let (r, g, b, dst) = tuple;
2021            let src = PackedRgba::rgba(r, g, b, 255);
2022            let result = src.over(dst);
2023            prop_assert_eq!(result, src);
2024        }
2025
2026        #[test]
2027        fn grapheme_id_components_roundtrip(
2028            tuple in (
2029                0u32..=GraphemeId::MAX_SLOT,
2030                0u16..=GraphemeId::MAX_GENERATION,
2031                0u8..=GraphemeId::MAX_WIDTH,
2032            )
2033        ) {
2034            let (slot, generation, width) = tuple;
2035            let id = GraphemeId::new(slot, generation, width);
2036            prop_assert_eq!(id.slot(), slot as usize);
2037            prop_assert_eq!(id.generation(), generation);
2038            prop_assert_eq!(id.width(), width as usize);
2039        }
2040
2041        #[test]
2042        fn grapheme_id_raw_roundtrip(id in arb_grapheme_id()) {
2043            let raw = id.raw();
2044            let restored = GraphemeId::from_raw(raw);
2045            prop_assert_eq!(restored.slot(), id.slot());
2046            prop_assert_eq!(restored.width(), id.width());
2047        }
2048
2049        #[test]
2050        fn cell_content_char_roundtrip(c in (0x20u32..0xD800u32).prop_union(0xE000u32..0x110000u32)) {
2051            if let Some(ch) = char::from_u32(c) {
2052                let content = CellContent::from_char(ch);
2053                prop_assert_eq!(content.as_char(), Some(ch));
2054                prop_assert!(!content.is_grapheme());
2055                prop_assert!(!content.is_empty());
2056                prop_assert!(!content.is_continuation());
2057            }
2058        }
2059
2060        #[test]
2061        fn cell_content_grapheme_roundtrip(id in arb_grapheme_id()) {
2062            let content = CellContent::from_grapheme(id);
2063            prop_assert!(content.is_grapheme());
2064            prop_assert_eq!(content.grapheme_id(), Some(id));
2065            prop_assert_eq!(content.width_hint(), id.width());
2066        }
2067
2068        #[test]
2069        fn cell_bits_eq_is_reflexive(
2070            tuple in (
2071                (0x20u32..0x80u32).prop_map(|c| char::from_u32(c).unwrap()),
2072                any::<u8>(), any::<u8>(), any::<u8>(),
2073                arb_style_flags(),
2074            ),
2075        ) {
2076            let (c, r, g, b, flags) = tuple;
2077            let cell = Cell::from_char(c)
2078                .with_fg(PackedRgba::rgb(r, g, b))
2079                .with_attrs(CellAttrs::new(flags, 0));
2080            prop_assert!(cell.bits_eq(&cell));
2081        }
2082
2083        #[test]
2084        fn cell_bits_eq_detects_fg_difference(
2085            tuple in (
2086                (0x41u32..0x5Bu32).prop_map(|c| char::from_u32(c).unwrap()),
2087                any::<u8>(), any::<u8>(),
2088            ),
2089        ) {
2090            let (c, r1, r2) = tuple;
2091            prop_assume!(r1 != r2);
2092            let cell1 = Cell::from_char(c).with_fg(PackedRgba::rgb(r1, 0, 0));
2093            let cell2 = Cell::from_char(c).with_fg(PackedRgba::rgb(r2, 0, 0));
2094            prop_assert!(!cell1.bits_eq(&cell2));
2095        }
2096
2097        #[test]
2098        fn cell_attrs_flags_roundtrip(tuple in (arb_style_flags(), 0u32..CellAttrs::LINK_ID_MAX)) {
2099            let (flags, link) = tuple;
2100            let attrs = CellAttrs::new(flags, link);
2101            prop_assert_eq!(attrs.flags(), flags);
2102            prop_assert_eq!(attrs.link_id(), link);
2103        }
2104
2105        #[test]
2106        fn cell_attrs_with_flags_preserves_link(tuple in (arb_style_flags(), 0u32..CellAttrs::LINK_ID_MAX, arb_style_flags())) {
2107            let (flags, link, new_flags) = tuple;
2108            let attrs = CellAttrs::new(flags, link);
2109            let updated = attrs.with_flags(new_flags);
2110            prop_assert_eq!(updated.flags(), new_flags);
2111            prop_assert_eq!(updated.link_id(), link);
2112        }
2113
2114        #[test]
2115        fn cell_attrs_with_link_preserves_flags(tuple in (arb_style_flags(), 0u32..CellAttrs::LINK_ID_MAX, 0u32..CellAttrs::LINK_ID_MAX)) {
2116            let (flags, link1, link2) = tuple;
2117            let attrs = CellAttrs::new(flags, link1);
2118            let updated = attrs.with_link(link2);
2119            prop_assert_eq!(updated.flags(), flags);
2120            prop_assert_eq!(updated.link_id(), link2);
2121        }
2122
2123        // --- Executable Invariant Tests (bd-10i.13.2) ---
2124
2125        #[test]
2126        fn cell_bits_eq_is_symmetric(
2127            tuple in (
2128                (0x41u32..0x5Bu32).prop_map(|c| char::from_u32(c).unwrap()),
2129                (0x41u32..0x5Bu32).prop_map(|c| char::from_u32(c).unwrap()),
2130                arb_packed_rgba(),
2131                arb_packed_rgba(),
2132            ),
2133        ) {
2134            let (c1, c2, fg1, fg2) = tuple;
2135            let cell_a = Cell::from_char(c1).with_fg(fg1);
2136            let cell_b = Cell::from_char(c2).with_fg(fg2);
2137            prop_assert_eq!(cell_a.bits_eq(&cell_b), cell_b.bits_eq(&cell_a),
2138                "bits_eq is not symmetric");
2139        }
2140
2141        #[test]
2142        fn cell_content_bit31_discriminates(id in arb_grapheme_id()) {
2143            // Char content: bit 31 is 0
2144            let char_content = CellContent::from_char('A');
2145            prop_assert!(!char_content.is_grapheme());
2146            prop_assert!(char_content.as_char().is_some());
2147            prop_assert!(char_content.grapheme_id().is_none());
2148
2149            // Grapheme content: bit 31 is 1
2150            let grapheme_content = CellContent::from_grapheme(id);
2151            prop_assert!(grapheme_content.is_grapheme());
2152            prop_assert!(grapheme_content.grapheme_id().is_some());
2153            prop_assert!(grapheme_content.as_char().is_none());
2154        }
2155
2156        #[test]
2157        fn cell_from_char_width_matches_unicode(
2158            c in (0x20u32..0x7Fu32).prop_map(|c| char::from_u32(c).unwrap()),
2159        ) {
2160            let cell = Cell::from_char(c);
2161            prop_assert_eq!(cell.width_hint(), 1,
2162                "Cell width hint for '{}' should be 1 for ASCII", c);
2163        }
2164    }
2165
2166    // Zero-parameter invariant tests (cannot be inside proptest! macro)
2167
2168    #[test]
2169    fn cell_content_continuation_has_zero_width() {
2170        let cont = CellContent::CONTINUATION;
2171        assert_eq!(cont.width(), 0, "CONTINUATION cell should have width 0");
2172        assert!(cont.is_continuation());
2173        assert!(!cont.is_grapheme());
2174    }
2175
2176    #[test]
2177    fn cell_content_empty_has_zero_width() {
2178        let empty = CellContent::EMPTY;
2179        assert_eq!(empty.width(), 0, "EMPTY cell should have width 0");
2180        assert!(empty.is_empty());
2181        assert!(!empty.is_grapheme());
2182        assert!(!empty.is_continuation());
2183    }
2184
2185    #[test]
2186    fn cell_default_is_empty() {
2187        let cell = Cell::default();
2188        assert!(cell.is_empty());
2189        assert_eq!(cell.width_hint(), 0);
2190    }
2191}
2192
2193#[cfg(test)]
2194mod bit_layout_tests {
2195    use super::GraphemeId;
2196
2197    #[test]
2198    fn grapheme_id_bit_layout_verification() {
2199        // Layout verification to match corrected documentation:
2200        // [30-27: width (4 bits)][26-16: generation (11 bits)][15-0: pool slot (16 bits)]
2201
2202        // Slot = 0xFFFF (max 16 bits)
2203        let t1 = GraphemeId::new(0xFFFF, 0, 0);
2204        assert_eq!(t1.raw(), 0xFFFF, "Slot 0xFFFF should be 0xFFFF");
2205
2206        // Generation = 0x7FF (max 11 bits)
2207        let t2 = GraphemeId::new(0, 0x7FF, 0);
2208        // 0x7FF << 16 = 0x07FF0000
2209        assert_eq!(t2.raw(), 0x07FF0000, "Gen 0x7FF should be 0x07FF0000");
2210
2211        // Width = 0xF (max 4 bits)
2212        let t3 = GraphemeId::new(0, 0, 0xF);
2213        // 0xF << 27 = 0x78000000
2214        assert_eq!(t3.raw(), 0x78000000, "Width 0xF should be 0x78000000");
2215
2216        // Combined max values
2217        let t4 = GraphemeId::new(0xFFFF, 0x7FF, 0xF);
2218        // 0x78000000 | 0x07FF0000 | 0xFFFF = 0x7FFFFFFF
2219        assert_eq!(
2220            t4.raw(),
2221            0x7FFFFFFF,
2222            "Combined max values should be 0x7FFFFFFF"
2223        );
2224
2225        // Ensure bit 31 is clear (reserved for CellContent discriminator)
2226        assert_eq!(t4.raw() & 0x80000000, 0, "Bit 31 must be clear");
2227    }
2228}