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