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 const fn is_default(self) -> bool {
180 self.0 == Self::EMPTY.0
181 }
182
183 #[inline]
187 pub fn as_char(self) -> Option<char> {
188 if self.is_grapheme() || self.0 == Self::EMPTY.0 || self.0 == Self::CONTINUATION.0 {
189 None
190 } else {
191 char::from_u32(self.0)
192 }
193 }
194
195 #[inline]
199 pub const fn grapheme_id(self) -> Option<GraphemeId> {
200 if self.is_grapheme() {
201 Some(GraphemeId::from_raw(self.0 & !0x8000_0000))
202 } else {
203 None
204 }
205 }
206
207 #[inline]
217 pub const fn width_hint(self) -> usize {
218 if self.is_empty() || self.is_continuation() {
219 0
220 } else if self.is_grapheme() {
221 ((self.0 >> 24) & 0x7F) as usize
222 } else {
223 1
226 }
227 }
228
229 #[inline]
233 pub fn width(self) -> usize {
234 if self.is_empty() || self.is_continuation() {
235 0
236 } else if self.is_grapheme() {
237 ((self.0 >> 24) & 0x7F) as usize
238 } else {
239 let Some(c) = self.as_char() else {
240 return 1;
241 };
242 char_width(c)
243 }
244 }
245
246 #[inline]
248 pub const fn raw(self) -> u32 {
249 self.0
250 }
251}
252
253impl Default for CellContent {
254 fn default() -> Self {
255 Self::EMPTY
256 }
257}
258
259impl core::fmt::Debug for CellContent {
260 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
261 if self.is_empty() {
262 write!(f, "CellContent::EMPTY")
263 } else if self.is_continuation() {
264 write!(f, "CellContent::CONTINUATION")
265 } else if let Some(c) = self.as_char() {
266 write!(f, "CellContent::Char({c:?})")
267 } else if let Some(id) = self.grapheme_id() {
268 write!(f, "CellContent::Grapheme({id:?})")
269 } else {
270 write!(f, "CellContent(0x{:08x})", self.0)
271 }
272 }
273}
274
275#[derive(Clone, Copy, PartialEq, Eq)]
300#[repr(C, align(16))]
301pub struct Cell {
302 pub content: CellContent,
304 pub fg: PackedRgba,
306 pub bg: PackedRgba,
308 pub attrs: CellAttrs,
310}
311
312const _: () = assert!(core::mem::size_of::<Cell>() == 16);
314
315impl Cell {
316 pub const CONTINUATION: Self = Self {
321 content: CellContent::CONTINUATION,
322 fg: PackedRgba::TRANSPARENT,
323 bg: PackedRgba::TRANSPARENT,
324 attrs: CellAttrs::NONE,
325 };
326
327 #[inline]
329 pub const fn new(content: CellContent) -> Self {
330 Self {
331 content,
332 fg: PackedRgba::WHITE,
333 bg: PackedRgba::TRANSPARENT,
334 attrs: CellAttrs::NONE,
335 }
336 }
337
338 #[inline]
340 pub const fn from_char(c: char) -> Self {
341 Self::new(CellContent::from_char(c))
342 }
343
344 #[inline]
346 pub const fn is_continuation(&self) -> bool {
347 self.content.is_continuation()
348 }
349
350 #[inline]
352 pub const fn is_empty(&self) -> bool {
353 self.content.is_empty()
354 }
355
356 #[inline]
360 pub const fn width_hint(&self) -> usize {
361 self.content.width_hint()
362 }
363
364 #[inline(always)]
371 pub fn bits_eq(&self, other: &Self) -> bool {
372 (self.content.raw() == other.content.raw())
373 & (self.fg == other.fg)
374 & (self.bg == other.bg)
375 & (self.attrs == other.attrs)
376 }
377
378 #[inline]
380 #[must_use]
381 pub const fn with_char(mut self, c: char) -> Self {
382 self.content = CellContent::from_char(c);
383 self
384 }
385
386 #[inline]
388 #[must_use]
389 pub const fn with_fg(mut self, fg: PackedRgba) -> Self {
390 self.fg = fg;
391 self
392 }
393
394 #[inline]
396 #[must_use]
397 pub const fn with_bg(mut self, bg: PackedRgba) -> Self {
398 self.bg = bg;
399 self
400 }
401
402 #[inline]
404 #[must_use]
405 pub const fn with_attrs(mut self, attrs: CellAttrs) -> Self {
406 self.attrs = attrs;
407 self
408 }
409}
410impl Default for Cell {
411 fn default() -> Self {
412 Self {
413 content: CellContent::EMPTY,
414 fg: PackedRgba::WHITE,
415 bg: PackedRgba::TRANSPARENT,
416 attrs: CellAttrs::NONE,
417 }
418 }
419}
420
421impl core::fmt::Debug for Cell {
422 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
423 f.debug_struct("Cell")
424 .field("content", &self.content)
425 .field("fg", &self.fg)
426 .field("bg", &self.bg)
427 .field("attrs", &self.attrs)
428 .finish()
429 }
430}
431
432#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
442#[repr(transparent)]
443pub struct PackedRgba(pub u32);
444
445impl PackedRgba {
446 pub const TRANSPARENT: Self = Self(0);
448 pub const BLACK: Self = Self::rgb(0, 0, 0);
450 pub const WHITE: Self = Self::rgb(255, 255, 255);
452 pub const RED: Self = Self::rgb(255, 0, 0);
454 pub const GREEN: Self = Self::rgb(0, 255, 0);
456 pub const BLUE: Self = Self::rgb(0, 0, 255);
458
459 #[inline]
461 pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
462 Self::rgba(r, g, b, 255)
463 }
464
465 #[inline]
467 pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
468 Self(((r as u32) << 24) | ((g as u32) << 16) | ((b as u32) << 8) | (a as u32))
469 }
470
471 #[inline]
473 pub const fn r(self) -> u8 {
474 (self.0 >> 24) as u8
475 }
476
477 #[inline]
479 pub const fn g(self) -> u8 {
480 (self.0 >> 16) as u8
481 }
482
483 #[inline]
485 pub const fn b(self) -> u8 {
486 (self.0 >> 8) as u8
487 }
488
489 #[inline]
491 pub const fn a(self) -> u8 {
492 self.0 as u8
493 }
494
495 #[inline]
496 const fn div_round_u8(numer: u64, denom: u64) -> u8 {
497 debug_assert!(denom != 0);
498 let v = (numer + (denom / 2)) / denom;
499 if v > 255 { 255 } else { v as u8 }
500 }
501
502 #[inline]
507 #[must_use]
508 pub fn over(self, dst: Self) -> Self {
509 let s_a = self.a() as u64;
510 if s_a == 255 {
511 return self;
512 }
513 if s_a == 0 {
514 return dst;
515 }
516
517 let d_a = dst.a() as u64;
518 let inv_s_a = 255 - s_a;
519
520 let numer_a = 255 * s_a + d_a * inv_s_a;
525 if numer_a == 0 {
526 return Self::TRANSPARENT;
527 }
528
529 let out_a = Self::div_round_u8(numer_a, 255);
530
531 let r = Self::div_round_u8(
534 (self.r() as u64) * s_a * 255 + (dst.r() as u64) * d_a * inv_s_a,
535 numer_a,
536 );
537 let g = Self::div_round_u8(
538 (self.g() as u64) * s_a * 255 + (dst.g() as u64) * d_a * inv_s_a,
539 numer_a,
540 );
541 let b = Self::div_round_u8(
542 (self.b() as u64) * s_a * 255 + (dst.b() as u64) * d_a * inv_s_a,
543 numer_a,
544 );
545
546 Self::rgba(r, g, b, out_a)
547 }
548
549 #[inline]
551 #[must_use]
552 pub fn with_opacity(self, opacity: f32) -> Self {
553 let opacity = opacity.clamp(0.0, 1.0);
554 let a = ((self.a() as f32) * opacity).round().clamp(0.0, 255.0) as u8;
555 Self::rgba(self.r(), self.g(), self.b(), a)
556 }
557}
558
559bitflags::bitflags! {
560 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
562 pub struct StyleFlags: u8 {
563 const BOLD = 0b0000_0001;
565 const DIM = 0b0000_0010;
567 const ITALIC = 0b0000_0100;
569 const UNDERLINE = 0b0000_1000;
571 const BLINK = 0b0001_0000;
573 const REVERSE = 0b0010_0000;
575 const STRIKETHROUGH = 0b0100_0000;
577 const HIDDEN = 0b1000_0000;
579 }
580}
581
582#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
586#[repr(transparent)]
587pub struct CellAttrs(u32);
588
589impl CellAttrs {
590 pub const NONE: Self = Self(0);
592
593 pub const LINK_ID_NONE: u32 = 0;
595 pub const LINK_ID_MAX: u32 = 0x00FF_FFFE;
597
598 #[inline]
600 pub fn new(flags: StyleFlags, link_id: u32) -> Self {
601 debug_assert!(
602 link_id <= Self::LINK_ID_MAX,
603 "link_id overflow: {link_id} (max={})",
604 Self::LINK_ID_MAX
605 );
606 Self(((flags.bits() as u32) << 24) | (link_id & 0x00FF_FFFF))
607 }
608
609 #[inline]
611 pub fn flags(self) -> StyleFlags {
612 StyleFlags::from_bits_truncate((self.0 >> 24) as u8)
613 }
614
615 #[inline]
617 pub fn link_id(self) -> u32 {
618 self.0 & 0x00FF_FFFF
619 }
620
621 #[inline]
623 #[must_use]
624 pub fn with_flags(self, flags: StyleFlags) -> Self {
625 Self((self.0 & 0x00FF_FFFF) | ((flags.bits() as u32) << 24))
626 }
627
628 #[inline]
630 #[must_use]
631 pub fn with_link(self, link_id: u32) -> Self {
632 debug_assert!(
633 link_id <= Self::LINK_ID_MAX,
634 "link_id overflow: {link_id} (max={})",
635 Self::LINK_ID_MAX
636 );
637 Self((self.0 & 0xFF00_0000) | (link_id & 0x00FF_FFFF))
638 }
639
640 #[inline]
642 pub fn has_flag(self, flag: StyleFlags) -> bool {
643 self.flags().contains(flag)
644 }
645}
646
647#[cfg(test)]
648mod tests {
649 use super::{Cell, CellAttrs, CellContent, GraphemeId, PackedRgba, StyleFlags};
650
651 fn reference_over(src: PackedRgba, dst: PackedRgba) -> PackedRgba {
652 let sr = src.r() as f64 / 255.0;
653 let sg = src.g() as f64 / 255.0;
654 let sb = src.b() as f64 / 255.0;
655 let sa = src.a() as f64 / 255.0;
656
657 let dr = dst.r() as f64 / 255.0;
658 let dg = dst.g() as f64 / 255.0;
659 let db = dst.b() as f64 / 255.0;
660 let da = dst.a() as f64 / 255.0;
661
662 let out_a = sa + da * (1.0 - sa);
663 if out_a <= 0.0 {
664 return PackedRgba::TRANSPARENT;
665 }
666
667 let out_r = (sr * sa + dr * da * (1.0 - sa)) / out_a;
668 let out_g = (sg * sa + dg * da * (1.0 - sa)) / out_a;
669 let out_b = (sb * sa + db * da * (1.0 - sa)) / out_a;
670
671 let to_u8 = |x: f64| -> u8 { (x * 255.0).round().clamp(0.0, 255.0) as u8 };
672 PackedRgba::rgba(to_u8(out_r), to_u8(out_g), to_u8(out_b), to_u8(out_a))
673 }
674
675 #[test]
676 fn packed_rgba_is_4_bytes() {
677 assert_eq!(core::mem::size_of::<PackedRgba>(), 4);
678 }
679
680 #[test]
681 fn rgb_sets_alpha_to_255() {
682 let c = PackedRgba::rgb(1, 2, 3);
683 assert_eq!(c.r(), 1);
684 assert_eq!(c.g(), 2);
685 assert_eq!(c.b(), 3);
686 assert_eq!(c.a(), 255);
687 }
688
689 #[test]
690 fn rgba_round_trips_components() {
691 let c = PackedRgba::rgba(10, 20, 30, 40);
692 assert_eq!(c.r(), 10);
693 assert_eq!(c.g(), 20);
694 assert_eq!(c.b(), 30);
695 assert_eq!(c.a(), 40);
696 }
697
698 #[test]
699 fn over_with_opaque_src_returns_src() {
700 let src = PackedRgba::rgba(1, 2, 3, 255);
701 let dst = PackedRgba::rgba(9, 8, 7, 200);
702 assert_eq!(src.over(dst), src);
703 }
704
705 #[test]
706 fn over_with_transparent_src_returns_dst() {
707 let src = PackedRgba::TRANSPARENT;
708 let dst = PackedRgba::rgba(9, 8, 7, 200);
709 assert_eq!(src.over(dst), dst);
710 }
711
712 #[test]
713 fn over_blends_correctly_for_half_alpha_over_opaque() {
714 let src = PackedRgba::rgba(255, 0, 0, 128);
716 let dst = PackedRgba::rgba(0, 0, 255, 255);
717 assert_eq!(src.over(dst), PackedRgba::rgba(128, 0, 127, 255));
718 }
719
720 #[test]
721 fn over_matches_reference_for_partial_alpha_cases() {
722 let cases = [
723 (
724 PackedRgba::rgba(200, 10, 10, 64),
725 PackedRgba::rgba(10, 200, 10, 128),
726 ),
727 (
728 PackedRgba::rgba(1, 2, 3, 1),
729 PackedRgba::rgba(250, 251, 252, 254),
730 ),
731 (
732 PackedRgba::rgba(100, 0, 200, 200),
733 PackedRgba::rgba(0, 120, 30, 50),
734 ),
735 ];
736
737 for (src, dst) in cases {
738 assert_eq!(src.over(dst), reference_over(src, dst));
739 }
740 }
741
742 #[test]
743 fn with_opacity_scales_alpha() {
744 let c = PackedRgba::rgba(10, 20, 30, 255);
745 assert_eq!(c.with_opacity(0.5).a(), 128);
746 assert_eq!(c.with_opacity(-1.0).a(), 0);
747 assert_eq!(c.with_opacity(2.0).a(), 255);
748 }
749
750 #[test]
751 fn cell_attrs_is_4_bytes() {
752 assert_eq!(core::mem::size_of::<CellAttrs>(), 4);
753 }
754
755 #[test]
756 fn cell_attrs_none_has_no_flags_and_no_link() {
757 assert!(CellAttrs::NONE.flags().is_empty());
758 assert_eq!(CellAttrs::NONE.link_id(), 0);
759 }
760
761 #[test]
762 fn cell_attrs_new_stores_flags_and_link() {
763 let flags = StyleFlags::BOLD | StyleFlags::ITALIC;
764 let a = CellAttrs::new(flags, 42);
765 assert_eq!(a.flags(), flags);
766 assert_eq!(a.link_id(), 42);
767 }
768
769 #[test]
770 fn cell_attrs_with_flags_preserves_link_id() {
771 let a = CellAttrs::new(StyleFlags::BOLD, 123);
772 let b = a.with_flags(StyleFlags::UNDERLINE);
773 assert_eq!(b.flags(), StyleFlags::UNDERLINE);
774 assert_eq!(b.link_id(), 123);
775 }
776
777 #[test]
778 fn cell_attrs_with_link_preserves_flags() {
779 let a = CellAttrs::new(StyleFlags::BOLD | StyleFlags::ITALIC, 1);
780 let b = a.with_link(999);
781 assert_eq!(b.flags(), StyleFlags::BOLD | StyleFlags::ITALIC);
782 assert_eq!(b.link_id(), 999);
783 }
784
785 #[test]
786 fn cell_attrs_flag_combinations_work() {
787 let flags = StyleFlags::BOLD | StyleFlags::ITALIC;
788 let a = CellAttrs::new(flags, 0);
789 assert!(a.has_flag(StyleFlags::BOLD));
790 assert!(a.has_flag(StyleFlags::ITALIC));
791 assert!(!a.has_flag(StyleFlags::UNDERLINE));
792 }
793
794 #[test]
795 fn cell_attrs_link_id_max_boundary() {
796 let a = CellAttrs::new(StyleFlags::empty(), CellAttrs::LINK_ID_MAX);
797 assert_eq!(a.link_id(), CellAttrs::LINK_ID_MAX);
798 }
799
800 #[test]
803 fn grapheme_id_is_4_bytes() {
804 assert_eq!(core::mem::size_of::<GraphemeId>(), 4);
805 }
806
807 #[test]
808 fn grapheme_id_encoding_roundtrip() {
809 let id = GraphemeId::new(12345, 2);
810 assert_eq!(id.slot(), 12345);
811 assert_eq!(id.width(), 2);
812 }
813
814 #[test]
815 fn grapheme_id_max_values() {
816 let id = GraphemeId::new(GraphemeId::MAX_SLOT, GraphemeId::MAX_WIDTH);
817 assert_eq!(id.slot(), 0x00FF_FFFF);
818 assert_eq!(id.width(), 127);
819 }
820
821 #[test]
822 fn grapheme_id_zero_values() {
823 let id = GraphemeId::new(0, 0);
824 assert_eq!(id.slot(), 0);
825 assert_eq!(id.width(), 0);
826 }
827
828 #[test]
829 fn grapheme_id_raw_roundtrip() {
830 let id = GraphemeId::new(999, 5);
831 let raw = id.raw();
832 let restored = GraphemeId::from_raw(raw);
833 assert_eq!(restored.slot(), 999);
834 assert_eq!(restored.width(), 5);
835 }
836
837 #[test]
840 fn cell_content_is_4_bytes() {
841 assert_eq!(core::mem::size_of::<CellContent>(), 4);
842 }
843
844 #[test]
845 fn cell_content_empty_properties() {
846 assert!(CellContent::EMPTY.is_empty());
847 assert!(!CellContent::EMPTY.is_continuation());
848 assert!(!CellContent::EMPTY.is_grapheme());
849 assert_eq!(CellContent::EMPTY.width_hint(), 0);
850 }
851
852 #[test]
853 fn cell_content_continuation_properties() {
854 assert!(CellContent::CONTINUATION.is_continuation());
855 assert!(!CellContent::CONTINUATION.is_empty());
856 assert!(!CellContent::CONTINUATION.is_grapheme());
857 assert_eq!(CellContent::CONTINUATION.width_hint(), 0);
858 }
859
860 #[test]
861 fn cell_content_from_char_ascii() {
862 let c = CellContent::from_char('A');
863 assert!(!c.is_grapheme());
864 assert!(!c.is_empty());
865 assert!(!c.is_continuation());
866 assert_eq!(c.as_char(), Some('A'));
867 assert_eq!(c.width_hint(), 1);
868 }
869
870 #[test]
871 fn cell_content_from_char_unicode() {
872 let c = CellContent::from_char('日');
874 assert_eq!(c.as_char(), Some('日'));
875 assert!(!c.is_grapheme());
876
877 let c2 = CellContent::from_char('🎉');
879 assert_eq!(c2.as_char(), Some('🎉'));
880 assert!(!c2.is_grapheme());
881 }
882
883 #[test]
884 fn cell_content_from_grapheme() {
885 let id = GraphemeId::new(42, 2);
886 let c = CellContent::from_grapheme(id);
887
888 assert!(c.is_grapheme());
889 assert!(!c.is_empty());
890 assert!(!c.is_continuation());
891 assert_eq!(c.grapheme_id(), Some(id));
892 assert_eq!(c.as_char(), None);
893 assert_eq!(c.width_hint(), 2);
894 }
895
896 #[test]
897 fn cell_content_width_for_chars() {
898 let ascii = CellContent::from_char('A');
899 assert_eq!(ascii.width(), 1);
900
901 let wide = CellContent::from_char('日');
902 assert_eq!(wide.width(), 2);
903
904 let emoji = CellContent::from_char('🎉');
905 assert_eq!(emoji.width(), 2);
906
907 let bolt = CellContent::from_char('⚡');
912 assert_eq!(bolt.width(), 2, "bolt is Wide, always width 2");
913
914 let gear = CellContent::from_char('⚙');
916 let heart = CellContent::from_char('❤');
917 assert!(
918 [1, 2].contains(&gear.width()),
919 "gear should be 1 (non-CJK) or 2 (CJK), got {}",
920 gear.width()
921 );
922 assert_eq!(
923 gear.width(),
924 heart.width(),
925 "gear and heart should have same width (both Neutral)"
926 );
927 }
928
929 #[test]
930 fn cell_content_width_for_grapheme() {
931 let id = GraphemeId::new(7, 3);
932 let c = CellContent::from_grapheme(id);
933 assert_eq!(c.width(), 3);
934 }
935
936 #[test]
937 fn cell_content_width_empty_is_zero() {
938 assert_eq!(CellContent::EMPTY.width(), 0);
939 assert_eq!(CellContent::CONTINUATION.width(), 0);
940 }
941
942 #[test]
943 fn cell_content_grapheme_discriminator_bit() {
944 let char_content = CellContent::from_char('X');
946 assert_eq!(char_content.raw() & 0x8000_0000, 0);
947
948 let grapheme_content = CellContent::from_grapheme(GraphemeId::new(1, 1));
950 assert_ne!(grapheme_content.raw() & 0x8000_0000, 0);
951 }
952
953 #[test]
956 fn cell_is_16_bytes() {
957 assert_eq!(core::mem::size_of::<Cell>(), 16);
958 }
959
960 #[test]
961 fn cell_alignment_is_16() {
962 assert_eq!(core::mem::align_of::<Cell>(), 16);
963 }
964
965 #[test]
966 fn cell_default_properties() {
967 let cell = Cell::default();
968 assert!(cell.is_empty());
969 assert!(!cell.is_continuation());
970 assert_eq!(cell.fg, PackedRgba::WHITE);
971 assert_eq!(cell.bg, PackedRgba::TRANSPARENT);
972 assert_eq!(cell.attrs, CellAttrs::NONE);
973 }
974
975 #[test]
976 fn cell_continuation_constant() {
977 assert!(Cell::CONTINUATION.is_continuation());
978 assert!(!Cell::CONTINUATION.is_empty());
979 }
980
981 #[test]
982 fn cell_from_char() {
983 let cell = Cell::from_char('X');
984 assert_eq!(cell.content.as_char(), Some('X'));
985 assert_eq!(cell.fg, PackedRgba::WHITE);
986 assert_eq!(cell.bg, PackedRgba::TRANSPARENT);
987 }
988
989 #[test]
990 fn cell_builder_methods() {
991 let cell = Cell::from_char('A')
992 .with_fg(PackedRgba::rgb(255, 0, 0))
993 .with_bg(PackedRgba::rgb(0, 0, 255))
994 .with_attrs(CellAttrs::new(StyleFlags::BOLD, 0));
995
996 assert_eq!(cell.content.as_char(), Some('A'));
997 assert_eq!(cell.fg, PackedRgba::rgb(255, 0, 0));
998 assert_eq!(cell.bg, PackedRgba::rgb(0, 0, 255));
999 assert!(cell.attrs.has_flag(StyleFlags::BOLD));
1000 }
1001
1002 #[test]
1003 fn cell_bits_eq_same_cells() {
1004 let cell1 = Cell::from_char('X').with_fg(PackedRgba::rgb(1, 2, 3));
1005 let cell2 = Cell::from_char('X').with_fg(PackedRgba::rgb(1, 2, 3));
1006 assert!(cell1.bits_eq(&cell2));
1007 }
1008
1009 #[test]
1010 fn cell_bits_eq_different_cells() {
1011 let cell1 = Cell::from_char('X');
1012 let cell2 = Cell::from_char('Y');
1013 assert!(!cell1.bits_eq(&cell2));
1014
1015 let cell3 = Cell::from_char('X').with_fg(PackedRgba::rgb(1, 2, 3));
1016 assert!(!cell1.bits_eq(&cell3));
1017 }
1018
1019 #[test]
1020 fn cell_width_hint() {
1021 let empty = Cell::default();
1022 assert_eq!(empty.width_hint(), 0);
1023
1024 let cont = Cell::CONTINUATION;
1025 assert_eq!(cont.width_hint(), 0);
1026
1027 let ascii = Cell::from_char('A');
1028 assert_eq!(ascii.width_hint(), 1);
1029 }
1030
1031 #[test]
1036 fn packed_rgba_named_constants() {
1037 assert_eq!(PackedRgba::TRANSPARENT, PackedRgba(0));
1038 assert_eq!(PackedRgba::TRANSPARENT.a(), 0);
1039
1040 assert_eq!(PackedRgba::BLACK.r(), 0);
1041 assert_eq!(PackedRgba::BLACK.g(), 0);
1042 assert_eq!(PackedRgba::BLACK.b(), 0);
1043 assert_eq!(PackedRgba::BLACK.a(), 255);
1044
1045 assert_eq!(PackedRgba::WHITE.r(), 255);
1046 assert_eq!(PackedRgba::WHITE.g(), 255);
1047 assert_eq!(PackedRgba::WHITE.b(), 255);
1048 assert_eq!(PackedRgba::WHITE.a(), 255);
1049
1050 assert_eq!(PackedRgba::RED, PackedRgba::rgb(255, 0, 0));
1051 assert_eq!(PackedRgba::GREEN, PackedRgba::rgb(0, 255, 0));
1052 assert_eq!(PackedRgba::BLUE, PackedRgba::rgb(0, 0, 255));
1053 }
1054
1055 #[test]
1056 fn packed_rgba_default_is_transparent() {
1057 assert_eq!(PackedRgba::default(), PackedRgba::TRANSPARENT);
1058 }
1059
1060 #[test]
1061 fn over_both_transparent_returns_transparent() {
1062 let result = PackedRgba::TRANSPARENT.over(PackedRgba::TRANSPARENT);
1064 assert_eq!(result, PackedRgba::TRANSPARENT);
1065 }
1066
1067 #[test]
1068 fn over_partial_alpha_over_transparent_dst() {
1069 let src = PackedRgba::rgba(200, 100, 50, 128);
1071 let result = src.over(PackedRgba::TRANSPARENT);
1072 assert_eq!(result.a(), 128);
1074 assert_eq!(result.r(), 200);
1076 assert_eq!(result.g(), 100);
1077 assert_eq!(result.b(), 50);
1078 }
1079
1080 #[test]
1081 fn over_very_low_alpha() {
1082 let src = PackedRgba::rgba(255, 0, 0, 1);
1084 let dst = PackedRgba::rgba(0, 0, 255, 255);
1085 let result = src.over(dst);
1086 assert_eq!(result.a(), 255);
1088 assert!(result.b() > 250, "b={} should be near 255", result.b());
1089 assert!(result.r() < 5, "r={} should be near 0", result.r());
1090 }
1091
1092 #[test]
1093 fn with_opacity_exact_zero() {
1094 let c = PackedRgba::rgba(10, 20, 30, 200);
1095 let result = c.with_opacity(0.0);
1096 assert_eq!(result.a(), 0);
1097 assert_eq!(result.r(), 10); assert_eq!(result.g(), 20);
1099 assert_eq!(result.b(), 30);
1100 }
1101
1102 #[test]
1103 fn with_opacity_exact_one() {
1104 let c = PackedRgba::rgba(10, 20, 30, 200);
1105 let result = c.with_opacity(1.0);
1106 assert_eq!(result.a(), 200); assert_eq!(result.r(), 10);
1108 }
1109
1110 #[test]
1111 fn with_opacity_preserves_rgb() {
1112 let c = PackedRgba::rgba(42, 84, 168, 255);
1113 let result = c.with_opacity(0.25);
1114 assert_eq!(result.r(), 42);
1115 assert_eq!(result.g(), 84);
1116 assert_eq!(result.b(), 168);
1117 assert_eq!(result.a(), 64); }
1119
1120 #[test]
1123 fn cell_content_as_char_none_for_empty() {
1124 assert_eq!(CellContent::EMPTY.as_char(), None);
1125 }
1126
1127 #[test]
1128 fn cell_content_as_char_none_for_continuation() {
1129 assert_eq!(CellContent::CONTINUATION.as_char(), None);
1130 }
1131
1132 #[test]
1133 fn cell_content_as_char_none_for_grapheme() {
1134 let id = GraphemeId::new(1, 2);
1135 let c = CellContent::from_grapheme(id);
1136 assert_eq!(c.as_char(), None);
1137 }
1138
1139 #[test]
1140 fn cell_content_grapheme_id_none_for_char() {
1141 let c = CellContent::from_char('A');
1142 assert_eq!(c.grapheme_id(), None);
1143 }
1144
1145 #[test]
1146 fn cell_content_grapheme_id_none_for_empty() {
1147 assert_eq!(CellContent::EMPTY.grapheme_id(), None);
1148 }
1149
1150 #[test]
1151 fn cell_content_width_control_chars() {
1152 let tab = CellContent::from_char('\t');
1155 assert_eq!(tab.width(), 1);
1156
1157 let bel = CellContent::from_char('\x07');
1158 assert_eq!(bel.width(), 0);
1159 }
1160
1161 #[test]
1162 fn cell_content_width_hint_always_1_for_chars() {
1163 let wide = CellContent::from_char('日');
1165 assert_eq!(wide.width_hint(), 1); assert_eq!(wide.width(), 2); }
1168
1169 #[test]
1170 fn cell_content_default_is_empty() {
1171 assert_eq!(CellContent::default(), CellContent::EMPTY);
1172 }
1173
1174 #[test]
1175 fn cell_content_debug_empty() {
1176 let s = format!("{:?}", CellContent::EMPTY);
1177 assert_eq!(s, "CellContent::EMPTY");
1178 }
1179
1180 #[test]
1181 fn cell_content_debug_continuation() {
1182 let s = format!("{:?}", CellContent::CONTINUATION);
1183 assert_eq!(s, "CellContent::CONTINUATION");
1184 }
1185
1186 #[test]
1187 fn cell_content_debug_char() {
1188 let s = format!("{:?}", CellContent::from_char('X'));
1189 assert!(s.starts_with("CellContent::Char("), "got: {s}");
1190 }
1191
1192 #[test]
1193 fn cell_content_debug_grapheme() {
1194 let id = GraphemeId::new(1, 2);
1195 let s = format!("{:?}", CellContent::from_grapheme(id));
1196 assert!(s.starts_with("CellContent::Grapheme("), "got: {s}");
1197 }
1198
1199 #[test]
1200 fn cell_content_raw_value() {
1201 let c = CellContent::from_char('A');
1202 assert_eq!(c.raw(), 'A' as u32);
1203
1204 let g = CellContent::from_grapheme(GraphemeId::new(5, 2));
1205 assert_ne!(g.raw() & 0x8000_0000, 0);
1206 }
1207
1208 #[test]
1211 fn cell_attrs_default_is_none() {
1212 assert_eq!(CellAttrs::default(), CellAttrs::NONE);
1213 }
1214
1215 #[test]
1216 fn cell_attrs_each_flag_isolated() {
1217 let all_flags = [
1218 StyleFlags::BOLD,
1219 StyleFlags::DIM,
1220 StyleFlags::ITALIC,
1221 StyleFlags::UNDERLINE,
1222 StyleFlags::BLINK,
1223 StyleFlags::REVERSE,
1224 StyleFlags::STRIKETHROUGH,
1225 StyleFlags::HIDDEN,
1226 ];
1227
1228 for &flag in &all_flags {
1229 let a = CellAttrs::new(flag, 0);
1230 assert!(a.has_flag(flag), "flag {:?} should be set", flag);
1231
1232 for &other in &all_flags {
1234 if other != flag {
1235 assert!(
1236 !a.has_flag(other),
1237 "flag {:?} should NOT be set when only {:?} is",
1238 other,
1239 flag
1240 );
1241 }
1242 }
1243 }
1244 }
1245
1246 #[test]
1247 fn cell_attrs_all_flags_combined() {
1248 let all = StyleFlags::BOLD
1249 | StyleFlags::DIM
1250 | StyleFlags::ITALIC
1251 | StyleFlags::UNDERLINE
1252 | StyleFlags::BLINK
1253 | StyleFlags::REVERSE
1254 | StyleFlags::STRIKETHROUGH
1255 | StyleFlags::HIDDEN;
1256 let a = CellAttrs::new(all, 42);
1257 assert_eq!(a.flags(), all);
1258 assert!(a.has_flag(StyleFlags::BOLD));
1259 assert!(a.has_flag(StyleFlags::HIDDEN));
1260 assert_eq!(a.link_id(), 42);
1261 }
1262
1263 #[test]
1264 fn cell_attrs_link_id_zero() {
1265 let a = CellAttrs::new(StyleFlags::BOLD, CellAttrs::LINK_ID_NONE);
1266 assert_eq!(a.link_id(), 0);
1267 assert!(a.has_flag(StyleFlags::BOLD));
1268 }
1269
1270 #[test]
1271 fn cell_attrs_with_link_to_none() {
1272 let a = CellAttrs::new(StyleFlags::ITALIC, 500);
1273 let b = a.with_link(CellAttrs::LINK_ID_NONE);
1274 assert_eq!(b.link_id(), 0);
1275 assert!(b.has_flag(StyleFlags::ITALIC));
1276 }
1277
1278 #[test]
1279 fn cell_attrs_with_flags_to_empty() {
1280 let a = CellAttrs::new(StyleFlags::BOLD | StyleFlags::ITALIC, 123);
1281 let b = a.with_flags(StyleFlags::empty());
1282 assert!(b.flags().is_empty());
1283 assert_eq!(b.link_id(), 123);
1284 }
1285
1286 #[test]
1289 fn cell_bits_eq_detects_bg_difference() {
1290 let cell1 = Cell::from_char('X');
1291 let cell2 = Cell::from_char('X').with_bg(PackedRgba::RED);
1292 assert!(!cell1.bits_eq(&cell2));
1293 }
1294
1295 #[test]
1296 fn cell_bits_eq_detects_attrs_difference() {
1297 let cell1 = Cell::from_char('X');
1298 let cell2 = Cell::from_char('X').with_attrs(CellAttrs::new(StyleFlags::BOLD, 0));
1299 assert!(!cell1.bits_eq(&cell2));
1300 }
1301
1302 #[test]
1303 fn cell_with_char_preserves_colors_and_attrs() {
1304 let cell = Cell::from_char('A')
1305 .with_fg(PackedRgba::RED)
1306 .with_bg(PackedRgba::BLUE)
1307 .with_attrs(CellAttrs::new(StyleFlags::BOLD, 42));
1308
1309 let updated = cell.with_char('Z');
1310 assert_eq!(updated.content.as_char(), Some('Z'));
1311 assert_eq!(updated.fg, PackedRgba::RED);
1312 assert_eq!(updated.bg, PackedRgba::BLUE);
1313 assert!(updated.attrs.has_flag(StyleFlags::BOLD));
1314 assert_eq!(updated.attrs.link_id(), 42);
1315 }
1316
1317 #[test]
1318 fn cell_new_vs_from_char() {
1319 let a = Cell::new(CellContent::from_char('A'));
1320 let b = Cell::from_char('A');
1321 assert!(a.bits_eq(&b));
1322 }
1323
1324 #[test]
1325 fn cell_continuation_has_transparent_colors() {
1326 assert_eq!(Cell::CONTINUATION.fg, PackedRgba::TRANSPARENT);
1327 assert_eq!(Cell::CONTINUATION.bg, PackedRgba::TRANSPARENT);
1328 assert_eq!(Cell::CONTINUATION.attrs, CellAttrs::NONE);
1329 }
1330
1331 #[test]
1332 fn cell_debug_format() {
1333 let cell = Cell::from_char('A');
1334 let s = format!("{:?}", cell);
1335 assert!(s.contains("Cell"), "got: {s}");
1336 assert!(s.contains("content"), "got: {s}");
1337 assert!(s.contains("fg"), "got: {s}");
1338 assert!(s.contains("bg"), "got: {s}");
1339 assert!(s.contains("attrs"), "got: {s}");
1340 }
1341
1342 #[test]
1343 fn cell_is_empty_for_various() {
1344 assert!(Cell::default().is_empty());
1345 assert!(!Cell::from_char('A').is_empty());
1346 assert!(!Cell::CONTINUATION.is_empty());
1347 }
1348
1349 #[test]
1350 fn cell_is_continuation_for_various() {
1351 assert!(!Cell::default().is_continuation());
1352 assert!(!Cell::from_char('A').is_continuation());
1353 assert!(Cell::CONTINUATION.is_continuation());
1354 }
1355
1356 #[test]
1357 fn cell_width_hint_for_grapheme() {
1358 let id = GraphemeId::new(100, 3);
1359 let cell = Cell::new(CellContent::from_grapheme(id));
1360 assert_eq!(cell.width_hint(), 3);
1361 }
1362
1363 #[test]
1366 fn grapheme_id_default() {
1367 let id = GraphemeId::default();
1368 assert_eq!(id.slot(), 0);
1369 assert_eq!(id.width(), 0);
1370 }
1371
1372 #[test]
1373 fn grapheme_id_debug_format() {
1374 let id = GraphemeId::new(42, 2);
1375 let s = format!("{:?}", id);
1376 assert!(s.contains("GraphemeId"), "got: {s}");
1377 assert!(s.contains("42"), "got: {s}");
1378 assert!(s.contains("2"), "got: {s}");
1379 }
1380
1381 #[test]
1382 fn grapheme_id_width_isolated_from_slot() {
1383 let id = GraphemeId::new(0x00FF_FFFF, 0);
1385 assert_eq!(id.width(), 0);
1386 assert_eq!(id.slot(), 0x00FF_FFFF);
1387
1388 let id2 = GraphemeId::new(0, 127);
1389 assert_eq!(id2.slot(), 0);
1390 assert_eq!(id2.width(), 127);
1391 }
1392
1393 #[test]
1396 fn style_flags_empty_has_no_bits() {
1397 assert!(StyleFlags::empty().is_empty());
1398 assert_eq!(StyleFlags::empty().bits(), 0);
1399 }
1400
1401 #[test]
1402 fn style_flags_all_has_all_bits() {
1403 let all = StyleFlags::all();
1404 assert!(all.contains(StyleFlags::BOLD));
1405 assert!(all.contains(StyleFlags::DIM));
1406 assert!(all.contains(StyleFlags::ITALIC));
1407 assert!(all.contains(StyleFlags::UNDERLINE));
1408 assert!(all.contains(StyleFlags::BLINK));
1409 assert!(all.contains(StyleFlags::REVERSE));
1410 assert!(all.contains(StyleFlags::STRIKETHROUGH));
1411 assert!(all.contains(StyleFlags::HIDDEN));
1412 }
1413
1414 #[test]
1415 fn style_flags_union_and_intersection() {
1416 let a = StyleFlags::BOLD | StyleFlags::ITALIC;
1417 let b = StyleFlags::ITALIC | StyleFlags::UNDERLINE;
1418 assert_eq!(
1419 a | b,
1420 StyleFlags::BOLD | StyleFlags::ITALIC | StyleFlags::UNDERLINE
1421 );
1422 assert_eq!(a & b, StyleFlags::ITALIC);
1423 }
1424
1425 #[test]
1426 fn style_flags_from_bits_truncate() {
1427 let all = StyleFlags::from_bits_truncate(0xFF);
1429 assert_eq!(all, StyleFlags::all());
1430
1431 let none = StyleFlags::from_bits_truncate(0x00);
1433 assert!(none.is_empty());
1434 }
1435
1436 #[test]
1441 fn over_not_commutative() {
1442 let red_half = PackedRgba::rgba(255, 0, 0, 128);
1443 let blue_half = PackedRgba::rgba(0, 0, 255, 128);
1444 let a_over_b = red_half.over(blue_half);
1445 let b_over_a = blue_half.over(red_half);
1446 assert_ne!(a_over_b, b_over_a);
1448 }
1449
1450 #[test]
1451 fn over_opaque_self_compositing_is_idempotent() {
1452 let c = PackedRgba::rgba(42, 84, 168, 255);
1453 assert_eq!(c.over(c), c);
1454 }
1455
1456 #[test]
1457 fn over_near_opaque_src_alpha_254() {
1458 let src = PackedRgba::rgba(255, 0, 0, 254);
1460 let dst = PackedRgba::rgba(0, 0, 255, 255);
1461 let result = src.over(dst);
1462 assert_eq!(result.a(), 255);
1463 assert!(result.r() >= 253, "r={}", result.r());
1465 assert!(result.b() <= 2, "b={}", result.b());
1466 }
1467
1468 #[test]
1469 fn over_both_partial_alpha_symmetric_colors() {
1470 let c = PackedRgba::rgba(200, 100, 50, 128);
1472 let result = c.over(c);
1473 let ref_result = reference_over(c, c);
1474 assert_eq!(result, ref_result);
1475 assert!(result.a() >= 190 && result.a() <= 194, "a={}", result.a());
1477 }
1478
1479 #[test]
1480 fn over_both_alpha_1_minimal() {
1481 let src = PackedRgba::rgba(255, 255, 255, 1);
1482 let dst = PackedRgba::rgba(0, 0, 0, 1);
1483 let result = src.over(dst);
1484 let ref_result = reference_over(src, dst);
1485 assert_eq!(result, ref_result);
1486 assert!(result.a() <= 3, "a={}", result.a());
1488 }
1489
1490 #[test]
1491 fn over_white_alpha_0_over_opaque_is_dst() {
1492 let src = PackedRgba::rgba(255, 255, 255, 0);
1494 let dst = PackedRgba::rgba(100, 50, 25, 255);
1495 assert_eq!(src.over(dst), dst);
1496 }
1497
1498 #[test]
1499 fn with_opacity_nan_clamps_to_zero() {
1500 let c = PackedRgba::rgba(10, 20, 30, 200);
1501 let result = c.with_opacity(f32::NAN);
1502 assert_eq!(result.r(), 10);
1506 assert_eq!(result.g(), 20);
1507 assert_eq!(result.b(), 30);
1508 }
1509
1510 #[test]
1511 fn with_opacity_negative_infinity_clamps_to_zero() {
1512 let c = PackedRgba::rgba(10, 20, 30, 200);
1513 let result = c.with_opacity(f32::NEG_INFINITY);
1514 assert_eq!(result.a(), 0);
1515 }
1516
1517 #[test]
1518 fn with_opacity_positive_infinity_clamps_to_original() {
1519 let c = PackedRgba::rgba(10, 20, 30, 200);
1520 let result = c.with_opacity(f32::INFINITY);
1521 assert_eq!(result.a(), 200);
1522 }
1523
1524 #[test]
1525 fn with_opacity_on_transparent_stays_transparent() {
1526 let c = PackedRgba::TRANSPARENT;
1527 assert_eq!(c.with_opacity(0.5).a(), 0);
1528 assert_eq!(c.with_opacity(1.0).a(), 0);
1529 }
1530
1531 #[test]
1532 fn packed_rgba_extreme_channel_values() {
1533 let all_max = PackedRgba::rgba(255, 255, 255, 255);
1534 assert_eq!(all_max.r(), 255);
1535 assert_eq!(all_max.g(), 255);
1536 assert_eq!(all_max.b(), 255);
1537 assert_eq!(all_max.a(), 255);
1538
1539 let all_zero = PackedRgba::rgba(0, 0, 0, 0);
1540 assert_eq!(all_zero, PackedRgba::TRANSPARENT);
1541 }
1542
1543 #[test]
1544 fn packed_rgba_hash_differs_for_different_values() {
1545 use std::collections::HashSet;
1546 let mut set = HashSet::new();
1547 set.insert(PackedRgba::RED);
1548 set.insert(PackedRgba::GREEN);
1549 set.insert(PackedRgba::BLUE);
1550 set.insert(PackedRgba::RED); assert_eq!(set.len(), 3);
1552 }
1553
1554 #[test]
1555 fn packed_rgba_channel_isolation() {
1556 let base = PackedRgba::rgba(10, 20, 30, 40);
1558 let different_r = PackedRgba::rgba(99, 20, 30, 40);
1559 assert_ne!(base, different_r);
1560 assert_eq!(base.g(), different_r.g());
1561 assert_eq!(base.b(), different_r.b());
1562 assert_eq!(base.a(), different_r.a());
1563 }
1564
1565 #[test]
1568 fn cell_content_nul_char_equals_empty() {
1569 let nul = CellContent::from_char('\0');
1571 assert_eq!(nul.raw(), CellContent::EMPTY.raw());
1572 assert!(nul.is_empty());
1573 assert_eq!(nul.as_char(), None); }
1575
1576 #[test]
1577 fn cell_content_max_unicode_codepoint() {
1578 let max = CellContent::from_char('\u{10FFFF}');
1579 assert_eq!(max.as_char(), Some('\u{10FFFF}'));
1580 assert!(!max.is_grapheme());
1581 assert_eq!(max.width_hint(), 1);
1583 }
1584
1585 #[test]
1586 fn cell_content_bmp_boundary_chars() {
1587 let last_before_surrogates = CellContent::from_char('\u{D7FF}');
1589 assert_eq!(last_before_surrogates.as_char(), Some('\u{D7FF}'));
1590
1591 let first_after_surrogates = CellContent::from_char('\u{E000}');
1593 assert_eq!(first_after_surrogates.as_char(), Some('\u{E000}'));
1594
1595 let supplementary = CellContent::from_char('\u{10000}');
1597 assert_eq!(supplementary.as_char(), Some('\u{10000}'));
1598 assert!(!supplementary.is_grapheme()); }
1600
1601 #[test]
1602 fn cell_content_grapheme_with_zero_width() {
1603 let id = GraphemeId::new(42, 0);
1604 let c = CellContent::from_grapheme(id);
1605 assert_eq!(c.width_hint(), 0);
1606 assert_eq!(c.width(), 0);
1607 assert!(c.is_grapheme());
1608 }
1609
1610 #[test]
1611 fn cell_content_grapheme_with_max_width() {
1612 let id = GraphemeId::new(1, GraphemeId::MAX_WIDTH);
1613 let c = CellContent::from_grapheme(id);
1614 assert_eq!(c.width_hint(), 127);
1615 assert_eq!(c.width(), 127);
1616 }
1617
1618 #[test]
1619 fn cell_content_continuation_value_is_max_i31() {
1620 assert_eq!(CellContent::CONTINUATION.raw(), 0x7FFF_FFFF);
1622 assert!(!CellContent::CONTINUATION.is_grapheme()); assert!(CellContent::CONTINUATION.is_continuation());
1624 }
1625
1626 #[test]
1627 fn cell_content_empty_and_continuation_are_distinct() {
1628 assert_ne!(CellContent::EMPTY, CellContent::CONTINUATION);
1629 assert!(CellContent::EMPTY.is_empty());
1630 assert!(!CellContent::EMPTY.is_continuation());
1631 assert!(!CellContent::CONTINUATION.is_empty());
1632 assert!(CellContent::CONTINUATION.is_continuation());
1633 }
1634
1635 #[test]
1636 fn cell_content_grapheme_id_strips_high_bit() {
1637 let id = GraphemeId::new(0x00FF_FFFF, 127);
1638 let c = CellContent::from_grapheme(id);
1639 let extracted = c.grapheme_id().unwrap();
1640 assert_eq!(extracted.slot(), id.slot());
1641 assert_eq!(extracted.width(), id.width());
1642 }
1643
1644 #[test]
1647 fn grapheme_id_slot_one_width_one() {
1648 let id = GraphemeId::new(1, 1);
1649 assert_eq!(id.slot(), 1);
1650 assert_eq!(id.width(), 1);
1651 }
1652
1653 #[test]
1654 fn grapheme_id_hash_eq_consistency() {
1655 use std::collections::HashSet;
1656 let a = GraphemeId::new(42, 2);
1657 let b = GraphemeId::new(42, 2);
1658 let c = GraphemeId::new(42, 3);
1659 assert_eq!(a, b);
1660 assert_ne!(a, c);
1661 let mut set = HashSet::new();
1662 set.insert(a);
1663 assert!(set.contains(&b));
1664 assert!(!set.contains(&c));
1665 }
1666
1667 #[test]
1668 fn grapheme_id_adjacent_slots_differ() {
1669 let a = GraphemeId::new(0, 1);
1670 let b = GraphemeId::new(1, 1);
1671 assert_ne!(a, b);
1672 assert_ne!(a.slot(), b.slot());
1673 assert_eq!(a.width(), b.width());
1674 }
1675
1676 #[test]
1679 fn cell_attrs_link_id_masks_overflow() {
1680 let a = CellAttrs::new(StyleFlags::empty(), 0x00FF_FFFE);
1682 assert_eq!(a.link_id(), 0x00FF_FFFE);
1683 }
1684
1685 #[test]
1686 fn cell_attrs_chained_mutations() {
1687 let a = CellAttrs::new(StyleFlags::BOLD, 100)
1688 .with_flags(StyleFlags::ITALIC)
1689 .with_link(200)
1690 .with_flags(StyleFlags::UNDERLINE | StyleFlags::DIM)
1691 .with_link(300);
1692 assert_eq!(a.flags(), StyleFlags::UNDERLINE | StyleFlags::DIM);
1693 assert_eq!(a.link_id(), 300);
1694 }
1695
1696 #[test]
1697 fn cell_attrs_all_flags_max_link() {
1698 let all_flags = StyleFlags::all();
1699 let a = CellAttrs::new(all_flags, CellAttrs::LINK_ID_MAX);
1700 assert_eq!(a.flags(), all_flags);
1701 assert_eq!(a.link_id(), CellAttrs::LINK_ID_MAX);
1702 assert_eq!(a.flags().bits(), 0xFF);
1704 assert_eq!(a.link_id(), 0x00FF_FFFE);
1705 }
1706
1707 #[test]
1708 fn cell_attrs_link_id_none_is_zero() {
1709 assert_eq!(CellAttrs::LINK_ID_NONE, 0);
1710 }
1711
1712 #[test]
1715 fn cell_eq_matches_bits_eq() {
1716 let pairs = [
1717 (Cell::default(), Cell::default()),
1718 (Cell::from_char('A'), Cell::from_char('A')),
1719 (Cell::from_char('A'), Cell::from_char('B')),
1720 (Cell::CONTINUATION, Cell::CONTINUATION),
1721 (
1722 Cell::from_char('X').with_fg(PackedRgba::RED),
1723 Cell::from_char('X').with_fg(PackedRgba::BLUE),
1724 ),
1725 ];
1726 for (a, b) in &pairs {
1727 assert_eq!(
1728 a == b,
1729 a.bits_eq(b),
1730 "PartialEq and bits_eq disagree for {:?} vs {:?}",
1731 a,
1732 b
1733 );
1734 }
1735 }
1736
1737 #[test]
1738 fn cell_from_grapheme_content() {
1739 let id = GraphemeId::new(42, 2);
1740 let cell = Cell::new(CellContent::from_grapheme(id));
1741 assert!(cell.content.is_grapheme());
1742 assert_eq!(cell.width_hint(), 2);
1743 assert!(!cell.is_empty());
1744 assert!(!cell.is_continuation());
1745 }
1746
1747 #[test]
1748 fn cell_with_char_on_continuation() {
1749 let cell = Cell::CONTINUATION.with_char('A');
1750 assert_eq!(cell.content.as_char(), Some('A'));
1751 assert!(!cell.is_continuation());
1752 assert_eq!(cell.fg, PackedRgba::TRANSPARENT);
1754 assert_eq!(cell.bg, PackedRgba::TRANSPARENT);
1755 }
1756
1757 #[test]
1758 fn cell_default_bits_eq_self() {
1759 let cell = Cell::default();
1760 assert!(cell.bits_eq(&cell));
1761 }
1762
1763 #[test]
1764 fn cell_new_empty_equals_default() {
1765 let a = Cell::new(CellContent::EMPTY);
1766 let b = Cell::default();
1767 assert!(a.bits_eq(&b));
1768 }
1769
1770 #[test]
1771 fn cell_all_builder_methods_chain() {
1772 let cell = Cell::default()
1773 .with_char('Z')
1774 .with_fg(PackedRgba::rgba(1, 2, 3, 4))
1775 .with_bg(PackedRgba::rgba(5, 6, 7, 8))
1776 .with_attrs(CellAttrs::new(
1777 StyleFlags::BOLD | StyleFlags::STRIKETHROUGH,
1778 999,
1779 ));
1780 assert_eq!(cell.content.as_char(), Some('Z'));
1781 assert_eq!(cell.fg.r(), 1);
1782 assert_eq!(cell.bg.a(), 8);
1783 assert!(cell.attrs.has_flag(StyleFlags::BOLD));
1784 assert!(cell.attrs.has_flag(StyleFlags::STRIKETHROUGH));
1785 assert!(!cell.attrs.has_flag(StyleFlags::ITALIC));
1786 assert_eq!(cell.attrs.link_id(), 999);
1787 }
1788
1789 #[test]
1790 fn cell_size_and_alignment_invariants() {
1791 assert_eq!(core::mem::size_of::<Cell>(), 16);
1793 assert_eq!(core::mem::align_of::<Cell>(), 16);
1794 assert_eq!(64 / core::mem::size_of::<Cell>(), 4);
1796 }
1797
1798 #[test]
1799 fn cell_content_size_invariant() {
1800 assert_eq!(core::mem::size_of::<CellContent>(), 4);
1801 }
1802
1803 #[test]
1804 fn cell_attrs_size_invariant() {
1805 assert_eq!(core::mem::size_of::<CellAttrs>(), 4);
1806 }
1807
1808 #[test]
1811 fn over_associativity_approximate() {
1812 let a = PackedRgba::rgba(200, 50, 100, 128);
1815 let b = PackedRgba::rgba(50, 200, 50, 128);
1816 let c = PackedRgba::rgba(100, 100, 200, 128);
1817
1818 let ab_c = a.over(b).over(c);
1819 let a_bc = a.over(b.over(c));
1820
1821 assert!(
1823 (ab_c.r() as i16 - a_bc.r() as i16).unsigned_abs() <= 1,
1824 "r: {} vs {}",
1825 ab_c.r(),
1826 a_bc.r()
1827 );
1828 assert!(
1829 (ab_c.g() as i16 - a_bc.g() as i16).unsigned_abs() <= 1,
1830 "g: {} vs {}",
1831 ab_c.g(),
1832 a_bc.g()
1833 );
1834 assert!(
1835 (ab_c.b() as i16 - a_bc.b() as i16).unsigned_abs() <= 1,
1836 "b: {} vs {}",
1837 ab_c.b(),
1838 a_bc.b()
1839 );
1840 assert!(
1841 (ab_c.a() as i16 - a_bc.a() as i16).unsigned_abs() <= 1,
1842 "a: {} vs {}",
1843 ab_c.a(),
1844 a_bc.a()
1845 );
1846 }
1847
1848 #[test]
1849 fn over_output_alpha_monotonic_with_src_alpha() {
1850 let dst = PackedRgba::rgba(0, 0, 255, 128);
1852 let mut prev_a = 0u8;
1853 for alpha in (0..=255).step_by(5) {
1854 let src = PackedRgba::rgba(255, 0, 0, alpha);
1855 let result = src.over(dst);
1856 assert!(
1857 result.a() >= prev_a,
1858 "alpha monotonicity violated at src_a={}: result_a={} < prev={}",
1859 alpha,
1860 result.a(),
1861 prev_a
1862 );
1863 prev_a = result.a();
1864 }
1865 }
1866
1867 #[test]
1868 fn over_sweep_matches_reference() {
1869 for alpha in (0..=255).step_by(17) {
1871 let src = PackedRgba::rgba(200, 100, 50, alpha);
1872 let dst = PackedRgba::rgba(50, 100, 200, 200);
1873 assert_eq!(
1874 src.over(dst),
1875 reference_over(src, dst),
1876 "mismatch at src_alpha={}",
1877 alpha
1878 );
1879 }
1880 }
1881}
1882
1883#[cfg(test)]
1888mod cell_proptests {
1889 use super::{Cell, CellAttrs, CellContent, GraphemeId, PackedRgba, StyleFlags};
1890 use proptest::prelude::*;
1891
1892 fn arb_packed_rgba() -> impl Strategy<Value = PackedRgba> {
1893 (any::<u8>(), any::<u8>(), any::<u8>(), any::<u8>())
1894 .prop_map(|(r, g, b, a)| PackedRgba::rgba(r, g, b, a))
1895 }
1896
1897 fn arb_grapheme_id() -> impl Strategy<Value = GraphemeId> {
1898 (0u32..=GraphemeId::MAX_SLOT, 0u8..=GraphemeId::MAX_WIDTH)
1899 .prop_map(|(slot, width)| GraphemeId::new(slot, width))
1900 }
1901
1902 fn arb_style_flags() -> impl Strategy<Value = StyleFlags> {
1903 any::<u8>().prop_map(StyleFlags::from_bits_truncate)
1904 }
1905
1906 proptest! {
1907 #[test]
1908 fn packed_rgba_roundtrips_all_components(tuple in (any::<u8>(), any::<u8>(), any::<u8>(), any::<u8>())) {
1909 let (r, g, b, a) = tuple;
1910 let c = PackedRgba::rgba(r, g, b, a);
1911 prop_assert_eq!(c.r(), r);
1912 prop_assert_eq!(c.g(), g);
1913 prop_assert_eq!(c.b(), b);
1914 prop_assert_eq!(c.a(), a);
1915 }
1916
1917 #[test]
1918 fn packed_rgba_rgb_always_opaque(tuple in (any::<u8>(), any::<u8>(), any::<u8>())) {
1919 let (r, g, b) = tuple;
1920 let c = PackedRgba::rgb(r, g, b);
1921 prop_assert_eq!(c.a(), 255);
1922 prop_assert_eq!(c.r(), r);
1923 prop_assert_eq!(c.g(), g);
1924 prop_assert_eq!(c.b(), b);
1925 }
1926
1927 #[test]
1928 fn packed_rgba_over_identity_transparent(dst in arb_packed_rgba()) {
1929 let result = PackedRgba::TRANSPARENT.over(dst);
1931 prop_assert_eq!(result, dst);
1932 }
1933
1934 #[test]
1935 fn packed_rgba_over_identity_opaque(tuple in (any::<u8>(), any::<u8>(), any::<u8>(), arb_packed_rgba())) {
1936 let (r, g, b, dst) = tuple;
1938 let src = PackedRgba::rgba(r, g, b, 255);
1939 let result = src.over(dst);
1940 prop_assert_eq!(result, src);
1941 }
1942
1943 #[test]
1944 fn grapheme_id_slot_width_roundtrip(tuple in (0u32..=GraphemeId::MAX_SLOT, 0u8..=GraphemeId::MAX_WIDTH)) {
1945 let (slot, width) = tuple;
1946 let id = GraphemeId::new(slot, width);
1947 prop_assert_eq!(id.slot(), slot as usize);
1948 prop_assert_eq!(id.width(), width as usize);
1949 }
1950
1951 #[test]
1952 fn grapheme_id_raw_roundtrip(id in arb_grapheme_id()) {
1953 let raw = id.raw();
1954 let restored = GraphemeId::from_raw(raw);
1955 prop_assert_eq!(restored.slot(), id.slot());
1956 prop_assert_eq!(restored.width(), id.width());
1957 }
1958
1959 #[test]
1960 fn cell_content_char_roundtrip(c in (0x20u32..0xD800u32).prop_union(0xE000u32..0x110000u32)) {
1961 if let Some(ch) = char::from_u32(c) {
1962 let content = CellContent::from_char(ch);
1963 prop_assert_eq!(content.as_char(), Some(ch));
1964 prop_assert!(!content.is_grapheme());
1965 prop_assert!(!content.is_empty());
1966 prop_assert!(!content.is_continuation());
1967 }
1968 }
1969
1970 #[test]
1971 fn cell_content_grapheme_roundtrip(id in arb_grapheme_id()) {
1972 let content = CellContent::from_grapheme(id);
1973 prop_assert!(content.is_grapheme());
1974 prop_assert_eq!(content.grapheme_id(), Some(id));
1975 prop_assert_eq!(content.width_hint(), id.width());
1976 }
1977
1978 #[test]
1979 fn cell_bits_eq_is_reflexive(
1980 tuple in (
1981 (0x20u32..0x80u32).prop_map(|c| char::from_u32(c).unwrap()),
1982 any::<u8>(), any::<u8>(), any::<u8>(),
1983 arb_style_flags(),
1984 ),
1985 ) {
1986 let (c, r, g, b, flags) = tuple;
1987 let cell = Cell::from_char(c)
1988 .with_fg(PackedRgba::rgb(r, g, b))
1989 .with_attrs(CellAttrs::new(flags, 0));
1990 prop_assert!(cell.bits_eq(&cell));
1991 }
1992
1993 #[test]
1994 fn cell_bits_eq_detects_fg_difference(
1995 tuple in (
1996 (0x41u32..0x5Bu32).prop_map(|c| char::from_u32(c).unwrap()),
1997 any::<u8>(), any::<u8>(),
1998 ),
1999 ) {
2000 let (c, r1, r2) = tuple;
2001 prop_assume!(r1 != r2);
2002 let cell1 = Cell::from_char(c).with_fg(PackedRgba::rgb(r1, 0, 0));
2003 let cell2 = Cell::from_char(c).with_fg(PackedRgba::rgb(r2, 0, 0));
2004 prop_assert!(!cell1.bits_eq(&cell2));
2005 }
2006
2007 #[test]
2008 fn cell_attrs_flags_roundtrip(tuple in (arb_style_flags(), 0u32..CellAttrs::LINK_ID_MAX)) {
2009 let (flags, link) = tuple;
2010 let attrs = CellAttrs::new(flags, link);
2011 prop_assert_eq!(attrs.flags(), flags);
2012 prop_assert_eq!(attrs.link_id(), link);
2013 }
2014
2015 #[test]
2016 fn cell_attrs_with_flags_preserves_link(tuple in (arb_style_flags(), 0u32..CellAttrs::LINK_ID_MAX, arb_style_flags())) {
2017 let (flags, link, new_flags) = tuple;
2018 let attrs = CellAttrs::new(flags, link);
2019 let updated = attrs.with_flags(new_flags);
2020 prop_assert_eq!(updated.flags(), new_flags);
2021 prop_assert_eq!(updated.link_id(), link);
2022 }
2023
2024 #[test]
2025 fn cell_attrs_with_link_preserves_flags(tuple in (arb_style_flags(), 0u32..CellAttrs::LINK_ID_MAX, 0u32..CellAttrs::LINK_ID_MAX)) {
2026 let (flags, link1, link2) = tuple;
2027 let attrs = CellAttrs::new(flags, link1);
2028 let updated = attrs.with_link(link2);
2029 prop_assert_eq!(updated.flags(), flags);
2030 prop_assert_eq!(updated.link_id(), link2);
2031 }
2032
2033 #[test]
2036 fn cell_bits_eq_is_symmetric(
2037 tuple in (
2038 (0x41u32..0x5Bu32).prop_map(|c| char::from_u32(c).unwrap()),
2039 (0x41u32..0x5Bu32).prop_map(|c| char::from_u32(c).unwrap()),
2040 arb_packed_rgba(),
2041 arb_packed_rgba(),
2042 ),
2043 ) {
2044 let (c1, c2, fg1, fg2) = tuple;
2045 let cell_a = Cell::from_char(c1).with_fg(fg1);
2046 let cell_b = Cell::from_char(c2).with_fg(fg2);
2047 prop_assert_eq!(cell_a.bits_eq(&cell_b), cell_b.bits_eq(&cell_a),
2048 "bits_eq is not symmetric");
2049 }
2050
2051 #[test]
2052 fn cell_content_bit31_discriminates(id in arb_grapheme_id()) {
2053 let char_content = CellContent::from_char('A');
2055 prop_assert!(!char_content.is_grapheme());
2056 prop_assert!(char_content.as_char().is_some());
2057 prop_assert!(char_content.grapheme_id().is_none());
2058
2059 let grapheme_content = CellContent::from_grapheme(id);
2061 prop_assert!(grapheme_content.is_grapheme());
2062 prop_assert!(grapheme_content.grapheme_id().is_some());
2063 prop_assert!(grapheme_content.as_char().is_none());
2064 }
2065
2066 #[test]
2067 fn cell_from_char_width_matches_unicode(
2068 c in (0x20u32..0x7Fu32).prop_map(|c| char::from_u32(c).unwrap()),
2069 ) {
2070 let cell = Cell::from_char(c);
2071 prop_assert_eq!(cell.width_hint(), 1,
2072 "Cell width hint for '{}' should be 1 for ASCII", c);
2073 }
2074 }
2075
2076 #[test]
2079 fn cell_content_continuation_has_zero_width() {
2080 let cont = CellContent::CONTINUATION;
2081 assert_eq!(cont.width(), 0, "CONTINUATION cell should have width 0");
2082 assert!(cont.is_continuation());
2083 assert!(!cont.is_grapheme());
2084 }
2085
2086 #[test]
2087 fn cell_content_empty_has_zero_width() {
2088 let empty = CellContent::EMPTY;
2089 assert_eq!(empty.width(), 0, "EMPTY cell should have width 0");
2090 assert!(empty.is_empty());
2091 assert!(!empty.is_grapheme());
2092 assert!(!empty.is_continuation());
2093 }
2094
2095 #[test]
2096 fn cell_default_is_empty() {
2097 let cell = Cell::default();
2098 assert!(cell.is_empty());
2099 assert_eq!(cell.width_hint(), 0);
2100 }
2101}