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-24: width (7 bits)][23-0: pool slot (24 bits)]
35/// ```
36///
37/// # Capacity
38///
39/// - Pool slots: 16,777,216 (24 bits = 16M entries)
40/// - Width range: 0-127 (7 bits, plenty for any display width)
41///
42/// # Design Rationale
43///
44/// - 24 bits for slot allows 16M unique graphemes (far exceeding practical usage)
45/// - 7 bits for width allows display widths 0-127 (most graphemes are 1-2)
46/// - Embedded width avoids pool lookup for width queries
47/// - Total 31 bits leaves bit 31 for `CellContent` type discrimination
48#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
49#[repr(transparent)]
50pub struct GraphemeId(u32);
51
52impl GraphemeId {
53    /// Maximum slot index (24 bits).
54    pub const MAX_SLOT: u32 = 0x00FF_FFFF;
55
56    /// Maximum width (7 bits).
57    pub const MAX_WIDTH: u8 = 127;
58
59    /// Create a new `GraphemeId` from slot index and display width.
60    ///
61    /// # Panics
62    ///
63    /// Panics in debug mode if `slot > MAX_SLOT` or `width > MAX_WIDTH`.
64    #[inline]
65    pub const fn new(slot: u32, width: u8) -> Self {
66        debug_assert!(slot <= Self::MAX_SLOT, "slot overflow");
67        debug_assert!(width <= Self::MAX_WIDTH, "width overflow");
68        Self((slot & Self::MAX_SLOT) | ((width as u32) << 24))
69    }
70
71    /// Extract the pool slot index (0-16M).
72    #[inline]
73    pub const fn slot(self) -> usize {
74        (self.0 & Self::MAX_SLOT) as usize
75    }
76
77    /// Extract the display width (0-127).
78    #[inline]
79    pub const fn width(self) -> usize {
80        ((self.0 >> 24) & 0x7F) as usize
81    }
82
83    /// Raw u32 value for storage in `CellContent`.
84    #[inline]
85    pub const fn raw(self) -> u32 {
86        self.0
87    }
88
89    /// Reconstruct from a raw u32.
90    #[inline]
91    pub const fn from_raw(raw: u32) -> Self {
92        Self(raw)
93    }
94}
95
96impl core::fmt::Debug for GraphemeId {
97    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
98        f.debug_struct("GraphemeId")
99            .field("slot", &self.slot())
100            .field("width", &self.width())
101            .finish()
102    }
103}
104
105/// Cell content: either a direct Unicode char or a reference to a grapheme cluster.
106///
107/// # Encoding Scheme (4 bytes)
108///
109/// ```text
110/// Bit 31 (type discriminator):
111///   0: Direct char (bits 0-20 contain Unicode scalar value, max U+10FFFF)
112///   1: GraphemeId reference (bits 0-30 contain slot + width)
113/// ```
114///
115/// This allows:
116/// - 99% of cells (ASCII/BMP) to be stored without heap allocation
117/// - Complex graphemes (emoji, ZWJ sequences) stored in pool
118///
119/// # Special Values
120///
121/// - `EMPTY` (0x0): Empty cell, width 0
122/// - `CONTINUATION` (0x1): Placeholder for wide character continuation
123#[derive(Clone, Copy, PartialEq, Eq, Hash)]
124#[repr(transparent)]
125pub struct CellContent(u32);
126
127impl CellContent {
128    /// Empty cell content (no character).
129    pub const EMPTY: Self = Self(0);
130
131    /// Continuation marker for wide characters.
132    ///
133    /// When a character has display width > 1, subsequent cells are filled
134    /// with this marker to indicate they are part of the previous character.
135    ///
136    /// Value is `0x7FFF_FFFF` (max i32), which is outside valid Unicode scalar
137    /// range (0..0x10FFFF) but fits in 31 bits (Direct Char mode).
138    pub const CONTINUATION: Self = Self(0x7FFF_FFFF);
139
140    /// Create content from a single Unicode character.
141    ///
142    /// For characters with display width > 1, subsequent cells should be
143    /// filled with `CONTINUATION`.
144    #[inline]
145    pub const fn from_char(c: char) -> Self {
146        Self(c as u32)
147    }
148
149    /// Create content from a grapheme ID (for multi-codepoint clusters).
150    ///
151    /// The grapheme ID references an entry in the `GraphemePool`.
152    #[inline]
153    pub const fn from_grapheme(id: GraphemeId) -> Self {
154        Self(0x8000_0000 | id.raw())
155    }
156
157    /// Check if this content is a grapheme reference (vs direct char).
158    #[inline]
159    pub const fn is_grapheme(self) -> bool {
160        self.0 & 0x8000_0000 != 0
161    }
162
163    /// Check if this is a continuation cell (part of a wide character).
164    #[inline]
165    pub const fn is_continuation(self) -> bool {
166        self.0 == Self::CONTINUATION.0
167    }
168
169    /// Check if this cell is empty.
170    #[inline]
171    pub const fn is_empty(self) -> bool {
172        self.0 == Self::EMPTY.0
173    }
174
175    /// Extract the character if this is a direct char (not a grapheme).
176    ///
177    /// Returns `None` if this is empty, continuation, or a grapheme reference.
178    #[inline]
179    pub fn as_char(self) -> Option<char> {
180        if self.is_grapheme() || self.0 == Self::EMPTY.0 || self.0 == Self::CONTINUATION.0 {
181            None
182        } else {
183            char::from_u32(self.0)
184        }
185    }
186
187    /// Extract the grapheme ID if this is a grapheme reference.
188    ///
189    /// Returns `None` if this is a direct char.
190    #[inline]
191    pub const fn grapheme_id(self) -> Option<GraphemeId> {
192        if self.is_grapheme() {
193            Some(GraphemeId::from_raw(self.0 & !0x8000_0000))
194        } else {
195            None
196        }
197    }
198
199    /// Get the display width of this content.
200    ///
201    /// - Empty: 0
202    /// - Continuation: 0
203    /// - Grapheme: width embedded in GraphemeId
204    /// - Char: requires external width lookup (returns 1 as default for ASCII)
205    ///
206    /// Note: For accurate char width, use the unicode-display-width-based
207    /// helpers in this crate. This method provides a fast path for known cases.
208    #[inline]
209    pub const fn width_hint(self) -> usize {
210        if self.is_empty() || self.is_continuation() {
211            0
212        } else if self.is_grapheme() {
213            ((self.0 >> 24) & 0x7F) as usize
214        } else {
215            // For direct chars, assume width 1 (fast path for ASCII)
216            // Callers should use unicode-width for accurate measurement
217            1
218        }
219    }
220
221    /// Get the display width of this content with Unicode width semantics.
222    ///
223    /// This is the accurate (but slower) width computation for direct chars.
224    #[inline]
225    pub fn width(self) -> usize {
226        if self.is_empty() || self.is_continuation() {
227            0
228        } else if self.is_grapheme() {
229            ((self.0 >> 24) & 0x7F) as usize
230        } else {
231            let Some(c) = self.as_char() else {
232                return 1;
233            };
234            char_width(c)
235        }
236    }
237
238    /// Raw u32 value.
239    #[inline]
240    pub const fn raw(self) -> u32 {
241        self.0
242    }
243}
244
245impl Default for CellContent {
246    fn default() -> Self {
247        Self::EMPTY
248    }
249}
250
251impl core::fmt::Debug for CellContent {
252    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
253        if self.is_empty() {
254            write!(f, "CellContent::EMPTY")
255        } else if self.is_continuation() {
256            write!(f, "CellContent::CONTINUATION")
257        } else if let Some(c) = self.as_char() {
258            write!(f, "CellContent::Char({c:?})")
259        } else if let Some(id) = self.grapheme_id() {
260            write!(f, "CellContent::Grapheme({id:?})")
261        } else {
262            write!(f, "CellContent(0x{:08x})", self.0)
263        }
264    }
265}
266
267/// A single terminal cell (16 bytes).
268///
269/// # Layout
270///
271/// ```text
272/// #[repr(C, align(16))]
273/// Cell {
274///     content: CellContent,  // 4 bytes
275///     fg: PackedRgba,        // 4 bytes
276///     bg: PackedRgba,        // 4 bytes
277///     attrs: CellAttrs,      // 4 bytes
278/// }
279/// ```
280///
281/// # Invariants
282///
283/// - Size is exactly 16 bytes (verified by compile-time assert)
284/// - All fields are valid (no uninitialized memory)
285/// - Continuation cells should not have meaningful fg/bg (they inherit from parent)
286///
287/// # Default
288///
289/// The default cell is empty with transparent background, white foreground,
290/// and no style attributes.
291#[derive(Clone, Copy, PartialEq, Eq)]
292#[repr(C, align(16))]
293pub struct Cell {
294    /// Character or grapheme content.
295    pub content: CellContent,
296    /// Foreground color.
297    pub fg: PackedRgba,
298    /// Background color.
299    pub bg: PackedRgba,
300    /// Style flags and hyperlink ID.
301    pub attrs: CellAttrs,
302}
303
304// Compile-time size check
305const _: () = assert!(core::mem::size_of::<Cell>() == 16);
306
307impl Cell {
308    /// A continuation cell (placeholder for wide characters).
309    ///
310    /// When a character has display width > 1, subsequent cells are filled
311    /// with this to indicate they are "owned" by the previous cell.
312    pub const CONTINUATION: Self = Self {
313        content: CellContent::CONTINUATION,
314        fg: PackedRgba::TRANSPARENT,
315        bg: PackedRgba::TRANSPARENT,
316        attrs: CellAttrs::NONE,
317    };
318
319    /// Create a new cell with the given content and default colors.
320    #[inline]
321    pub const fn new(content: CellContent) -> Self {
322        Self {
323            content,
324            fg: PackedRgba::WHITE,
325            bg: PackedRgba::TRANSPARENT,
326            attrs: CellAttrs::NONE,
327        }
328    }
329
330    /// Create a cell from a single character.
331    #[inline]
332    pub const fn from_char(c: char) -> Self {
333        Self::new(CellContent::from_char(c))
334    }
335
336    /// Check if this is a continuation cell.
337    #[inline]
338    pub const fn is_continuation(&self) -> bool {
339        self.content.is_continuation()
340    }
341
342    /// Check if this cell is empty.
343    #[inline]
344    pub const fn is_empty(&self) -> bool {
345        self.content.is_empty()
346    }
347
348    /// Get the display width hint for this cell.
349    ///
350    /// See [`CellContent::width_hint`] for details.
351    #[inline]
352    pub const fn width_hint(&self) -> usize {
353        self.content.width_hint()
354    }
355
356    /// Bitwise equality comparison (fast path for diffing).
357    ///
358    /// Uses bitwise AND (`&`) instead of short-circuit AND (`&&`) so all
359    /// four u32 comparisons are always evaluated. This avoids branch
360    /// mispredictions in tight loops and allows LLVM to lower the check
361    /// to a single 128-bit SIMD compare on supported targets.
362    #[inline]
363    pub fn bits_eq(&self, other: &Self) -> bool {
364        (self.content.raw() == other.content.raw())
365            & (self.fg == other.fg)
366            & (self.bg == other.bg)
367            & (self.attrs == other.attrs)
368    }
369
370    /// Set the cell content to a character, preserving other fields.
371    #[inline]
372    pub const fn with_char(mut self, c: char) -> Self {
373        self.content = CellContent::from_char(c);
374        self
375    }
376
377    /// Set the foreground color.
378    #[inline]
379    pub const fn with_fg(mut self, fg: PackedRgba) -> Self {
380        self.fg = fg;
381        self
382    }
383
384    /// Set the background color.
385    #[inline]
386    pub const fn with_bg(mut self, bg: PackedRgba) -> Self {
387        self.bg = bg;
388        self
389    }
390
391    /// Set the style attributes.
392    #[inline]
393    pub const fn with_attrs(mut self, attrs: CellAttrs) -> Self {
394        self.attrs = attrs;
395        self
396    }
397}
398impl Default for Cell {
399    fn default() -> Self {
400        Self {
401            content: CellContent::EMPTY,
402            fg: PackedRgba::WHITE,
403            bg: PackedRgba::TRANSPARENT,
404            attrs: CellAttrs::NONE,
405        }
406    }
407}
408
409impl core::fmt::Debug for Cell {
410    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
411        f.debug_struct("Cell")
412            .field("content", &self.content)
413            .field("fg", &self.fg)
414            .field("bg", &self.bg)
415            .field("attrs", &self.attrs)
416            .finish()
417    }
418}
419
420/// A compact RGBA color.
421///
422/// - **Size:** 4 bytes (fits within the `Cell` 16-byte budget).
423/// - **Layout:** `0xRRGGBBAA` (R in bits 31..24, A in bits 7..0).
424///
425/// Notes
426/// -----
427/// This is **straight alpha** storage (RGB channels are not pre-multiplied).
428/// Compositing uses Porter-Duff **SourceOver** (`src over dst`).
429#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
430#[repr(transparent)]
431pub struct PackedRgba(pub u32);
432
433impl PackedRgba {
434    /// Fully transparent (alpha = 0).
435    pub const TRANSPARENT: Self = Self(0);
436    /// Opaque black.
437    pub const BLACK: Self = Self::rgb(0, 0, 0);
438    /// Opaque white.
439    pub const WHITE: Self = Self::rgb(255, 255, 255);
440    /// Opaque red.
441    pub const RED: Self = Self::rgb(255, 0, 0);
442    /// Opaque green.
443    pub const GREEN: Self = Self::rgb(0, 255, 0);
444    /// Opaque blue.
445    pub const BLUE: Self = Self::rgb(0, 0, 255);
446
447    /// Create an opaque RGB color (alpha = 255).
448    #[inline]
449    pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
450        Self::rgba(r, g, b, 255)
451    }
452
453    /// Create an RGBA color with explicit alpha.
454    #[inline]
455    pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
456        Self(((r as u32) << 24) | ((g as u32) << 16) | ((b as u32) << 8) | (a as u32))
457    }
458
459    /// Red channel.
460    #[inline]
461    pub const fn r(self) -> u8 {
462        (self.0 >> 24) as u8
463    }
464
465    /// Green channel.
466    #[inline]
467    pub const fn g(self) -> u8 {
468        (self.0 >> 16) as u8
469    }
470
471    /// Blue channel.
472    #[inline]
473    pub const fn b(self) -> u8 {
474        (self.0 >> 8) as u8
475    }
476
477    /// Alpha channel.
478    #[inline]
479    pub const fn a(self) -> u8 {
480        self.0 as u8
481    }
482
483    #[inline]
484    const fn div_round_u8(numer: u64, denom: u64) -> u8 {
485        debug_assert!(denom != 0);
486        let v = (numer + (denom / 2)) / denom;
487        if v > 255 { 255 } else { v as u8 }
488    }
489
490    /// Porter-Duff SourceOver: `src over dst`.
491    ///
492    /// Stored as straight alpha, so we compute the exact rational form and round at the end
493    /// (avoids accumulating rounding error across intermediate steps).
494    #[inline]
495    pub fn over(self, dst: Self) -> Self {
496        let s_a = self.a() as u64;
497        if s_a == 255 {
498            return self;
499        }
500        if s_a == 0 {
501            return dst;
502        }
503
504        let d_a = dst.a() as u64;
505        let inv_s_a = 255 - s_a;
506
507        // out_a = s_a + d_a*(1 - s_a)  (all in [0,1], scaled by 255)
508        // We compute numer_a in the "255^2 domain" to keep channels exact:
509        // numer_a = 255*s_a + d_a*(255 - s_a)
510        // out_a_u8 = round(numer_a / 255)
511        let numer_a = 255 * s_a + d_a * inv_s_a;
512        if numer_a == 0 {
513            return Self::TRANSPARENT;
514        }
515
516        let out_a = Self::div_round_u8(numer_a, 255);
517
518        // For straight alpha, the exact rational (scaled to [0,255]) is:
519        // out_c_u8 = round( (src_c*s_a*255 + dst_c*d_a*(255 - s_a)) / numer_a )
520        let r = Self::div_round_u8(
521            (self.r() as u64) * s_a * 255 + (dst.r() as u64) * d_a * inv_s_a,
522            numer_a,
523        );
524        let g = Self::div_round_u8(
525            (self.g() as u64) * s_a * 255 + (dst.g() as u64) * d_a * inv_s_a,
526            numer_a,
527        );
528        let b = Self::div_round_u8(
529            (self.b() as u64) * s_a * 255 + (dst.b() as u64) * d_a * inv_s_a,
530            numer_a,
531        );
532
533        Self::rgba(r, g, b, out_a)
534    }
535
536    /// Apply uniform opacity in `[0.0, 1.0]` by scaling alpha.
537    #[inline]
538    pub fn with_opacity(self, opacity: f32) -> Self {
539        let opacity = opacity.clamp(0.0, 1.0);
540        let a = ((self.a() as f32) * opacity).round().clamp(0.0, 255.0) as u8;
541        Self::rgba(self.r(), self.g(), self.b(), a)
542    }
543}
544
545bitflags::bitflags! {
546    /// 8-bit cell style flags.
547    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
548    pub struct StyleFlags: u8 {
549        /// Bold / increased intensity.
550        const BOLD          = 0b0000_0001;
551        /// Dim / decreased intensity.
552        const DIM           = 0b0000_0010;
553        /// Italic text.
554        const ITALIC        = 0b0000_0100;
555        /// Underlined text.
556        const UNDERLINE     = 0b0000_1000;
557        /// Blinking text.
558        const BLINK         = 0b0001_0000;
559        /// Reverse video (swap fg/bg).
560        const REVERSE       = 0b0010_0000;
561        /// Strikethrough text.
562        const STRIKETHROUGH = 0b0100_0000;
563        /// Hidden / invisible text.
564        const HIDDEN        = 0b1000_0000;
565    }
566}
567
568/// Packed cell attributes:
569/// - bits 31..24: `StyleFlags` (8 bits)
570/// - bits 23..0: `link_id` (24 bits)
571#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
572#[repr(transparent)]
573pub struct CellAttrs(u32);
574
575impl CellAttrs {
576    /// No attributes or link.
577    pub const NONE: Self = Self(0);
578
579    /// Sentinel value for "no hyperlink".
580    pub const LINK_ID_NONE: u32 = 0;
581    /// Maximum link ID (24-bit range).
582    pub const LINK_ID_MAX: u32 = 0x00FF_FFFE;
583
584    /// Create attributes from flags and a hyperlink ID.
585    #[inline]
586    pub fn new(flags: StyleFlags, link_id: u32) -> Self {
587        debug_assert!(
588            link_id <= Self::LINK_ID_MAX,
589            "link_id overflow: {link_id} (max={})",
590            Self::LINK_ID_MAX
591        );
592        Self(((flags.bits() as u32) << 24) | (link_id & 0x00FF_FFFF))
593    }
594
595    /// Extract the style flags.
596    #[inline]
597    pub fn flags(self) -> StyleFlags {
598        StyleFlags::from_bits_truncate((self.0 >> 24) as u8)
599    }
600
601    /// Extract the hyperlink ID.
602    #[inline]
603    pub fn link_id(self) -> u32 {
604        self.0 & 0x00FF_FFFF
605    }
606
607    /// Return a copy with different style flags.
608    #[inline]
609    pub fn with_flags(self, flags: StyleFlags) -> Self {
610        Self((self.0 & 0x00FF_FFFF) | ((flags.bits() as u32) << 24))
611    }
612
613    /// Return a copy with a different hyperlink ID.
614    #[inline]
615    pub fn with_link(self, link_id: u32) -> Self {
616        debug_assert!(
617            link_id <= Self::LINK_ID_MAX,
618            "link_id overflow: {link_id} (max={})",
619            Self::LINK_ID_MAX
620        );
621        Self((self.0 & 0xFF00_0000) | (link_id & 0x00FF_FFFF))
622    }
623
624    /// Check whether a specific flag is set.
625    #[inline]
626    pub fn has_flag(self, flag: StyleFlags) -> bool {
627        self.flags().contains(flag)
628    }
629}
630
631#[cfg(test)]
632mod tests {
633    use super::{Cell, CellAttrs, CellContent, GraphemeId, PackedRgba, StyleFlags};
634
635    fn reference_over(src: PackedRgba, dst: PackedRgba) -> PackedRgba {
636        let sr = src.r() as f64 / 255.0;
637        let sg = src.g() as f64 / 255.0;
638        let sb = src.b() as f64 / 255.0;
639        let sa = src.a() as f64 / 255.0;
640
641        let dr = dst.r() as f64 / 255.0;
642        let dg = dst.g() as f64 / 255.0;
643        let db = dst.b() as f64 / 255.0;
644        let da = dst.a() as f64 / 255.0;
645
646        let out_a = sa + da * (1.0 - sa);
647        if out_a <= 0.0 {
648            return PackedRgba::TRANSPARENT;
649        }
650
651        let out_r = (sr * sa + dr * da * (1.0 - sa)) / out_a;
652        let out_g = (sg * sa + dg * da * (1.0 - sa)) / out_a;
653        let out_b = (sb * sa + db * da * (1.0 - sa)) / out_a;
654
655        let to_u8 = |x: f64| -> u8 { (x * 255.0).round().clamp(0.0, 255.0) as u8 };
656        PackedRgba::rgba(to_u8(out_r), to_u8(out_g), to_u8(out_b), to_u8(out_a))
657    }
658
659    #[test]
660    fn packed_rgba_is_4_bytes() {
661        assert_eq!(core::mem::size_of::<PackedRgba>(), 4);
662    }
663
664    #[test]
665    fn rgb_sets_alpha_to_255() {
666        let c = PackedRgba::rgb(1, 2, 3);
667        assert_eq!(c.r(), 1);
668        assert_eq!(c.g(), 2);
669        assert_eq!(c.b(), 3);
670        assert_eq!(c.a(), 255);
671    }
672
673    #[test]
674    fn rgba_round_trips_components() {
675        let c = PackedRgba::rgba(10, 20, 30, 40);
676        assert_eq!(c.r(), 10);
677        assert_eq!(c.g(), 20);
678        assert_eq!(c.b(), 30);
679        assert_eq!(c.a(), 40);
680    }
681
682    #[test]
683    fn over_with_opaque_src_returns_src() {
684        let src = PackedRgba::rgba(1, 2, 3, 255);
685        let dst = PackedRgba::rgba(9, 8, 7, 200);
686        assert_eq!(src.over(dst), src);
687    }
688
689    #[test]
690    fn over_with_transparent_src_returns_dst() {
691        let src = PackedRgba::TRANSPARENT;
692        let dst = PackedRgba::rgba(9, 8, 7, 200);
693        assert_eq!(src.over(dst), dst);
694    }
695
696    #[test]
697    fn over_blends_correctly_for_half_alpha_over_opaque() {
698        // 50% red over opaque blue -> purple-ish, and resulting alpha stays opaque.
699        let src = PackedRgba::rgba(255, 0, 0, 128);
700        let dst = PackedRgba::rgba(0, 0, 255, 255);
701        assert_eq!(src.over(dst), PackedRgba::rgba(128, 0, 127, 255));
702    }
703
704    #[test]
705    fn over_matches_reference_for_partial_alpha_cases() {
706        let cases = [
707            (
708                PackedRgba::rgba(200, 10, 10, 64),
709                PackedRgba::rgba(10, 200, 10, 128),
710            ),
711            (
712                PackedRgba::rgba(1, 2, 3, 1),
713                PackedRgba::rgba(250, 251, 252, 254),
714            ),
715            (
716                PackedRgba::rgba(100, 0, 200, 200),
717                PackedRgba::rgba(0, 120, 30, 50),
718            ),
719        ];
720
721        for (src, dst) in cases {
722            assert_eq!(src.over(dst), reference_over(src, dst));
723        }
724    }
725
726    #[test]
727    fn with_opacity_scales_alpha() {
728        let c = PackedRgba::rgba(10, 20, 30, 255);
729        assert_eq!(c.with_opacity(0.5).a(), 128);
730        assert_eq!(c.with_opacity(-1.0).a(), 0);
731        assert_eq!(c.with_opacity(2.0).a(), 255);
732    }
733
734    #[test]
735    fn cell_attrs_is_4_bytes() {
736        assert_eq!(core::mem::size_of::<CellAttrs>(), 4);
737    }
738
739    #[test]
740    fn cell_attrs_none_has_no_flags_and_no_link() {
741        assert!(CellAttrs::NONE.flags().is_empty());
742        assert_eq!(CellAttrs::NONE.link_id(), 0);
743    }
744
745    #[test]
746    fn cell_attrs_new_stores_flags_and_link() {
747        let flags = StyleFlags::BOLD | StyleFlags::ITALIC;
748        let a = CellAttrs::new(flags, 42);
749        assert_eq!(a.flags(), flags);
750        assert_eq!(a.link_id(), 42);
751    }
752
753    #[test]
754    fn cell_attrs_with_flags_preserves_link_id() {
755        let a = CellAttrs::new(StyleFlags::BOLD, 123);
756        let b = a.with_flags(StyleFlags::UNDERLINE);
757        assert_eq!(b.flags(), StyleFlags::UNDERLINE);
758        assert_eq!(b.link_id(), 123);
759    }
760
761    #[test]
762    fn cell_attrs_with_link_preserves_flags() {
763        let a = CellAttrs::new(StyleFlags::BOLD | StyleFlags::ITALIC, 1);
764        let b = a.with_link(999);
765        assert_eq!(b.flags(), StyleFlags::BOLD | StyleFlags::ITALIC);
766        assert_eq!(b.link_id(), 999);
767    }
768
769    #[test]
770    fn cell_attrs_flag_combinations_work() {
771        let flags = StyleFlags::BOLD | StyleFlags::ITALIC;
772        let a = CellAttrs::new(flags, 0);
773        assert!(a.has_flag(StyleFlags::BOLD));
774        assert!(a.has_flag(StyleFlags::ITALIC));
775        assert!(!a.has_flag(StyleFlags::UNDERLINE));
776    }
777
778    #[test]
779    fn cell_attrs_link_id_max_boundary() {
780        let a = CellAttrs::new(StyleFlags::empty(), CellAttrs::LINK_ID_MAX);
781        assert_eq!(a.link_id(), CellAttrs::LINK_ID_MAX);
782    }
783
784    // ====== GraphemeId tests ======
785
786    #[test]
787    fn grapheme_id_is_4_bytes() {
788        assert_eq!(core::mem::size_of::<GraphemeId>(), 4);
789    }
790
791    #[test]
792    fn grapheme_id_encoding_roundtrip() {
793        let id = GraphemeId::new(12345, 2);
794        assert_eq!(id.slot(), 12345);
795        assert_eq!(id.width(), 2);
796    }
797
798    #[test]
799    fn grapheme_id_max_values() {
800        let id = GraphemeId::new(GraphemeId::MAX_SLOT, GraphemeId::MAX_WIDTH);
801        assert_eq!(id.slot(), 0x00FF_FFFF);
802        assert_eq!(id.width(), 127);
803    }
804
805    #[test]
806    fn grapheme_id_zero_values() {
807        let id = GraphemeId::new(0, 0);
808        assert_eq!(id.slot(), 0);
809        assert_eq!(id.width(), 0);
810    }
811
812    #[test]
813    fn grapheme_id_raw_roundtrip() {
814        let id = GraphemeId::new(999, 5);
815        let raw = id.raw();
816        let restored = GraphemeId::from_raw(raw);
817        assert_eq!(restored.slot(), 999);
818        assert_eq!(restored.width(), 5);
819    }
820
821    // ====== CellContent tests ======
822
823    #[test]
824    fn cell_content_is_4_bytes() {
825        assert_eq!(core::mem::size_of::<CellContent>(), 4);
826    }
827
828    #[test]
829    fn cell_content_empty_properties() {
830        assert!(CellContent::EMPTY.is_empty());
831        assert!(!CellContent::EMPTY.is_continuation());
832        assert!(!CellContent::EMPTY.is_grapheme());
833        assert_eq!(CellContent::EMPTY.width_hint(), 0);
834    }
835
836    #[test]
837    fn cell_content_continuation_properties() {
838        assert!(CellContent::CONTINUATION.is_continuation());
839        assert!(!CellContent::CONTINUATION.is_empty());
840        assert!(!CellContent::CONTINUATION.is_grapheme());
841        assert_eq!(CellContent::CONTINUATION.width_hint(), 0);
842    }
843
844    #[test]
845    fn cell_content_from_char_ascii() {
846        let c = CellContent::from_char('A');
847        assert!(!c.is_grapheme());
848        assert!(!c.is_empty());
849        assert!(!c.is_continuation());
850        assert_eq!(c.as_char(), Some('A'));
851        assert_eq!(c.width_hint(), 1);
852    }
853
854    #[test]
855    fn cell_content_from_char_unicode() {
856        // BMP character
857        let c = CellContent::from_char('日');
858        assert_eq!(c.as_char(), Some('日'));
859        assert!(!c.is_grapheme());
860
861        // Supplementary plane character (emoji)
862        let c2 = CellContent::from_char('🎉');
863        assert_eq!(c2.as_char(), Some('🎉'));
864        assert!(!c2.is_grapheme());
865    }
866
867    #[test]
868    fn cell_content_from_grapheme() {
869        let id = GraphemeId::new(42, 2);
870        let c = CellContent::from_grapheme(id);
871
872        assert!(c.is_grapheme());
873        assert!(!c.is_empty());
874        assert!(!c.is_continuation());
875        assert_eq!(c.grapheme_id(), Some(id));
876        assert_eq!(c.as_char(), None);
877        assert_eq!(c.width_hint(), 2);
878    }
879
880    #[test]
881    fn cell_content_width_for_chars() {
882        let ascii = CellContent::from_char('A');
883        assert_eq!(ascii.width(), 1);
884
885        let wide = CellContent::from_char('日');
886        assert_eq!(wide.width(), 2);
887
888        let emoji = CellContent::from_char('🎉');
889        assert_eq!(emoji.width(), 2);
890
891        // Unicode East Asian Width properties:
892        // - '⚡' (U+26A1) is Wide → always width 2
893        // - '⚙' (U+2699) is Neutral → 1 (non-CJK) or 2 (CJK)
894        // - '❤' (U+2764) is Neutral → 1 (non-CJK) or 2 (CJK)
895        let bolt = CellContent::from_char('⚡');
896        assert_eq!(bolt.width(), 2, "bolt is Wide, always width 2");
897
898        // Neutral-width characters: width depends on CJK mode
899        let gear = CellContent::from_char('⚙');
900        let heart = CellContent::from_char('❤');
901        assert!(
902            [1, 2].contains(&gear.width()),
903            "gear should be 1 (non-CJK) or 2 (CJK), got {}",
904            gear.width()
905        );
906        assert_eq!(
907            gear.width(),
908            heart.width(),
909            "gear and heart should have same width (both Neutral)"
910        );
911    }
912
913    #[test]
914    fn cell_content_width_for_grapheme() {
915        let id = GraphemeId::new(7, 3);
916        let c = CellContent::from_grapheme(id);
917        assert_eq!(c.width(), 3);
918    }
919
920    #[test]
921    fn cell_content_width_empty_is_zero() {
922        assert_eq!(CellContent::EMPTY.width(), 0);
923        assert_eq!(CellContent::CONTINUATION.width(), 0);
924    }
925
926    #[test]
927    fn cell_content_grapheme_discriminator_bit() {
928        // Chars should have bit 31 = 0
929        let char_content = CellContent::from_char('X');
930        assert_eq!(char_content.raw() & 0x8000_0000, 0);
931
932        // Graphemes should have bit 31 = 1
933        let grapheme_content = CellContent::from_grapheme(GraphemeId::new(1, 1));
934        assert_ne!(grapheme_content.raw() & 0x8000_0000, 0);
935    }
936
937    // ====== Cell tests ======
938
939    #[test]
940    fn cell_is_16_bytes() {
941        assert_eq!(core::mem::size_of::<Cell>(), 16);
942    }
943
944    #[test]
945    fn cell_alignment_is_16() {
946        assert_eq!(core::mem::align_of::<Cell>(), 16);
947    }
948
949    #[test]
950    fn cell_default_properties() {
951        let cell = Cell::default();
952        assert!(cell.is_empty());
953        assert!(!cell.is_continuation());
954        assert_eq!(cell.fg, PackedRgba::WHITE);
955        assert_eq!(cell.bg, PackedRgba::TRANSPARENT);
956        assert_eq!(cell.attrs, CellAttrs::NONE);
957    }
958
959    #[test]
960    fn cell_continuation_constant() {
961        assert!(Cell::CONTINUATION.is_continuation());
962        assert!(!Cell::CONTINUATION.is_empty());
963    }
964
965    #[test]
966    fn cell_from_char() {
967        let cell = Cell::from_char('X');
968        assert_eq!(cell.content.as_char(), Some('X'));
969        assert_eq!(cell.fg, PackedRgba::WHITE);
970        assert_eq!(cell.bg, PackedRgba::TRANSPARENT);
971    }
972
973    #[test]
974    fn cell_builder_methods() {
975        let cell = Cell::from_char('A')
976            .with_fg(PackedRgba::rgb(255, 0, 0))
977            .with_bg(PackedRgba::rgb(0, 0, 255))
978            .with_attrs(CellAttrs::new(StyleFlags::BOLD, 0));
979
980        assert_eq!(cell.content.as_char(), Some('A'));
981        assert_eq!(cell.fg, PackedRgba::rgb(255, 0, 0));
982        assert_eq!(cell.bg, PackedRgba::rgb(0, 0, 255));
983        assert!(cell.attrs.has_flag(StyleFlags::BOLD));
984    }
985
986    #[test]
987    fn cell_bits_eq_same_cells() {
988        let cell1 = Cell::from_char('X').with_fg(PackedRgba::rgb(1, 2, 3));
989        let cell2 = Cell::from_char('X').with_fg(PackedRgba::rgb(1, 2, 3));
990        assert!(cell1.bits_eq(&cell2));
991    }
992
993    #[test]
994    fn cell_bits_eq_different_cells() {
995        let cell1 = Cell::from_char('X');
996        let cell2 = Cell::from_char('Y');
997        assert!(!cell1.bits_eq(&cell2));
998
999        let cell3 = Cell::from_char('X').with_fg(PackedRgba::rgb(1, 2, 3));
1000        assert!(!cell1.bits_eq(&cell3));
1001    }
1002
1003    #[test]
1004    fn cell_width_hint() {
1005        let empty = Cell::default();
1006        assert_eq!(empty.width_hint(), 0);
1007
1008        let cont = Cell::CONTINUATION;
1009        assert_eq!(cont.width_hint(), 0);
1010
1011        let ascii = Cell::from_char('A');
1012        assert_eq!(ascii.width_hint(), 1);
1013    }
1014
1015    // Property tests moved to top-level `cell_proptests` module for edition 2024 compat.
1016
1017    // ====== PackedRgba extended coverage ======
1018
1019    #[test]
1020    fn packed_rgba_named_constants() {
1021        assert_eq!(PackedRgba::TRANSPARENT, PackedRgba(0));
1022        assert_eq!(PackedRgba::TRANSPARENT.a(), 0);
1023
1024        assert_eq!(PackedRgba::BLACK.r(), 0);
1025        assert_eq!(PackedRgba::BLACK.g(), 0);
1026        assert_eq!(PackedRgba::BLACK.b(), 0);
1027        assert_eq!(PackedRgba::BLACK.a(), 255);
1028
1029        assert_eq!(PackedRgba::WHITE.r(), 255);
1030        assert_eq!(PackedRgba::WHITE.g(), 255);
1031        assert_eq!(PackedRgba::WHITE.b(), 255);
1032        assert_eq!(PackedRgba::WHITE.a(), 255);
1033
1034        assert_eq!(PackedRgba::RED, PackedRgba::rgb(255, 0, 0));
1035        assert_eq!(PackedRgba::GREEN, PackedRgba::rgb(0, 255, 0));
1036        assert_eq!(PackedRgba::BLUE, PackedRgba::rgb(0, 0, 255));
1037    }
1038
1039    #[test]
1040    fn packed_rgba_default_is_transparent() {
1041        assert_eq!(PackedRgba::default(), PackedRgba::TRANSPARENT);
1042    }
1043
1044    #[test]
1045    fn over_both_transparent_returns_transparent() {
1046        // Exercises numer_a == 0 branch (line 508)
1047        let result = PackedRgba::TRANSPARENT.over(PackedRgba::TRANSPARENT);
1048        assert_eq!(result, PackedRgba::TRANSPARENT);
1049    }
1050
1051    #[test]
1052    fn over_partial_alpha_over_transparent_dst() {
1053        // d_a == 0 path: src partial alpha over fully transparent
1054        let src = PackedRgba::rgba(200, 100, 50, 128);
1055        let result = src.over(PackedRgba::TRANSPARENT);
1056        // Output alpha = src_alpha (dst contributes nothing)
1057        assert_eq!(result.a(), 128);
1058        // Colors should be src colors since dst has no contribution
1059        assert_eq!(result.r(), 200);
1060        assert_eq!(result.g(), 100);
1061        assert_eq!(result.b(), 50);
1062    }
1063
1064    #[test]
1065    fn over_very_low_alpha() {
1066        // Near-transparent source (alpha=1) over opaque destination
1067        let src = PackedRgba::rgba(255, 0, 0, 1);
1068        let dst = PackedRgba::rgba(0, 0, 255, 255);
1069        let result = src.over(dst);
1070        // Result should be very close to dst
1071        assert_eq!(result.a(), 255);
1072        assert!(result.b() > 250, "b={} should be near 255", result.b());
1073        assert!(result.r() < 5, "r={} should be near 0", result.r());
1074    }
1075
1076    #[test]
1077    fn with_opacity_exact_zero() {
1078        let c = PackedRgba::rgba(10, 20, 30, 200);
1079        let result = c.with_opacity(0.0);
1080        assert_eq!(result.a(), 0);
1081        assert_eq!(result.r(), 10); // RGB preserved
1082        assert_eq!(result.g(), 20);
1083        assert_eq!(result.b(), 30);
1084    }
1085
1086    #[test]
1087    fn with_opacity_exact_one() {
1088        let c = PackedRgba::rgba(10, 20, 30, 200);
1089        let result = c.with_opacity(1.0);
1090        assert_eq!(result.a(), 200); // Alpha unchanged
1091        assert_eq!(result.r(), 10);
1092    }
1093
1094    #[test]
1095    fn with_opacity_preserves_rgb() {
1096        let c = PackedRgba::rgba(42, 84, 168, 255);
1097        let result = c.with_opacity(0.25);
1098        assert_eq!(result.r(), 42);
1099        assert_eq!(result.g(), 84);
1100        assert_eq!(result.b(), 168);
1101        assert_eq!(result.a(), 64); // 255 * 0.25 = 63.75 → 64
1102    }
1103
1104    // ====== CellContent extended coverage ======
1105
1106    #[test]
1107    fn cell_content_as_char_none_for_empty() {
1108        assert_eq!(CellContent::EMPTY.as_char(), None);
1109    }
1110
1111    #[test]
1112    fn cell_content_as_char_none_for_continuation() {
1113        assert_eq!(CellContent::CONTINUATION.as_char(), None);
1114    }
1115
1116    #[test]
1117    fn cell_content_as_char_none_for_grapheme() {
1118        let id = GraphemeId::new(1, 2);
1119        let c = CellContent::from_grapheme(id);
1120        assert_eq!(c.as_char(), None);
1121    }
1122
1123    #[test]
1124    fn cell_content_grapheme_id_none_for_char() {
1125        let c = CellContent::from_char('A');
1126        assert_eq!(c.grapheme_id(), None);
1127    }
1128
1129    #[test]
1130    fn cell_content_grapheme_id_none_for_empty() {
1131        assert_eq!(CellContent::EMPTY.grapheme_id(), None);
1132    }
1133
1134    #[test]
1135    fn cell_content_width_control_chars() {
1136        // Control characters have width 0, except tab/newline/CR which are 1 cell
1137        // Note: NUL (0x00) is CellContent::EMPTY, so test with other controls
1138        let tab = CellContent::from_char('\t');
1139        assert_eq!(tab.width(), 1);
1140
1141        let bel = CellContent::from_char('\x07');
1142        assert_eq!(bel.width(), 0);
1143    }
1144
1145    #[test]
1146    fn cell_content_width_hint_always_1_for_chars() {
1147        // width_hint is the fast path that always returns 1 for non-special chars
1148        let wide = CellContent::from_char('日');
1149        assert_eq!(wide.width_hint(), 1); // fast path says 1
1150        assert_eq!(wide.width(), 2); // accurate path says 2
1151    }
1152
1153    #[test]
1154    fn cell_content_default_is_empty() {
1155        assert_eq!(CellContent::default(), CellContent::EMPTY);
1156    }
1157
1158    #[test]
1159    fn cell_content_debug_empty() {
1160        let s = format!("{:?}", CellContent::EMPTY);
1161        assert_eq!(s, "CellContent::EMPTY");
1162    }
1163
1164    #[test]
1165    fn cell_content_debug_continuation() {
1166        let s = format!("{:?}", CellContent::CONTINUATION);
1167        assert_eq!(s, "CellContent::CONTINUATION");
1168    }
1169
1170    #[test]
1171    fn cell_content_debug_char() {
1172        let s = format!("{:?}", CellContent::from_char('X'));
1173        assert!(s.starts_with("CellContent::Char("), "got: {s}");
1174    }
1175
1176    #[test]
1177    fn cell_content_debug_grapheme() {
1178        let id = GraphemeId::new(1, 2);
1179        let s = format!("{:?}", CellContent::from_grapheme(id));
1180        assert!(s.starts_with("CellContent::Grapheme("), "got: {s}");
1181    }
1182
1183    #[test]
1184    fn cell_content_raw_value() {
1185        let c = CellContent::from_char('A');
1186        assert_eq!(c.raw(), 'A' as u32);
1187
1188        let g = CellContent::from_grapheme(GraphemeId::new(5, 2));
1189        assert_ne!(g.raw() & 0x8000_0000, 0);
1190    }
1191
1192    // ====== CellAttrs extended coverage ======
1193
1194    #[test]
1195    fn cell_attrs_default_is_none() {
1196        assert_eq!(CellAttrs::default(), CellAttrs::NONE);
1197    }
1198
1199    #[test]
1200    fn cell_attrs_each_flag_isolated() {
1201        let all_flags = [
1202            StyleFlags::BOLD,
1203            StyleFlags::DIM,
1204            StyleFlags::ITALIC,
1205            StyleFlags::UNDERLINE,
1206            StyleFlags::BLINK,
1207            StyleFlags::REVERSE,
1208            StyleFlags::STRIKETHROUGH,
1209            StyleFlags::HIDDEN,
1210        ];
1211
1212        for &flag in &all_flags {
1213            let a = CellAttrs::new(flag, 0);
1214            assert!(a.has_flag(flag), "flag {:?} should be set", flag);
1215
1216            // Verify no other flags are set
1217            for &other in &all_flags {
1218                if other != flag {
1219                    assert!(
1220                        !a.has_flag(other),
1221                        "flag {:?} should NOT be set when only {:?} is",
1222                        other,
1223                        flag
1224                    );
1225                }
1226            }
1227        }
1228    }
1229
1230    #[test]
1231    fn cell_attrs_all_flags_combined() {
1232        let all = StyleFlags::BOLD
1233            | StyleFlags::DIM
1234            | StyleFlags::ITALIC
1235            | StyleFlags::UNDERLINE
1236            | StyleFlags::BLINK
1237            | StyleFlags::REVERSE
1238            | StyleFlags::STRIKETHROUGH
1239            | StyleFlags::HIDDEN;
1240        let a = CellAttrs::new(all, 42);
1241        assert_eq!(a.flags(), all);
1242        assert!(a.has_flag(StyleFlags::BOLD));
1243        assert!(a.has_flag(StyleFlags::HIDDEN));
1244        assert_eq!(a.link_id(), 42);
1245    }
1246
1247    #[test]
1248    fn cell_attrs_link_id_zero() {
1249        let a = CellAttrs::new(StyleFlags::BOLD, CellAttrs::LINK_ID_NONE);
1250        assert_eq!(a.link_id(), 0);
1251        assert!(a.has_flag(StyleFlags::BOLD));
1252    }
1253
1254    #[test]
1255    fn cell_attrs_with_link_to_none() {
1256        let a = CellAttrs::new(StyleFlags::ITALIC, 500);
1257        let b = a.with_link(CellAttrs::LINK_ID_NONE);
1258        assert_eq!(b.link_id(), 0);
1259        assert!(b.has_flag(StyleFlags::ITALIC));
1260    }
1261
1262    #[test]
1263    fn cell_attrs_with_flags_to_empty() {
1264        let a = CellAttrs::new(StyleFlags::BOLD | StyleFlags::ITALIC, 123);
1265        let b = a.with_flags(StyleFlags::empty());
1266        assert!(b.flags().is_empty());
1267        assert_eq!(b.link_id(), 123);
1268    }
1269
1270    // ====== Cell extended coverage ======
1271
1272    #[test]
1273    fn cell_bits_eq_detects_bg_difference() {
1274        let cell1 = Cell::from_char('X');
1275        let cell2 = Cell::from_char('X').with_bg(PackedRgba::RED);
1276        assert!(!cell1.bits_eq(&cell2));
1277    }
1278
1279    #[test]
1280    fn cell_bits_eq_detects_attrs_difference() {
1281        let cell1 = Cell::from_char('X');
1282        let cell2 = Cell::from_char('X').with_attrs(CellAttrs::new(StyleFlags::BOLD, 0));
1283        assert!(!cell1.bits_eq(&cell2));
1284    }
1285
1286    #[test]
1287    fn cell_with_char_preserves_colors_and_attrs() {
1288        let cell = Cell::from_char('A')
1289            .with_fg(PackedRgba::RED)
1290            .with_bg(PackedRgba::BLUE)
1291            .with_attrs(CellAttrs::new(StyleFlags::BOLD, 42));
1292
1293        let updated = cell.with_char('Z');
1294        assert_eq!(updated.content.as_char(), Some('Z'));
1295        assert_eq!(updated.fg, PackedRgba::RED);
1296        assert_eq!(updated.bg, PackedRgba::BLUE);
1297        assert!(updated.attrs.has_flag(StyleFlags::BOLD));
1298        assert_eq!(updated.attrs.link_id(), 42);
1299    }
1300
1301    #[test]
1302    fn cell_new_vs_from_char() {
1303        let a = Cell::new(CellContent::from_char('A'));
1304        let b = Cell::from_char('A');
1305        assert!(a.bits_eq(&b));
1306    }
1307
1308    #[test]
1309    fn cell_continuation_has_transparent_colors() {
1310        assert_eq!(Cell::CONTINUATION.fg, PackedRgba::TRANSPARENT);
1311        assert_eq!(Cell::CONTINUATION.bg, PackedRgba::TRANSPARENT);
1312        assert_eq!(Cell::CONTINUATION.attrs, CellAttrs::NONE);
1313    }
1314
1315    #[test]
1316    fn cell_debug_format() {
1317        let cell = Cell::from_char('A');
1318        let s = format!("{:?}", cell);
1319        assert!(s.contains("Cell"), "got: {s}");
1320        assert!(s.contains("content"), "got: {s}");
1321        assert!(s.contains("fg"), "got: {s}");
1322        assert!(s.contains("bg"), "got: {s}");
1323        assert!(s.contains("attrs"), "got: {s}");
1324    }
1325
1326    #[test]
1327    fn cell_is_empty_for_various() {
1328        assert!(Cell::default().is_empty());
1329        assert!(!Cell::from_char('A').is_empty());
1330        assert!(!Cell::CONTINUATION.is_empty());
1331    }
1332
1333    #[test]
1334    fn cell_is_continuation_for_various() {
1335        assert!(!Cell::default().is_continuation());
1336        assert!(!Cell::from_char('A').is_continuation());
1337        assert!(Cell::CONTINUATION.is_continuation());
1338    }
1339
1340    #[test]
1341    fn cell_width_hint_for_grapheme() {
1342        let id = GraphemeId::new(100, 3);
1343        let cell = Cell::new(CellContent::from_grapheme(id));
1344        assert_eq!(cell.width_hint(), 3);
1345    }
1346
1347    // ====== GraphemeId extended coverage ======
1348
1349    #[test]
1350    fn grapheme_id_default() {
1351        let id = GraphemeId::default();
1352        assert_eq!(id.slot(), 0);
1353        assert_eq!(id.width(), 0);
1354    }
1355
1356    #[test]
1357    fn grapheme_id_debug_format() {
1358        let id = GraphemeId::new(42, 2);
1359        let s = format!("{:?}", id);
1360        assert!(s.contains("GraphemeId"), "got: {s}");
1361        assert!(s.contains("42"), "got: {s}");
1362        assert!(s.contains("2"), "got: {s}");
1363    }
1364
1365    #[test]
1366    fn grapheme_id_width_isolated_from_slot() {
1367        // Verify slot bits don't leak into width field
1368        let id = GraphemeId::new(0x00FF_FFFF, 0);
1369        assert_eq!(id.width(), 0);
1370        assert_eq!(id.slot(), 0x00FF_FFFF);
1371
1372        let id2 = GraphemeId::new(0, 127);
1373        assert_eq!(id2.slot(), 0);
1374        assert_eq!(id2.width(), 127);
1375    }
1376
1377    // ====== StyleFlags coverage ======
1378
1379    #[test]
1380    fn style_flags_empty_has_no_bits() {
1381        assert!(StyleFlags::empty().is_empty());
1382        assert_eq!(StyleFlags::empty().bits(), 0);
1383    }
1384
1385    #[test]
1386    fn style_flags_all_has_all_bits() {
1387        let all = StyleFlags::all();
1388        assert!(all.contains(StyleFlags::BOLD));
1389        assert!(all.contains(StyleFlags::DIM));
1390        assert!(all.contains(StyleFlags::ITALIC));
1391        assert!(all.contains(StyleFlags::UNDERLINE));
1392        assert!(all.contains(StyleFlags::BLINK));
1393        assert!(all.contains(StyleFlags::REVERSE));
1394        assert!(all.contains(StyleFlags::STRIKETHROUGH));
1395        assert!(all.contains(StyleFlags::HIDDEN));
1396    }
1397
1398    #[test]
1399    fn style_flags_union_and_intersection() {
1400        let a = StyleFlags::BOLD | StyleFlags::ITALIC;
1401        let b = StyleFlags::ITALIC | StyleFlags::UNDERLINE;
1402        assert_eq!(
1403            a | b,
1404            StyleFlags::BOLD | StyleFlags::ITALIC | StyleFlags::UNDERLINE
1405        );
1406        assert_eq!(a & b, StyleFlags::ITALIC);
1407    }
1408
1409    #[test]
1410    fn style_flags_from_bits_truncate() {
1411        // 0xFF should give all flags
1412        let all = StyleFlags::from_bits_truncate(0xFF);
1413        assert_eq!(all, StyleFlags::all());
1414
1415        // 0x00 should give empty
1416        let none = StyleFlags::from_bits_truncate(0x00);
1417        assert!(none.is_empty());
1418    }
1419}
1420
1421/// Property tests for Cell types (bd-10i.13.2).
1422///
1423/// Top-level `#[cfg(test)]` scope: the `proptest!` macro has edition-2024
1424/// compatibility issues when nested inside another test module.
1425#[cfg(test)]
1426mod cell_proptests {
1427    use super::{Cell, CellAttrs, CellContent, GraphemeId, PackedRgba, StyleFlags};
1428    use proptest::prelude::*;
1429
1430    fn arb_packed_rgba() -> impl Strategy<Value = PackedRgba> {
1431        (any::<u8>(), any::<u8>(), any::<u8>(), any::<u8>())
1432            .prop_map(|(r, g, b, a)| PackedRgba::rgba(r, g, b, a))
1433    }
1434
1435    fn arb_grapheme_id() -> impl Strategy<Value = GraphemeId> {
1436        (0u32..=GraphemeId::MAX_SLOT, 0u8..=GraphemeId::MAX_WIDTH)
1437            .prop_map(|(slot, width)| GraphemeId::new(slot, width))
1438    }
1439
1440    fn arb_style_flags() -> impl Strategy<Value = StyleFlags> {
1441        any::<u8>().prop_map(StyleFlags::from_bits_truncate)
1442    }
1443
1444    proptest! {
1445        #[test]
1446        fn packed_rgba_roundtrips_all_components(tuple in (any::<u8>(), any::<u8>(), any::<u8>(), any::<u8>())) {
1447            let (r, g, b, a) = tuple;
1448            let c = PackedRgba::rgba(r, g, b, a);
1449            prop_assert_eq!(c.r(), r);
1450            prop_assert_eq!(c.g(), g);
1451            prop_assert_eq!(c.b(), b);
1452            prop_assert_eq!(c.a(), a);
1453        }
1454
1455        #[test]
1456        fn packed_rgba_rgb_always_opaque(tuple in (any::<u8>(), any::<u8>(), any::<u8>())) {
1457            let (r, g, b) = tuple;
1458            let c = PackedRgba::rgb(r, g, b);
1459            prop_assert_eq!(c.a(), 255);
1460            prop_assert_eq!(c.r(), r);
1461            prop_assert_eq!(c.g(), g);
1462            prop_assert_eq!(c.b(), b);
1463        }
1464
1465        #[test]
1466        fn packed_rgba_over_identity_transparent(dst in arb_packed_rgba()) {
1467            // Transparent source leaves destination unchanged
1468            let result = PackedRgba::TRANSPARENT.over(dst);
1469            prop_assert_eq!(result, dst);
1470        }
1471
1472        #[test]
1473        fn packed_rgba_over_identity_opaque(tuple in (any::<u8>(), any::<u8>(), any::<u8>(), arb_packed_rgba())) {
1474            // Fully opaque source replaces destination
1475            let (r, g, b, dst) = tuple;
1476            let src = PackedRgba::rgba(r, g, b, 255);
1477            let result = src.over(dst);
1478            prop_assert_eq!(result, src);
1479        }
1480
1481        #[test]
1482        fn grapheme_id_slot_width_roundtrip(tuple in (0u32..=GraphemeId::MAX_SLOT, 0u8..=GraphemeId::MAX_WIDTH)) {
1483            let (slot, width) = tuple;
1484            let id = GraphemeId::new(slot, width);
1485            prop_assert_eq!(id.slot(), slot as usize);
1486            prop_assert_eq!(id.width(), width as usize);
1487        }
1488
1489        #[test]
1490        fn grapheme_id_raw_roundtrip(id in arb_grapheme_id()) {
1491            let raw = id.raw();
1492            let restored = GraphemeId::from_raw(raw);
1493            prop_assert_eq!(restored.slot(), id.slot());
1494            prop_assert_eq!(restored.width(), id.width());
1495        }
1496
1497        #[test]
1498        fn cell_content_char_roundtrip(c in (0x20u32..0xD800u32).prop_union(0xE000u32..0x110000u32)) {
1499            if let Some(ch) = char::from_u32(c) {
1500                let content = CellContent::from_char(ch);
1501                prop_assert_eq!(content.as_char(), Some(ch));
1502                prop_assert!(!content.is_grapheme());
1503                prop_assert!(!content.is_empty());
1504                prop_assert!(!content.is_continuation());
1505            }
1506        }
1507
1508        #[test]
1509        fn cell_content_grapheme_roundtrip(id in arb_grapheme_id()) {
1510            let content = CellContent::from_grapheme(id);
1511            prop_assert!(content.is_grapheme());
1512            prop_assert_eq!(content.grapheme_id(), Some(id));
1513            prop_assert_eq!(content.width_hint(), id.width());
1514        }
1515
1516        #[test]
1517        fn cell_bits_eq_is_reflexive(
1518            tuple in (
1519                (0x20u32..0x80u32).prop_map(|c| char::from_u32(c).unwrap()),
1520                any::<u8>(), any::<u8>(), any::<u8>(),
1521                arb_style_flags(),
1522            ),
1523        ) {
1524            let (c, r, g, b, flags) = tuple;
1525            let cell = Cell::from_char(c)
1526                .with_fg(PackedRgba::rgb(r, g, b))
1527                .with_attrs(CellAttrs::new(flags, 0));
1528            prop_assert!(cell.bits_eq(&cell));
1529        }
1530
1531        #[test]
1532        fn cell_bits_eq_detects_fg_difference(
1533            tuple in (
1534                (0x41u32..0x5Bu32).prop_map(|c| char::from_u32(c).unwrap()),
1535                any::<u8>(), any::<u8>(),
1536            ),
1537        ) {
1538            let (c, r1, r2) = tuple;
1539            prop_assume!(r1 != r2);
1540            let cell1 = Cell::from_char(c).with_fg(PackedRgba::rgb(r1, 0, 0));
1541            let cell2 = Cell::from_char(c).with_fg(PackedRgba::rgb(r2, 0, 0));
1542            prop_assert!(!cell1.bits_eq(&cell2));
1543        }
1544
1545        #[test]
1546        fn cell_attrs_flags_roundtrip(tuple in (arb_style_flags(), 0u32..CellAttrs::LINK_ID_MAX)) {
1547            let (flags, link) = tuple;
1548            let attrs = CellAttrs::new(flags, link);
1549            prop_assert_eq!(attrs.flags(), flags);
1550            prop_assert_eq!(attrs.link_id(), link);
1551        }
1552
1553        #[test]
1554        fn cell_attrs_with_flags_preserves_link(tuple in (arb_style_flags(), 0u32..CellAttrs::LINK_ID_MAX, arb_style_flags())) {
1555            let (flags, link, new_flags) = tuple;
1556            let attrs = CellAttrs::new(flags, link);
1557            let updated = attrs.with_flags(new_flags);
1558            prop_assert_eq!(updated.flags(), new_flags);
1559            prop_assert_eq!(updated.link_id(), link);
1560        }
1561
1562        #[test]
1563        fn cell_attrs_with_link_preserves_flags(tuple in (arb_style_flags(), 0u32..CellAttrs::LINK_ID_MAX, 0u32..CellAttrs::LINK_ID_MAX)) {
1564            let (flags, link1, link2) = tuple;
1565            let attrs = CellAttrs::new(flags, link1);
1566            let updated = attrs.with_link(link2);
1567            prop_assert_eq!(updated.flags(), flags);
1568            prop_assert_eq!(updated.link_id(), link2);
1569        }
1570
1571        // --- Executable Invariant Tests (bd-10i.13.2) ---
1572
1573        #[test]
1574        fn cell_bits_eq_is_symmetric(
1575            tuple in (
1576                (0x41u32..0x5Bu32).prop_map(|c| char::from_u32(c).unwrap()),
1577                (0x41u32..0x5Bu32).prop_map(|c| char::from_u32(c).unwrap()),
1578                arb_packed_rgba(),
1579                arb_packed_rgba(),
1580            ),
1581        ) {
1582            let (c1, c2, fg1, fg2) = tuple;
1583            let cell_a = Cell::from_char(c1).with_fg(fg1);
1584            let cell_b = Cell::from_char(c2).with_fg(fg2);
1585            prop_assert_eq!(cell_a.bits_eq(&cell_b), cell_b.bits_eq(&cell_a),
1586                "bits_eq is not symmetric");
1587        }
1588
1589        #[test]
1590        fn cell_content_bit31_discriminates(id in arb_grapheme_id()) {
1591            // Char content: bit 31 is 0
1592            let char_content = CellContent::from_char('A');
1593            prop_assert!(!char_content.is_grapheme());
1594            prop_assert!(char_content.as_char().is_some());
1595            prop_assert!(char_content.grapheme_id().is_none());
1596
1597            // Grapheme content: bit 31 is 1
1598            let grapheme_content = CellContent::from_grapheme(id);
1599            prop_assert!(grapheme_content.is_grapheme());
1600            prop_assert!(grapheme_content.grapheme_id().is_some());
1601            prop_assert!(grapheme_content.as_char().is_none());
1602        }
1603
1604        #[test]
1605        fn cell_from_char_width_matches_unicode(
1606            c in (0x20u32..0x7Fu32).prop_map(|c| char::from_u32(c).unwrap()),
1607        ) {
1608            let cell = Cell::from_char(c);
1609            prop_assert_eq!(cell.width_hint(), 1,
1610                "Cell width hint for '{}' should be 1 for ASCII", c);
1611        }
1612    }
1613
1614    // Zero-parameter invariant tests (cannot be inside proptest! macro)
1615
1616    #[test]
1617    fn cell_content_continuation_has_zero_width() {
1618        let cont = CellContent::CONTINUATION;
1619        assert_eq!(cont.width(), 0, "CONTINUATION cell should have width 0");
1620        assert!(cont.is_continuation());
1621        assert!(!cont.is_grapheme());
1622    }
1623
1624    #[test]
1625    fn cell_content_empty_has_zero_width() {
1626        let empty = CellContent::EMPTY;
1627        assert_eq!(empty.width(), 0, "EMPTY cell should have width 0");
1628        assert!(empty.is_empty());
1629        assert!(!empty.is_grapheme());
1630        assert!(!empty.is_continuation());
1631    }
1632
1633    #[test]
1634    fn cell_default_is_empty() {
1635        let cell = Cell::default();
1636        assert!(cell.is_empty());
1637        assert_eq!(cell.width_hint(), 0);
1638    }
1639}