1#![forbid(unsafe_code)]
2
3use crate::char_width;
28
29#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
49#[repr(transparent)]
50pub struct GraphemeId(u32);
51
52impl GraphemeId {
53 pub const MAX_SLOT: u32 = 0x00FF_FFFF;
55
56 pub const MAX_WIDTH: u8 = 127;
58
59 #[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 #[inline]
73 pub const fn slot(self) -> usize {
74 (self.0 & Self::MAX_SLOT) as usize
75 }
76
77 #[inline]
79 pub const fn width(self) -> usize {
80 ((self.0 >> 24) & 0x7F) as usize
81 }
82
83 #[inline]
85 pub const fn raw(self) -> u32 {
86 self.0
87 }
88
89 #[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#[derive(Clone, Copy, PartialEq, Eq, Hash)]
124#[repr(transparent)]
125pub struct CellContent(u32);
126
127impl CellContent {
128 pub const EMPTY: Self = Self(0);
130
131 pub const CONTINUATION: Self = Self(0x7FFF_FFFF);
139
140 #[inline]
145 pub const fn from_char(c: char) -> Self {
146 Self(c as u32)
147 }
148
149 #[inline]
153 pub const fn from_grapheme(id: GraphemeId) -> Self {
154 Self(0x8000_0000 | id.raw())
155 }
156
157 #[inline]
159 pub const fn is_grapheme(self) -> bool {
160 self.0 & 0x8000_0000 != 0
161 }
162
163 #[inline]
165 pub const fn is_continuation(self) -> bool {
166 self.0 == Self::CONTINUATION.0
167 }
168
169 #[inline]
171 pub const fn is_empty(self) -> bool {
172 self.0 == Self::EMPTY.0
173 }
174
175 #[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 #[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 #[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 1
218 }
219 }
220
221 #[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 #[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#[derive(Clone, Copy, PartialEq, Eq)]
292#[repr(C, align(16))]
293pub struct Cell {
294 pub content: CellContent,
296 pub fg: PackedRgba,
298 pub bg: PackedRgba,
300 pub attrs: CellAttrs,
302}
303
304const _: () = assert!(core::mem::size_of::<Cell>() == 16);
306
307impl Cell {
308 pub const CONTINUATION: Self = Self {
313 content: CellContent::CONTINUATION,
314 fg: PackedRgba::TRANSPARENT,
315 bg: PackedRgba::TRANSPARENT,
316 attrs: CellAttrs::NONE,
317 };
318
319 #[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 #[inline]
332 pub const fn from_char(c: char) -> Self {
333 Self::new(CellContent::from_char(c))
334 }
335
336 #[inline]
338 pub const fn is_continuation(&self) -> bool {
339 self.content.is_continuation()
340 }
341
342 #[inline]
344 pub const fn is_empty(&self) -> bool {
345 self.content.is_empty()
346 }
347
348 #[inline]
352 pub const fn width_hint(&self) -> usize {
353 self.content.width_hint()
354 }
355
356 #[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 #[inline]
372 pub const fn with_char(mut self, c: char) -> Self {
373 self.content = CellContent::from_char(c);
374 self
375 }
376
377 #[inline]
379 pub const fn with_fg(mut self, fg: PackedRgba) -> Self {
380 self.fg = fg;
381 self
382 }
383
384 #[inline]
386 pub const fn with_bg(mut self, bg: PackedRgba) -> Self {
387 self.bg = bg;
388 self
389 }
390
391 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
430#[repr(transparent)]
431pub struct PackedRgba(pub u32);
432
433impl PackedRgba {
434 pub const TRANSPARENT: Self = Self(0);
436 pub const BLACK: Self = Self::rgb(0, 0, 0);
438 pub const WHITE: Self = Self::rgb(255, 255, 255);
440 pub const RED: Self = Self::rgb(255, 0, 0);
442 pub const GREEN: Self = Self::rgb(0, 255, 0);
444 pub const BLUE: Self = Self::rgb(0, 0, 255);
446
447 #[inline]
449 pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
450 Self::rgba(r, g, b, 255)
451 }
452
453 #[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 #[inline]
461 pub const fn r(self) -> u8 {
462 (self.0 >> 24) as u8
463 }
464
465 #[inline]
467 pub const fn g(self) -> u8 {
468 (self.0 >> 16) as u8
469 }
470
471 #[inline]
473 pub const fn b(self) -> u8 {
474 (self.0 >> 8) as u8
475 }
476
477 #[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 #[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 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 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 #[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 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
548 pub struct StyleFlags: u8 {
549 const BOLD = 0b0000_0001;
551 const DIM = 0b0000_0010;
553 const ITALIC = 0b0000_0100;
555 const UNDERLINE = 0b0000_1000;
557 const BLINK = 0b0001_0000;
559 const REVERSE = 0b0010_0000;
561 const STRIKETHROUGH = 0b0100_0000;
563 const HIDDEN = 0b1000_0000;
565 }
566}
567
568#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
572#[repr(transparent)]
573pub struct CellAttrs(u32);
574
575impl CellAttrs {
576 pub const NONE: Self = Self(0);
578
579 pub const LINK_ID_NONE: u32 = 0;
581 pub const LINK_ID_MAX: u32 = 0x00FF_FFFE;
583
584 #[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 #[inline]
597 pub fn flags(self) -> StyleFlags {
598 StyleFlags::from_bits_truncate((self.0 >> 24) as u8)
599 }
600
601 #[inline]
603 pub fn link_id(self) -> u32 {
604 self.0 & 0x00FF_FFFF
605 }
606
607 #[inline]
609 pub fn with_flags(self, flags: StyleFlags) -> Self {
610 Self((self.0 & 0x00FF_FFFF) | ((flags.bits() as u32) << 24))
611 }
612
613 #[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 #[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 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 #[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 #[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 let c = CellContent::from_char('日');
858 assert_eq!(c.as_char(), Some('日'));
859 assert!(!c.is_grapheme());
860
861 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 let bolt = CellContent::from_char('⚡');
896 assert_eq!(bolt.width(), 2, "bolt is Wide, always width 2");
897
898 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 let char_content = CellContent::from_char('X');
930 assert_eq!(char_content.raw() & 0x8000_0000, 0);
931
932 let grapheme_content = CellContent::from_grapheme(GraphemeId::new(1, 1));
934 assert_ne!(grapheme_content.raw() & 0x8000_0000, 0);
935 }
936
937 #[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 #[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 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 let src = PackedRgba::rgba(200, 100, 50, 128);
1055 let result = src.over(PackedRgba::TRANSPARENT);
1056 assert_eq!(result.a(), 128);
1058 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 let src = PackedRgba::rgba(255, 0, 0, 1);
1068 let dst = PackedRgba::rgba(0, 0, 255, 255);
1069 let result = src.over(dst);
1070 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); 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); 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); }
1103
1104 #[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 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 let wide = CellContent::from_char('日');
1149 assert_eq!(wide.width_hint(), 1); assert_eq!(wide.width(), 2); }
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 #[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 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 #[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 #[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 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 #[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 let all = StyleFlags::from_bits_truncate(0xFF);
1413 assert_eq!(all, StyleFlags::all());
1414
1415 let none = StyleFlags::from_bits_truncate(0x00);
1417 assert!(none.is_empty());
1418 }
1419}
1420
1421#[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 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 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 #[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 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 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 #[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}