1#![forbid(unsafe_code)]
2
3use crate::char_width;
28
29#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
50#[repr(transparent)]
51pub struct GraphemeId(u32);
52
53impl GraphemeId {
54 pub const MAX_SLOT: u32 = 0xFFFF;
56
57 pub const MAX_WIDTH: u8 = 15;
59
60 pub const MAX_GENERATION: u16 = 2047;
62
63 #[inline]
69 pub const fn new(slot: u32, generation: u16, width: u8) -> Self {
70 debug_assert!(slot <= Self::MAX_SLOT, "slot overflow");
71 debug_assert!(generation <= Self::MAX_GENERATION, "generation overflow");
72 debug_assert!(width <= Self::MAX_WIDTH, "width overflow");
73 Self(
74 (slot & Self::MAX_SLOT)
75 | (((generation as u32) & 0x7FF) << 16)
76 | ((width as u32) << 27),
77 )
78 }
79
80 #[inline]
82 pub const fn slot(self) -> usize {
83 (self.0 & Self::MAX_SLOT) as usize
84 }
85
86 #[inline]
88 pub const fn generation(self) -> u16 {
89 ((self.0 >> 16) & 0x7FF) as u16
90 }
91
92 #[inline]
94 pub const fn width(self) -> usize {
95 ((self.0 >> 27) & 0x0F) as usize
96 }
97
98 #[inline]
100 pub const fn raw(self) -> u32 {
101 self.0
102 }
103
104 #[inline]
106 pub const fn from_raw(raw: u32) -> Self {
107 Self(raw)
108 }
109}
110
111impl core::fmt::Debug for GraphemeId {
112 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
113 f.debug_struct("GraphemeId")
114 .field("slot", &self.slot())
115 .field("gen", &self.generation())
116 .field("width", &self.width())
117 .finish()
118 }
119}
120
121#[derive(Clone, Copy, PartialEq, Eq, Hash)]
140#[repr(transparent)]
141pub struct CellContent(u32);
142
143impl CellContent {
144 pub const EMPTY: Self = Self(0);
146
147 pub const CONTINUATION: Self = Self(0x7FFF_FFFF);
155
156 #[inline]
166 pub const fn from_char(c: char) -> Self {
167 if c == '\t' {
168 Self(' ' as u32)
169 } else {
170 Self(c as u32)
171 }
172 }
173
174 #[inline]
178 pub const fn from_grapheme(id: GraphemeId) -> Self {
179 Self(0x8000_0000 | id.raw())
180 }
181
182 #[inline]
184 pub const fn is_grapheme(self) -> bool {
185 self.0 & 0x8000_0000 != 0
186 }
187
188 #[inline]
190 pub const fn is_continuation(self) -> bool {
191 self.0 == Self::CONTINUATION.0
192 }
193
194 #[inline]
196 pub const fn is_empty(self) -> bool {
197 self.0 == Self::EMPTY.0
198 }
199
200 #[inline]
204 pub const fn is_default(self) -> bool {
205 self.0 == Self::EMPTY.0
206 }
207
208 #[inline]
212 pub fn as_char(self) -> Option<char> {
213 if self.is_grapheme() || self.0 == Self::EMPTY.0 || self.0 == Self::CONTINUATION.0 {
214 None
215 } else {
216 char::from_u32(self.0)
217 }
218 }
219
220 #[inline]
224 pub const fn grapheme_id(self) -> Option<GraphemeId> {
225 if self.is_grapheme() {
226 Some(GraphemeId::from_raw(self.0 & !0x8000_0000))
227 } else {
228 None
229 }
230 }
231
232 #[inline]
242 pub const fn width_hint(self) -> usize {
243 if self.is_empty() || self.is_continuation() {
244 0
245 } else if self.is_grapheme() {
246 ((self.0 >> 27) & 0x0F) as usize
247 } else {
248 1
251 }
252 }
253
254 #[inline]
258 pub fn width(self) -> usize {
259 if self.is_empty() || self.is_continuation() {
260 0
261 } else if self.is_grapheme() {
262 ((self.0 >> 27) & 0x0F) as usize
263 } else {
264 let Some(c) = self.as_char() else {
265 return 1;
266 };
267 char_width(c)
268 }
269 }
270
271 #[inline]
273 pub const fn raw(self) -> u32 {
274 self.0
275 }
276}
277
278impl Default for CellContent {
279 fn default() -> Self {
280 Self::EMPTY
281 }
282}
283
284impl core::fmt::Debug for CellContent {
285 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
286 if self.is_empty() {
287 write!(f, "CellContent::EMPTY")
288 } else if self.is_continuation() {
289 write!(f, "CellContent::CONTINUATION")
290 } else if let Some(c) = self.as_char() {
291 write!(f, "CellContent::Char({c:?})")
292 } else if let Some(id) = self.grapheme_id() {
293 write!(f, "CellContent::Grapheme({id:?})")
294 } else {
295 write!(f, "CellContent(0x{:08x})", self.0)
296 }
297 }
298}
299
300#[derive(Clone, Copy, PartialEq, Eq)]
325#[repr(C, align(16))]
326pub struct Cell {
327 pub content: CellContent,
329 pub fg: PackedRgba,
331 pub bg: PackedRgba,
333 pub attrs: CellAttrs,
335}
336
337const _: () = assert!(core::mem::size_of::<Cell>() == 16);
339
340impl Cell {
341 pub const CONTINUATION: Self = Self {
346 content: CellContent::CONTINUATION,
347 fg: PackedRgba::TRANSPARENT,
348 bg: PackedRgba::TRANSPARENT,
349 attrs: CellAttrs::NONE,
350 };
351
352 #[inline]
354 pub const fn new(content: CellContent) -> Self {
355 Self {
356 content,
357 fg: PackedRgba::WHITE,
358 bg: PackedRgba::TRANSPARENT,
359 attrs: CellAttrs::NONE,
360 }
361 }
362
363 #[inline]
365 pub const fn from_char(c: char) -> Self {
366 Self::new(CellContent::from_char(c))
367 }
368
369 #[inline]
371 pub const fn is_continuation(&self) -> bool {
372 self.content.is_continuation()
373 }
374
375 #[inline]
377 pub const fn is_empty(&self) -> bool {
378 self.content.is_empty()
379 }
380
381 #[inline]
385 pub const fn width_hint(&self) -> usize {
386 self.content.width_hint()
387 }
388
389 #[inline(always)]
396 pub fn bits_eq(&self, other: &Self) -> bool {
397 (self.content.raw() == other.content.raw())
398 & (self.fg == other.fg)
399 & (self.bg == other.bg)
400 & (self.attrs == other.attrs)
401 }
402
403 #[inline]
405 #[must_use]
406 pub const fn with_char(mut self, c: char) -> Self {
407 self.content = CellContent::from_char(c);
408 self
409 }
410
411 #[inline]
413 #[must_use]
414 pub const fn with_fg(mut self, fg: PackedRgba) -> Self {
415 self.fg = fg;
416 self
417 }
418
419 #[inline]
421 #[must_use]
422 pub const fn with_bg(mut self, bg: PackedRgba) -> Self {
423 self.bg = bg;
424 self
425 }
426
427 #[inline]
429 #[must_use]
430 pub const fn with_attrs(mut self, attrs: CellAttrs) -> Self {
431 self.attrs = attrs;
432 self
433 }
434}
435impl Default for Cell {
436 fn default() -> Self {
437 Self {
438 content: CellContent::EMPTY,
439 fg: PackedRgba::WHITE,
440 bg: PackedRgba::TRANSPARENT,
441 attrs: CellAttrs::NONE,
442 }
443 }
444}
445
446impl core::fmt::Debug for Cell {
447 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
448 f.debug_struct("Cell")
449 .field("content", &self.content)
450 .field("fg", &self.fg)
451 .field("bg", &self.bg)
452 .field("attrs", &self.attrs)
453 .finish()
454 }
455}
456
457#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
467#[repr(transparent)]
468pub struct PackedRgba(pub u32);
469
470impl PackedRgba {
471 pub const TRANSPARENT: Self = Self(0);
473 pub const BLACK: Self = Self::rgb(0, 0, 0);
475 pub const WHITE: Self = Self::rgb(255, 255, 255);
477 pub const RED: Self = Self::rgb(255, 0, 0);
479 pub const GREEN: Self = Self::rgb(0, 255, 0);
481 pub const BLUE: Self = Self::rgb(0, 0, 255);
483
484 #[inline]
486 pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
487 Self::rgba(r, g, b, 255)
488 }
489
490 #[inline]
492 pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
493 Self(((r as u32) << 24) | ((g as u32) << 16) | ((b as u32) << 8) | (a as u32))
494 }
495
496 #[inline]
498 pub const fn r(self) -> u8 {
499 (self.0 >> 24) as u8
500 }
501
502 #[inline]
504 pub const fn g(self) -> u8 {
505 (self.0 >> 16) as u8
506 }
507
508 #[inline]
510 pub const fn b(self) -> u8 {
511 (self.0 >> 8) as u8
512 }
513
514 #[inline]
516 pub const fn a(self) -> u8 {
517 self.0 as u8
518 }
519
520 #[inline]
521 const fn div_round_u8(numer: u64, denom: u64) -> u8 {
522 debug_assert!(denom != 0);
523 let v = (numer + (denom / 2)) / denom;
524 if v > 255 { 255 } else { v as u8 }
525 }
526
527 #[inline]
532 #[must_use]
533 pub fn over(self, dst: Self) -> Self {
534 let s_a = self.a() as u64;
535 if s_a == 255 {
536 return self;
537 }
538 if s_a == 0 {
539 return dst;
540 }
541
542 let d_a = dst.a() as u64;
543 let inv_s_a = 255 - s_a;
544
545 let numer_a = 255 * s_a + d_a * inv_s_a;
550 if numer_a == 0 {
551 return Self::TRANSPARENT;
552 }
553
554 let out_a = Self::div_round_u8(numer_a, 255);
555
556 let r = Self::div_round_u8(
559 (self.r() as u64) * s_a * 255 + (dst.r() as u64) * d_a * inv_s_a,
560 numer_a,
561 );
562 let g = Self::div_round_u8(
563 (self.g() as u64) * s_a * 255 + (dst.g() as u64) * d_a * inv_s_a,
564 numer_a,
565 );
566 let b = Self::div_round_u8(
567 (self.b() as u64) * s_a * 255 + (dst.b() as u64) * d_a * inv_s_a,
568 numer_a,
569 );
570
571 Self::rgba(r, g, b, out_a)
572 }
573
574 #[inline]
576 #[must_use]
577 pub fn with_opacity(self, opacity: f32) -> Self {
578 let opacity = opacity.clamp(0.0, 1.0);
579 let a = ((self.a() as f32) * opacity).round().clamp(0.0, 255.0) as u8;
580 Self::rgba(self.r(), self.g(), self.b(), a)
581 }
582}
583
584bitflags::bitflags! {
585 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
587 pub struct StyleFlags: u8 {
588 const BOLD = 0b0000_0001;
590 const DIM = 0b0000_0010;
592 const ITALIC = 0b0000_0100;
594 const UNDERLINE = 0b0000_1000;
596 const BLINK = 0b0001_0000;
598 const REVERSE = 0b0010_0000;
600 const STRIKETHROUGH = 0b0100_0000;
602 const HIDDEN = 0b1000_0000;
604 }
605}
606
607#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
611#[repr(transparent)]
612pub struct CellAttrs(u32);
613
614impl CellAttrs {
615 pub const NONE: Self = Self(0);
617
618 pub const LINK_ID_NONE: u32 = 0;
620 pub const LINK_ID_MAX: u32 = 0x00FF_FFFE;
622
623 #[inline]
625 pub fn new(flags: StyleFlags, link_id: u32) -> Self {
626 debug_assert!(
627 link_id <= Self::LINK_ID_MAX,
628 "link_id overflow: {link_id} (max={})",
629 Self::LINK_ID_MAX
630 );
631 Self(((flags.bits() as u32) << 24) | (link_id & 0x00FF_FFFF))
632 }
633
634 #[inline]
636 pub fn flags(self) -> StyleFlags {
637 StyleFlags::from_bits_truncate((self.0 >> 24) as u8)
638 }
639
640 #[inline]
642 pub fn link_id(self) -> u32 {
643 self.0 & 0x00FF_FFFF
644 }
645
646 #[inline]
648 #[must_use]
649 pub fn with_flags(self, flags: StyleFlags) -> Self {
650 Self((self.0 & 0x00FF_FFFF) | ((flags.bits() as u32) << 24))
651 }
652
653 #[inline]
655 #[must_use]
656 pub fn with_link(self, link_id: u32) -> Self {
657 debug_assert!(
658 link_id <= Self::LINK_ID_MAX,
659 "link_id overflow: {link_id} (max={})",
660 Self::LINK_ID_MAX
661 );
662 Self((self.0 & 0xFF00_0000) | (link_id & 0x00FF_FFFF))
663 }
664
665 #[inline]
670 #[must_use]
671 pub fn merged_flags(self, extra: StyleFlags) -> Self {
672 let combined = self.flags().union(extra);
673 Self((self.0 & 0x00FF_FFFF) | ((combined.bits() as u32) << 24))
674 }
675
676 #[inline]
678 pub fn has_flag(self, flag: StyleFlags) -> bool {
679 self.flags().contains(flag)
680 }
681}
682
683#[cfg(test)]
684mod tests {
685 use super::{Cell, CellAttrs, CellContent, GraphemeId, PackedRgba, StyleFlags};
686
687 fn reference_over(src: PackedRgba, dst: PackedRgba) -> PackedRgba {
688 let sr = src.r() as f64 / 255.0;
689 let sg = src.g() as f64 / 255.0;
690 let sb = src.b() as f64 / 255.0;
691 let sa = src.a() as f64 / 255.0;
692
693 let dr = dst.r() as f64 / 255.0;
694 let dg = dst.g() as f64 / 255.0;
695 let db = dst.b() as f64 / 255.0;
696 let da = dst.a() as f64 / 255.0;
697
698 let out_a = sa + da * (1.0 - sa);
699 if out_a <= 0.0 {
700 return PackedRgba::TRANSPARENT;
701 }
702
703 let out_r = (sr * sa + dr * da * (1.0 - sa)) / out_a;
704 let out_g = (sg * sa + dg * da * (1.0 - sa)) / out_a;
705 let out_b = (sb * sa + db * da * (1.0 - sa)) / out_a;
706
707 let to_u8 = |x: f64| -> u8 { (x * 255.0).round().clamp(0.0, 255.0) as u8 };
708 PackedRgba::rgba(to_u8(out_r), to_u8(out_g), to_u8(out_b), to_u8(out_a))
709 }
710
711 #[test]
712 fn packed_rgba_is_4_bytes() {
713 assert_eq!(core::mem::size_of::<PackedRgba>(), 4);
714 }
715
716 #[test]
717 fn rgb_sets_alpha_to_255() {
718 let c = PackedRgba::rgb(1, 2, 3);
719 assert_eq!(c.r(), 1);
720 assert_eq!(c.g(), 2);
721 assert_eq!(c.b(), 3);
722 assert_eq!(c.a(), 255);
723 }
724
725 #[test]
726 fn rgba_round_trips_components() {
727 let c = PackedRgba::rgba(10, 20, 30, 40);
728 assert_eq!(c.r(), 10);
729 assert_eq!(c.g(), 20);
730 assert_eq!(c.b(), 30);
731 assert_eq!(c.a(), 40);
732 }
733
734 #[test]
735 fn over_with_opaque_src_returns_src() {
736 let src = PackedRgba::rgba(1, 2, 3, 255);
737 let dst = PackedRgba::rgba(9, 8, 7, 200);
738 assert_eq!(src.over(dst), src);
739 }
740
741 #[test]
742 fn over_with_transparent_src_returns_dst() {
743 let src = PackedRgba::TRANSPARENT;
744 let dst = PackedRgba::rgba(9, 8, 7, 200);
745 assert_eq!(src.over(dst), dst);
746 }
747
748 #[test]
749 fn over_blends_correctly_for_half_alpha_over_opaque() {
750 let src = PackedRgba::rgba(255, 0, 0, 128);
752 let dst = PackedRgba::rgba(0, 0, 255, 255);
753 assert_eq!(src.over(dst), PackedRgba::rgba(128, 0, 127, 255));
754 }
755
756 #[test]
757 fn over_matches_reference_for_partial_alpha_cases() {
758 let cases = [
759 (
760 PackedRgba::rgba(200, 10, 10, 64),
761 PackedRgba::rgba(10, 200, 10, 128),
762 ),
763 (
764 PackedRgba::rgba(1, 2, 3, 1),
765 PackedRgba::rgba(250, 251, 252, 254),
766 ),
767 (
768 PackedRgba::rgba(100, 0, 200, 200),
769 PackedRgba::rgba(0, 120, 30, 50),
770 ),
771 ];
772
773 for (src, dst) in cases {
774 assert_eq!(src.over(dst), reference_over(src, dst));
775 }
776 }
777
778 #[test]
779 fn with_opacity_scales_alpha() {
780 let c = PackedRgba::rgba(10, 20, 30, 255);
781 assert_eq!(c.with_opacity(0.5).a(), 128);
782 assert_eq!(c.with_opacity(-1.0).a(), 0);
783 assert_eq!(c.with_opacity(2.0).a(), 255);
784 }
785
786 #[test]
787 fn cell_attrs_is_4_bytes() {
788 assert_eq!(core::mem::size_of::<CellAttrs>(), 4);
789 }
790
791 #[test]
792 fn cell_attrs_none_has_no_flags_and_no_link() {
793 assert!(CellAttrs::NONE.flags().is_empty());
794 assert_eq!(CellAttrs::NONE.link_id(), 0);
795 }
796
797 #[test]
798 fn cell_attrs_new_stores_flags_and_link() {
799 let flags = StyleFlags::BOLD | StyleFlags::ITALIC;
800 let a = CellAttrs::new(flags, 42);
801 assert_eq!(a.flags(), flags);
802 assert_eq!(a.link_id(), 42);
803 }
804
805 #[test]
806 fn cell_attrs_with_flags_preserves_link_id() {
807 let a = CellAttrs::new(StyleFlags::BOLD, 123);
808 let b = a.with_flags(StyleFlags::UNDERLINE);
809 assert_eq!(b.flags(), StyleFlags::UNDERLINE);
810 assert_eq!(b.link_id(), 123);
811 }
812
813 #[test]
814 fn cell_attrs_merged_flags_ors_without_clearing() {
815 let a = CellAttrs::new(StyleFlags::BOLD, 42);
816 let b = a.merged_flags(StyleFlags::ITALIC);
817 assert_eq!(b.flags(), StyleFlags::BOLD | StyleFlags::ITALIC);
818 assert_eq!(b.link_id(), 42, "link_id must be preserved");
819 }
820
821 #[test]
822 fn cell_attrs_merged_flags_noop_for_empty() {
823 let a = CellAttrs::new(StyleFlags::BOLD, 7);
824 let b = a.merged_flags(StyleFlags::empty());
825 assert_eq!(b.flags(), StyleFlags::BOLD);
826 assert_eq!(b.link_id(), 7);
827 }
828
829 #[test]
830 fn cell_attrs_with_link_preserves_flags() {
831 let a = CellAttrs::new(StyleFlags::BOLD | StyleFlags::ITALIC, 1);
832 let b = a.with_link(999);
833 assert_eq!(b.flags(), StyleFlags::BOLD | StyleFlags::ITALIC);
834 assert_eq!(b.link_id(), 999);
835 }
836
837 #[test]
838 fn cell_attrs_flag_combinations_work() {
839 let flags = StyleFlags::BOLD | StyleFlags::ITALIC;
840 let a = CellAttrs::new(flags, 0);
841 assert!(a.has_flag(StyleFlags::BOLD));
842 assert!(a.has_flag(StyleFlags::ITALIC));
843 assert!(!a.has_flag(StyleFlags::UNDERLINE));
844 }
845
846 #[test]
847 fn cell_attrs_link_id_max_boundary() {
848 let a = CellAttrs::new(StyleFlags::empty(), CellAttrs::LINK_ID_MAX);
849 assert_eq!(a.link_id(), CellAttrs::LINK_ID_MAX);
850 }
851
852 #[test]
855 fn grapheme_id_is_4_bytes() {
856 assert_eq!(core::mem::size_of::<GraphemeId>(), 4);
857 }
858
859 #[test]
860 fn grapheme_id_encoding_roundtrip() {
861 let id = GraphemeId::new(12345, 42, 2);
862 assert_eq!(id.slot(), 12345);
863 assert_eq!(id.generation(), 42);
864 assert_eq!(id.width(), 2);
865 }
866
867 #[test]
868 fn grapheme_id_max_values() {
869 let id = GraphemeId::new(
870 GraphemeId::MAX_SLOT,
871 GraphemeId::MAX_GENERATION,
872 GraphemeId::MAX_WIDTH,
873 );
874 assert_eq!(id.slot(), 0xFFFF);
875 assert_eq!(id.generation(), GraphemeId::MAX_GENERATION);
876 assert_eq!(id.width(), GraphemeId::MAX_WIDTH as usize);
877 }
878
879 #[test]
880 fn grapheme_id_zero_values() {
881 let id = GraphemeId::new(0, 0, 0);
882 assert_eq!(id.slot(), 0);
883 assert_eq!(id.generation(), 0);
884 assert_eq!(id.width(), 0);
885 }
886
887 #[test]
888 fn grapheme_id_raw_roundtrip() {
889 let id = GraphemeId::new(999, 128, 5);
890 let raw = id.raw();
891 let restored = GraphemeId::from_raw(raw);
892 assert_eq!(restored.slot(), 999);
893 assert_eq!(restored.generation(), 128);
894 assert_eq!(restored.width(), 5);
895 }
896
897 #[test]
900 fn cell_content_is_4_bytes() {
901 assert_eq!(core::mem::size_of::<CellContent>(), 4);
902 }
903
904 #[test]
905 fn cell_content_empty_properties() {
906 assert!(CellContent::EMPTY.is_empty());
907 assert!(!CellContent::EMPTY.is_continuation());
908 assert!(!CellContent::EMPTY.is_grapheme());
909 assert_eq!(CellContent::EMPTY.width_hint(), 0);
910 }
911
912 #[test]
913 fn cell_content_continuation_properties() {
914 assert!(CellContent::CONTINUATION.is_continuation());
915 assert!(!CellContent::CONTINUATION.is_empty());
916 assert!(!CellContent::CONTINUATION.is_grapheme());
917 assert_eq!(CellContent::CONTINUATION.width_hint(), 0);
918 }
919
920 #[test]
921 fn cell_content_from_char_ascii() {
922 let c = CellContent::from_char('A');
923 assert!(!c.is_grapheme());
924 assert!(!c.is_empty());
925 assert!(!c.is_continuation());
926 assert_eq!(c.as_char(), Some('A'));
927 assert_eq!(c.width_hint(), 1);
928 }
929
930 #[test]
931 fn cell_content_from_char_unicode() {
932 let c = CellContent::from_char('日');
934 assert_eq!(c.as_char(), Some('日'));
935 assert!(!c.is_grapheme());
936
937 let c2 = CellContent::from_char('🎉');
939 assert_eq!(c2.as_char(), Some('🎉'));
940 assert!(!c2.is_grapheme());
941 }
942
943 #[test]
944 fn cell_content_from_grapheme() {
945 let id = GraphemeId::new(42, 0, 2);
946 let c = CellContent::from_grapheme(id);
947
948 assert!(c.is_grapheme());
949 assert!(!c.is_empty());
950 assert!(!c.is_continuation());
951 assert_eq!(c.grapheme_id(), Some(id));
952 assert_eq!(c.as_char(), None);
953 assert_eq!(c.width_hint(), 2);
954 }
955
956 #[test]
957 fn cell_content_width_for_chars() {
958 let ascii = CellContent::from_char('A');
959 assert_eq!(ascii.width(), 1);
960
961 let wide = CellContent::from_char('日');
962 assert_eq!(wide.width(), 2);
963
964 let emoji = CellContent::from_char('🎉');
965 assert_eq!(emoji.width(), 2);
966
967 let bolt = CellContent::from_char('⚡');
972 assert_eq!(bolt.width(), 2, "bolt is Wide, always width 2");
973
974 let gear = CellContent::from_char('⚙');
976 let heart = CellContent::from_char('❤');
977 assert!(
978 [1, 2].contains(&gear.width()),
979 "gear should be 1 (non-CJK) or 2 (CJK), got {}",
980 gear.width()
981 );
982 assert_eq!(
983 gear.width(),
984 heart.width(),
985 "gear and heart should have same width (both Neutral)"
986 );
987 }
988
989 #[test]
990 fn cell_content_width_for_grapheme() {
991 let id = GraphemeId::new(7, 0, 3);
992 let c = CellContent::from_grapheme(id);
993 assert_eq!(c.width(), 3);
994 }
995
996 #[test]
997 fn cell_content_width_empty_is_zero() {
998 assert_eq!(CellContent::EMPTY.width(), 0);
999 assert_eq!(CellContent::CONTINUATION.width(), 0);
1000 }
1001
1002 #[test]
1003 fn cell_content_grapheme_discriminator_bit() {
1004 let char_content = CellContent::from_char('X');
1006 assert_eq!(char_content.raw() & 0x8000_0000, 0);
1007
1008 let grapheme_content = CellContent::from_grapheme(GraphemeId::new(1, 0, 1));
1010 assert_ne!(grapheme_content.raw() & 0x8000_0000, 0);
1011 }
1012
1013 #[test]
1016 fn cell_is_16_bytes() {
1017 assert_eq!(core::mem::size_of::<Cell>(), 16);
1018 }
1019
1020 #[test]
1021 fn cell_alignment_is_16() {
1022 assert_eq!(core::mem::align_of::<Cell>(), 16);
1023 }
1024
1025 #[test]
1026 fn cell_default_properties() {
1027 let cell = Cell::default();
1028 assert!(cell.is_empty());
1029 assert!(!cell.is_continuation());
1030 assert_eq!(cell.fg, PackedRgba::WHITE);
1031 assert_eq!(cell.bg, PackedRgba::TRANSPARENT);
1032 assert_eq!(cell.attrs, CellAttrs::NONE);
1033 }
1034
1035 #[test]
1036 fn cell_continuation_constant() {
1037 assert!(Cell::CONTINUATION.is_continuation());
1038 assert!(!Cell::CONTINUATION.is_empty());
1039 }
1040
1041 #[test]
1042 fn cell_from_char() {
1043 let cell = Cell::from_char('X');
1044 assert_eq!(cell.content.as_char(), Some('X'));
1045 assert_eq!(cell.fg, PackedRgba::WHITE);
1046 assert_eq!(cell.bg, PackedRgba::TRANSPARENT);
1047 }
1048
1049 #[test]
1050 fn cell_builder_methods() {
1051 let cell = Cell::from_char('A')
1052 .with_fg(PackedRgba::rgb(255, 0, 0))
1053 .with_bg(PackedRgba::rgb(0, 0, 255))
1054 .with_attrs(CellAttrs::new(StyleFlags::BOLD, 0));
1055
1056 assert_eq!(cell.content.as_char(), Some('A'));
1057 assert_eq!(cell.fg, PackedRgba::rgb(255, 0, 0));
1058 assert_eq!(cell.bg, PackedRgba::rgb(0, 0, 255));
1059 assert!(cell.attrs.has_flag(StyleFlags::BOLD));
1060 }
1061
1062 #[test]
1063 fn cell_bits_eq_same_cells() {
1064 let cell1 = Cell::from_char('X').with_fg(PackedRgba::rgb(1, 2, 3));
1065 let cell2 = Cell::from_char('X').with_fg(PackedRgba::rgb(1, 2, 3));
1066 assert!(cell1.bits_eq(&cell2));
1067 }
1068
1069 #[test]
1070 fn cell_bits_eq_different_cells() {
1071 let cell1 = Cell::from_char('X');
1072 let cell2 = Cell::from_char('Y');
1073 assert!(!cell1.bits_eq(&cell2));
1074
1075 let cell3 = Cell::from_char('X').with_fg(PackedRgba::rgb(1, 2, 3));
1076 assert!(!cell1.bits_eq(&cell3));
1077 }
1078
1079 #[test]
1080 fn cell_width_hint() {
1081 let empty = Cell::default();
1082 assert_eq!(empty.width_hint(), 0);
1083
1084 let cont = Cell::CONTINUATION;
1085 assert_eq!(cont.width_hint(), 0);
1086
1087 let ascii = Cell::from_char('A');
1088 assert_eq!(ascii.width_hint(), 1);
1089 }
1090
1091 #[test]
1096 fn packed_rgba_named_constants() {
1097 assert_eq!(PackedRgba::TRANSPARENT, PackedRgba(0));
1098 assert_eq!(PackedRgba::TRANSPARENT.a(), 0);
1099
1100 assert_eq!(PackedRgba::BLACK.r(), 0);
1101 assert_eq!(PackedRgba::BLACK.g(), 0);
1102 assert_eq!(PackedRgba::BLACK.b(), 0);
1103 assert_eq!(PackedRgba::BLACK.a(), 255);
1104
1105 assert_eq!(PackedRgba::WHITE.r(), 255);
1106 assert_eq!(PackedRgba::WHITE.g(), 255);
1107 assert_eq!(PackedRgba::WHITE.b(), 255);
1108 assert_eq!(PackedRgba::WHITE.a(), 255);
1109
1110 assert_eq!(PackedRgba::RED, PackedRgba::rgb(255, 0, 0));
1111 assert_eq!(PackedRgba::GREEN, PackedRgba::rgb(0, 255, 0));
1112 assert_eq!(PackedRgba::BLUE, PackedRgba::rgb(0, 0, 255));
1113 }
1114
1115 #[test]
1116 fn packed_rgba_default_is_transparent() {
1117 assert_eq!(PackedRgba::default(), PackedRgba::TRANSPARENT);
1118 }
1119
1120 #[test]
1121 fn over_both_transparent_returns_transparent() {
1122 let result = PackedRgba::TRANSPARENT.over(PackedRgba::TRANSPARENT);
1124 assert_eq!(result, PackedRgba::TRANSPARENT);
1125 }
1126
1127 #[test]
1128 fn over_partial_alpha_over_transparent_dst() {
1129 let src = PackedRgba::rgba(200, 100, 50, 128);
1131 let result = src.over(PackedRgba::TRANSPARENT);
1132 assert_eq!(result.a(), 128);
1134 assert_eq!(result.r(), 200);
1136 assert_eq!(result.g(), 100);
1137 assert_eq!(result.b(), 50);
1138 }
1139
1140 #[test]
1141 fn over_very_low_alpha() {
1142 let src = PackedRgba::rgba(255, 0, 0, 1);
1144 let dst = PackedRgba::rgba(0, 0, 255, 255);
1145 let result = src.over(dst);
1146 assert_eq!(result.a(), 255);
1148 assert!(result.b() > 250, "b={} should be near 255", result.b());
1149 assert!(result.r() < 5, "r={} should be near 0", result.r());
1150 }
1151
1152 #[test]
1153 fn with_opacity_exact_zero() {
1154 let c = PackedRgba::rgba(10, 20, 30, 200);
1155 let result = c.with_opacity(0.0);
1156 assert_eq!(result.a(), 0);
1157 assert_eq!(result.r(), 10); assert_eq!(result.g(), 20);
1159 assert_eq!(result.b(), 30);
1160 }
1161
1162 #[test]
1163 fn with_opacity_exact_one() {
1164 let c = PackedRgba::rgba(10, 20, 30, 200);
1165 let result = c.with_opacity(1.0);
1166 assert_eq!(result.a(), 200); assert_eq!(result.r(), 10);
1168 }
1169
1170 #[test]
1171 fn with_opacity_preserves_rgb() {
1172 let c = PackedRgba::rgba(42, 84, 168, 255);
1173 let result = c.with_opacity(0.25);
1174 assert_eq!(result.r(), 42);
1175 assert_eq!(result.g(), 84);
1176 assert_eq!(result.b(), 168);
1177 assert_eq!(result.a(), 64); }
1179
1180 #[test]
1183 fn cell_content_as_char_none_for_empty() {
1184 assert_eq!(CellContent::EMPTY.as_char(), None);
1185 }
1186
1187 #[test]
1188 fn cell_content_as_char_none_for_continuation() {
1189 assert_eq!(CellContent::CONTINUATION.as_char(), None);
1190 }
1191
1192 #[test]
1193 fn cell_content_as_char_none_for_grapheme() {
1194 let id = GraphemeId::new(1, 2, 1);
1195 let c = CellContent::from_grapheme(id);
1196 assert_eq!(c.as_char(), None);
1197 }
1198
1199 #[test]
1200 fn cell_content_grapheme_id_none_for_char() {
1201 let c = CellContent::from_char('A');
1202 assert_eq!(c.grapheme_id(), None);
1203 }
1204
1205 #[test]
1206 fn cell_content_grapheme_id_none_for_empty() {
1207 assert_eq!(CellContent::EMPTY.grapheme_id(), None);
1208 }
1209
1210 #[test]
1211 fn cell_content_width_control_chars() {
1212 let tab = CellContent::from_char('\t');
1215 assert_eq!(tab.width(), 1);
1216
1217 let bel = CellContent::from_char('\x07');
1218 assert_eq!(bel.width(), 0);
1219 }
1220
1221 #[test]
1222 fn cell_content_width_hint_always_1_for_chars() {
1223 let wide = CellContent::from_char('日');
1225 assert_eq!(wide.width_hint(), 1); assert_eq!(wide.width(), 2); }
1228
1229 #[test]
1230 fn cell_content_default_is_empty() {
1231 assert_eq!(CellContent::default(), CellContent::EMPTY);
1232 }
1233
1234 #[test]
1235 fn cell_content_debug_empty() {
1236 let s = format!("{:?}", CellContent::EMPTY);
1237 assert_eq!(s, "CellContent::EMPTY");
1238 }
1239
1240 #[test]
1241 fn cell_content_debug_continuation() {
1242 let s = format!("{:?}", CellContent::CONTINUATION);
1243 assert_eq!(s, "CellContent::CONTINUATION");
1244 }
1245
1246 #[test]
1247 fn cell_content_debug_char() {
1248 let s = format!("{:?}", CellContent::from_char('X'));
1249 assert!(s.starts_with("CellContent::Char("), "got: {s}");
1250 }
1251
1252 #[test]
1253 fn cell_content_debug_grapheme() {
1254 let id = GraphemeId::new(1, 2, 1);
1255 let s = format!("{:?}", CellContent::from_grapheme(id));
1256 assert!(s.starts_with("CellContent::Grapheme("), "got: {s}");
1257 }
1258
1259 #[test]
1260 fn cell_content_raw_value() {
1261 let c = CellContent::from_char('A');
1262 assert_eq!(c.raw(), 'A' as u32);
1263
1264 let g = CellContent::from_grapheme(GraphemeId::new(5, 2, 1));
1265 assert_ne!(g.raw() & 0x8000_0000, 0);
1266 }
1267
1268 #[test]
1271 fn cell_attrs_default_is_none() {
1272 assert_eq!(CellAttrs::default(), CellAttrs::NONE);
1273 }
1274
1275 #[test]
1276 fn cell_attrs_each_flag_isolated() {
1277 let all_flags = [
1278 StyleFlags::BOLD,
1279 StyleFlags::DIM,
1280 StyleFlags::ITALIC,
1281 StyleFlags::UNDERLINE,
1282 StyleFlags::BLINK,
1283 StyleFlags::REVERSE,
1284 StyleFlags::STRIKETHROUGH,
1285 StyleFlags::HIDDEN,
1286 ];
1287
1288 for &flag in &all_flags {
1289 let a = CellAttrs::new(flag, 0);
1290 assert!(a.has_flag(flag), "flag {:?} should be set", flag);
1291
1292 for &other in &all_flags {
1294 if other != flag {
1295 assert!(
1296 !a.has_flag(other),
1297 "flag {:?} should NOT be set when only {:?} is",
1298 other,
1299 flag
1300 );
1301 }
1302 }
1303 }
1304 }
1305
1306 #[test]
1307 fn cell_attrs_all_flags_combined() {
1308 let all = StyleFlags::BOLD
1309 | StyleFlags::DIM
1310 | StyleFlags::ITALIC
1311 | StyleFlags::UNDERLINE
1312 | StyleFlags::BLINK
1313 | StyleFlags::REVERSE
1314 | StyleFlags::STRIKETHROUGH
1315 | StyleFlags::HIDDEN;
1316 let a = CellAttrs::new(all, 42);
1317 assert_eq!(a.flags(), all);
1318 assert!(a.has_flag(StyleFlags::BOLD));
1319 assert!(a.has_flag(StyleFlags::HIDDEN));
1320 assert_eq!(a.link_id(), 42);
1321 }
1322
1323 #[test]
1324 fn cell_attrs_link_id_zero() {
1325 let a = CellAttrs::new(StyleFlags::BOLD, CellAttrs::LINK_ID_NONE);
1326 assert_eq!(a.link_id(), 0);
1327 assert!(a.has_flag(StyleFlags::BOLD));
1328 }
1329
1330 #[test]
1331 fn cell_attrs_with_link_to_none() {
1332 let a = CellAttrs::new(StyleFlags::ITALIC, 500);
1333 let b = a.with_link(CellAttrs::LINK_ID_NONE);
1334 assert_eq!(b.link_id(), 0);
1335 assert!(b.has_flag(StyleFlags::ITALIC));
1336 }
1337
1338 #[test]
1339 fn cell_attrs_with_flags_to_empty() {
1340 let a = CellAttrs::new(StyleFlags::BOLD | StyleFlags::ITALIC, 123);
1341 let b = a.with_flags(StyleFlags::empty());
1342 assert!(b.flags().is_empty());
1343 assert_eq!(b.link_id(), 123);
1344 }
1345
1346 #[test]
1349 fn cell_bits_eq_detects_bg_difference() {
1350 let cell1 = Cell::from_char('X');
1351 let cell2 = Cell::from_char('X').with_bg(PackedRgba::RED);
1352 assert!(!cell1.bits_eq(&cell2));
1353 }
1354
1355 #[test]
1356 fn cell_bits_eq_detects_attrs_difference() {
1357 let cell1 = Cell::from_char('X');
1358 let cell2 = Cell::from_char('X').with_attrs(CellAttrs::new(StyleFlags::BOLD, 0));
1359 assert!(!cell1.bits_eq(&cell2));
1360 }
1361
1362 #[test]
1363 fn cell_with_char_preserves_colors_and_attrs() {
1364 let cell = Cell::from_char('A')
1365 .with_fg(PackedRgba::RED)
1366 .with_bg(PackedRgba::BLUE)
1367 .with_attrs(CellAttrs::new(StyleFlags::BOLD, 42));
1368
1369 let updated = cell.with_char('Z');
1370 assert_eq!(updated.content.as_char(), Some('Z'));
1371 assert_eq!(updated.fg, PackedRgba::RED);
1372 assert_eq!(updated.bg, PackedRgba::BLUE);
1373 assert!(updated.attrs.has_flag(StyleFlags::BOLD));
1374 assert_eq!(updated.attrs.link_id(), 42);
1375 }
1376
1377 #[test]
1378 fn cell_new_vs_from_char() {
1379 let a = Cell::new(CellContent::from_char('A'));
1380 let b = Cell::from_char('A');
1381 assert!(a.bits_eq(&b));
1382 }
1383
1384 #[test]
1385 fn cell_continuation_has_transparent_colors() {
1386 assert_eq!(Cell::CONTINUATION.fg, PackedRgba::TRANSPARENT);
1387 assert_eq!(Cell::CONTINUATION.bg, PackedRgba::TRANSPARENT);
1388 assert_eq!(Cell::CONTINUATION.attrs, CellAttrs::NONE);
1389 }
1390
1391 #[test]
1392 fn cell_debug_format() {
1393 let cell = Cell::from_char('A');
1394 let s = format!("{:?}", cell);
1395 assert!(s.contains("Cell"), "got: {s}");
1396 assert!(s.contains("content"), "got: {s}");
1397 assert!(s.contains("fg"), "got: {s}");
1398 assert!(s.contains("bg"), "got: {s}");
1399 assert!(s.contains("attrs"), "got: {s}");
1400 }
1401
1402 #[test]
1403 fn cell_is_empty_for_various() {
1404 assert!(Cell::default().is_empty());
1405 assert!(!Cell::from_char('A').is_empty());
1406 assert!(!Cell::CONTINUATION.is_empty());
1407 }
1408
1409 #[test]
1410 fn cell_is_continuation_for_various() {
1411 assert!(!Cell::default().is_continuation());
1412 assert!(!Cell::from_char('A').is_continuation());
1413 assert!(Cell::CONTINUATION.is_continuation());
1414 }
1415
1416 #[test]
1417 fn cell_width_hint_for_grapheme() {
1418 let id = GraphemeId::new(100, 0, 3);
1419 let cell = Cell::new(CellContent::from_grapheme(id));
1420 assert_eq!(cell.width_hint(), 3);
1421 }
1422
1423 #[test]
1426 fn grapheme_id_default() {
1427 let id = GraphemeId::default();
1428 assert_eq!(id.slot(), 0);
1429 assert_eq!(id.generation(), 0);
1430 assert_eq!(id.width(), 0);
1431 }
1432
1433 #[test]
1434 fn grapheme_id_debug_format() {
1435 let id = GraphemeId::new(42, 5, 2);
1436 let s = format!("{:?}", id);
1437 assert!(s.contains("GraphemeId"), "got: {s}");
1438 assert!(s.contains("42"), "got: {s}");
1439 assert!(s.contains("5"), "got: {s}");
1440 assert!(s.contains("2"), "got: {s}");
1441 }
1442
1443 #[test]
1444 fn grapheme_id_width_isolated_from_slot() {
1445 let id = GraphemeId::new(GraphemeId::MAX_SLOT, 0, 0);
1447 assert_eq!(id.width(), 0);
1448 assert_eq!(id.slot(), 0xFFFF);
1449
1450 let id2 = GraphemeId::new(0, 0, GraphemeId::MAX_WIDTH);
1451 assert_eq!(id2.slot(), 0);
1452 assert_eq!(id2.width(), GraphemeId::MAX_WIDTH as usize);
1453 }
1454
1455 #[test]
1458 fn style_flags_empty_has_no_bits() {
1459 assert!(StyleFlags::empty().is_empty());
1460 assert_eq!(StyleFlags::empty().bits(), 0);
1461 }
1462
1463 #[test]
1464 fn style_flags_all_has_all_bits() {
1465 let all = StyleFlags::all();
1466 assert!(all.contains(StyleFlags::BOLD));
1467 assert!(all.contains(StyleFlags::DIM));
1468 assert!(all.contains(StyleFlags::ITALIC));
1469 assert!(all.contains(StyleFlags::UNDERLINE));
1470 assert!(all.contains(StyleFlags::BLINK));
1471 assert!(all.contains(StyleFlags::REVERSE));
1472 assert!(all.contains(StyleFlags::STRIKETHROUGH));
1473 assert!(all.contains(StyleFlags::HIDDEN));
1474 }
1475
1476 #[test]
1477 fn style_flags_union_and_intersection() {
1478 let a = StyleFlags::BOLD | StyleFlags::ITALIC;
1479 let b = StyleFlags::ITALIC | StyleFlags::UNDERLINE;
1480 assert_eq!(
1481 a | b,
1482 StyleFlags::BOLD | StyleFlags::ITALIC | StyleFlags::UNDERLINE
1483 );
1484 assert_eq!(a & b, StyleFlags::ITALIC);
1485 }
1486
1487 #[test]
1488 fn style_flags_from_bits_truncate() {
1489 let all = StyleFlags::from_bits_truncate(0xFF);
1491 assert_eq!(all, StyleFlags::all());
1492
1493 let none = StyleFlags::from_bits_truncate(0x00);
1495 assert!(none.is_empty());
1496 }
1497
1498 #[test]
1503 fn over_not_commutative() {
1504 let red_half = PackedRgba::rgba(255, 0, 0, 128);
1505 let blue_half = PackedRgba::rgba(0, 0, 255, 128);
1506 let a_over_b = red_half.over(blue_half);
1507 let b_over_a = blue_half.over(red_half);
1508 assert_ne!(a_over_b, b_over_a);
1510 }
1511
1512 #[test]
1513 fn over_opaque_self_compositing_is_idempotent() {
1514 let c = PackedRgba::rgba(42, 84, 168, 255);
1515 assert_eq!(c.over(c), c);
1516 }
1517
1518 #[test]
1519 fn over_near_opaque_src_alpha_254() {
1520 let src = PackedRgba::rgba(255, 0, 0, 254);
1522 let dst = PackedRgba::rgba(0, 0, 255, 255);
1523 let result = src.over(dst);
1524 assert_eq!(result.a(), 255);
1525 assert!(result.r() >= 253, "r={}", result.r());
1527 assert!(result.b() <= 2, "b={}", result.b());
1528 }
1529
1530 #[test]
1531 fn over_both_partial_alpha_symmetric_colors() {
1532 let c = PackedRgba::rgba(200, 100, 50, 128);
1534 let result = c.over(c);
1535 let ref_result = reference_over(c, c);
1536 assert_eq!(result, ref_result);
1537 assert!(result.a() >= 190 && result.a() <= 194, "a={}", result.a());
1539 }
1540
1541 #[test]
1542 fn over_both_alpha_1_minimal() {
1543 let src = PackedRgba::rgba(255, 255, 255, 1);
1544 let dst = PackedRgba::rgba(0, 0, 0, 1);
1545 let result = src.over(dst);
1546 let ref_result = reference_over(src, dst);
1547 assert_eq!(result, ref_result);
1548 assert!(result.a() <= 3, "a={}", result.a());
1550 }
1551
1552 #[test]
1553 fn over_white_alpha_0_over_opaque_is_dst() {
1554 let src = PackedRgba::rgba(255, 255, 255, 0);
1556 let dst = PackedRgba::rgba(100, 50, 25, 255);
1557 assert_eq!(src.over(dst), dst);
1558 }
1559
1560 #[test]
1561 fn with_opacity_nan_clamps_to_zero() {
1562 let c = PackedRgba::rgba(10, 20, 30, 200);
1563 let result = c.with_opacity(f32::NAN);
1564 assert_eq!(result.r(), 10);
1568 assert_eq!(result.g(), 20);
1569 assert_eq!(result.b(), 30);
1570 }
1571
1572 #[test]
1573 fn with_opacity_negative_infinity_clamps_to_zero() {
1574 let c = PackedRgba::rgba(10, 20, 30, 200);
1575 let result = c.with_opacity(f32::NEG_INFINITY);
1576 assert_eq!(result.a(), 0);
1577 }
1578
1579 #[test]
1580 fn with_opacity_positive_infinity_clamps_to_original() {
1581 let c = PackedRgba::rgba(10, 20, 30, 200);
1582 let result = c.with_opacity(f32::INFINITY);
1583 assert_eq!(result.a(), 200);
1584 }
1585
1586 #[test]
1587 fn with_opacity_on_transparent_stays_transparent() {
1588 let c = PackedRgba::TRANSPARENT;
1589 assert_eq!(c.with_opacity(0.5).a(), 0);
1590 assert_eq!(c.with_opacity(1.0).a(), 0);
1591 }
1592
1593 #[test]
1594 fn packed_rgba_extreme_channel_values() {
1595 let all_max = PackedRgba::rgba(255, 255, 255, 255);
1596 assert_eq!(all_max.r(), 255);
1597 assert_eq!(all_max.g(), 255);
1598 assert_eq!(all_max.b(), 255);
1599 assert_eq!(all_max.a(), 255);
1600
1601 let all_zero = PackedRgba::rgba(0, 0, 0, 0);
1602 assert_eq!(all_zero, PackedRgba::TRANSPARENT);
1603 }
1604
1605 #[test]
1606 fn packed_rgba_hash_differs_for_different_values() {
1607 use std::collections::HashSet;
1608 let mut set = HashSet::new();
1609 set.insert(PackedRgba::RED);
1610 set.insert(PackedRgba::GREEN);
1611 set.insert(PackedRgba::BLUE);
1612 set.insert(PackedRgba::RED); assert_eq!(set.len(), 3);
1614 }
1615
1616 #[test]
1617 fn packed_rgba_channel_isolation() {
1618 let base = PackedRgba::rgba(10, 20, 30, 40);
1620 let different_r = PackedRgba::rgba(99, 20, 30, 40);
1621 assert_ne!(base, different_r);
1622 assert_eq!(base.g(), different_r.g());
1623 assert_eq!(base.b(), different_r.b());
1624 assert_eq!(base.a(), different_r.a());
1625 }
1626
1627 #[test]
1630 fn cell_content_nul_char_equals_empty() {
1631 let nul = CellContent::from_char('\0');
1633 assert_eq!(nul.raw(), CellContent::EMPTY.raw());
1634 assert!(nul.is_empty());
1635 assert_eq!(nul.as_char(), None); }
1637
1638 #[test]
1639 fn cell_content_soh_char_is_not_continuation() {
1640 let soh = CellContent::from_char('\x01');
1641 assert_eq!(soh.raw(), 1);
1642 assert!(!soh.is_empty());
1643 assert!(!soh.is_continuation());
1644 assert_eq!(soh.as_char(), Some('\x01'));
1645 }
1646
1647 #[test]
1648 fn cell_content_max_unicode_codepoint() {
1649 let max = CellContent::from_char('\u{10FFFF}');
1650 assert_eq!(max.as_char(), Some('\u{10FFFF}'));
1651 assert!(!max.is_grapheme());
1652 assert_eq!(max.width_hint(), 1);
1654 }
1655
1656 #[test]
1657 fn cell_content_bmp_boundary_chars() {
1658 let last_before_surrogates = CellContent::from_char('\u{D7FF}');
1660 assert_eq!(last_before_surrogates.as_char(), Some('\u{D7FF}'));
1661
1662 let first_after_surrogates = CellContent::from_char('\u{E000}');
1664 assert_eq!(first_after_surrogates.as_char(), Some('\u{E000}'));
1665
1666 let supplementary = CellContent::from_char('\u{10000}');
1668 assert_eq!(supplementary.as_char(), Some('\u{10000}'));
1669 assert!(!supplementary.is_grapheme()); }
1671
1672 #[test]
1673 fn cell_content_grapheme_with_zero_width() {
1674 let id = GraphemeId::new(42, 0, 0);
1675 let c = CellContent::from_grapheme(id);
1676 assert_eq!(c.width_hint(), 0);
1677 assert_eq!(c.width(), 0);
1678 assert!(c.is_grapheme());
1679 }
1680
1681 #[test]
1682 fn cell_content_grapheme_with_max_width() {
1683 let id = GraphemeId::new(1, 0, GraphemeId::MAX_WIDTH);
1684 let c = CellContent::from_grapheme(id);
1685 assert_eq!(c.width_hint(), GraphemeId::MAX_WIDTH as usize);
1686 assert_eq!(c.width(), GraphemeId::MAX_WIDTH as usize);
1687 }
1688
1689 #[test]
1690 fn cell_content_continuation_value_is_max_i31() {
1691 assert_eq!(CellContent::CONTINUATION.raw(), 0x7FFF_FFFF);
1693 assert!(!CellContent::CONTINUATION.is_grapheme()); assert!(CellContent::CONTINUATION.is_continuation());
1695 }
1696
1697 #[test]
1698 fn cell_content_empty_and_continuation_are_distinct() {
1699 assert_ne!(CellContent::EMPTY, CellContent::CONTINUATION);
1700 assert!(CellContent::EMPTY.is_empty());
1701 assert!(!CellContent::EMPTY.is_continuation());
1702 assert!(!CellContent::CONTINUATION.is_empty());
1703 assert!(CellContent::CONTINUATION.is_continuation());
1704 }
1705
1706 #[test]
1707 fn cell_content_grapheme_id_strips_high_bit() {
1708 let id = GraphemeId::new(
1709 GraphemeId::MAX_SLOT,
1710 GraphemeId::MAX_GENERATION,
1711 GraphemeId::MAX_WIDTH,
1712 );
1713 let c = CellContent::from_grapheme(id);
1714 let extracted = c.grapheme_id().unwrap();
1715 assert_eq!(extracted.slot(), id.slot());
1716 assert_eq!(extracted.generation(), id.generation());
1717 assert_eq!(extracted.width(), id.width());
1718 }
1719
1720 #[test]
1723 fn grapheme_id_slot_one_width_one() {
1724 let id = GraphemeId::new(1, 0, 1);
1725 assert_eq!(id.slot(), 1);
1726 assert_eq!(id.width(), 1);
1727 }
1728
1729 #[test]
1730 fn grapheme_id_hash_eq_consistency() {
1731 use std::collections::HashSet;
1732 let a = GraphemeId::new(42, 0, 2);
1733 let b = GraphemeId::new(42, 0, 2);
1734 let c = GraphemeId::new(42, 0, 3);
1735 let d = GraphemeId::new(42, 1, 2);
1736 assert_eq!(a, b);
1737 assert_ne!(a, c);
1738 assert_ne!(a, d);
1739 let mut set = HashSet::new();
1740 set.insert(a);
1741 assert!(set.contains(&b));
1742 assert!(!set.contains(&c));
1743 assert!(!set.contains(&d));
1744 }
1745
1746 #[test]
1747 fn grapheme_id_adjacent_slots_differ() {
1748 let a = GraphemeId::new(0, 0, 1);
1749 let b = GraphemeId::new(1, 0, 1);
1750 assert_ne!(a, b);
1751 assert_ne!(a.slot(), b.slot());
1752 assert_eq!(a.width(), b.width());
1753 }
1754
1755 #[test]
1758 fn cell_attrs_link_id_masks_overflow() {
1759 let a = CellAttrs::new(StyleFlags::empty(), 0x00FF_FFFE);
1761 assert_eq!(a.link_id(), 0x00FF_FFFE);
1762 }
1763
1764 #[test]
1765 fn cell_attrs_chained_mutations() {
1766 let a = CellAttrs::new(StyleFlags::BOLD, 100)
1767 .with_flags(StyleFlags::ITALIC)
1768 .with_link(200)
1769 .with_flags(StyleFlags::UNDERLINE | StyleFlags::DIM)
1770 .with_link(300);
1771 assert_eq!(a.flags(), StyleFlags::UNDERLINE | StyleFlags::DIM);
1772 assert_eq!(a.link_id(), 300);
1773 }
1774
1775 #[test]
1776 fn cell_attrs_all_flags_max_link() {
1777 let all_flags = StyleFlags::all();
1778 let a = CellAttrs::new(all_flags, CellAttrs::LINK_ID_MAX);
1779 assert_eq!(a.flags(), all_flags);
1780 assert_eq!(a.link_id(), CellAttrs::LINK_ID_MAX);
1781 assert_eq!(a.flags().bits(), 0xFF);
1783 assert_eq!(a.link_id(), 0x00FF_FFFE);
1784 }
1785
1786 #[test]
1787 fn cell_attrs_link_id_none_is_zero() {
1788 assert_eq!(CellAttrs::LINK_ID_NONE, 0);
1789 }
1790
1791 #[test]
1794 fn cell_eq_matches_bits_eq() {
1795 let pairs = [
1796 (Cell::default(), Cell::default()),
1797 (Cell::from_char('A'), Cell::from_char('A')),
1798 (Cell::from_char('A'), Cell::from_char('B')),
1799 (Cell::CONTINUATION, Cell::CONTINUATION),
1800 (
1801 Cell::from_char('X').with_fg(PackedRgba::RED),
1802 Cell::from_char('X').with_fg(PackedRgba::BLUE),
1803 ),
1804 ];
1805 for (a, b) in &pairs {
1806 assert_eq!(
1807 a == b,
1808 a.bits_eq(b),
1809 "PartialEq and bits_eq disagree for {:?} vs {:?}",
1810 a,
1811 b
1812 );
1813 }
1814 }
1815
1816 #[test]
1817 fn cell_from_grapheme_content() {
1818 let id = GraphemeId::new(42, 0, 2);
1819 let cell = Cell::new(CellContent::from_grapheme(id));
1820 assert!(cell.content.is_grapheme());
1821 assert_eq!(cell.width_hint(), 2);
1822 assert!(!cell.is_empty());
1823 assert!(!cell.is_continuation());
1824 }
1825
1826 #[test]
1827 fn cell_with_char_on_continuation() {
1828 let cell = Cell::CONTINUATION.with_char('A');
1829 assert_eq!(cell.content.as_char(), Some('A'));
1830 assert!(!cell.is_continuation());
1831 assert_eq!(cell.fg, PackedRgba::TRANSPARENT);
1833 assert_eq!(cell.bg, PackedRgba::TRANSPARENT);
1834 }
1835
1836 #[test]
1837 fn cell_default_bits_eq_self() {
1838 let cell = Cell::default();
1839 assert!(cell.bits_eq(&cell));
1840 }
1841
1842 #[test]
1843 fn cell_new_empty_equals_default() {
1844 let a = Cell::new(CellContent::EMPTY);
1845 let b = Cell::default();
1846 assert!(a.bits_eq(&b));
1847 }
1848
1849 #[test]
1850 fn cell_all_builder_methods_chain() {
1851 let cell = Cell::default()
1852 .with_char('Z')
1853 .with_fg(PackedRgba::rgba(1, 2, 3, 4))
1854 .with_bg(PackedRgba::rgba(5, 6, 7, 8))
1855 .with_attrs(CellAttrs::new(
1856 StyleFlags::BOLD | StyleFlags::STRIKETHROUGH,
1857 999,
1858 ));
1859 assert_eq!(cell.content.as_char(), Some('Z'));
1860 assert_eq!(cell.fg.r(), 1);
1861 assert_eq!(cell.bg.a(), 8);
1862 assert!(cell.attrs.has_flag(StyleFlags::BOLD));
1863 assert!(cell.attrs.has_flag(StyleFlags::STRIKETHROUGH));
1864 assert!(!cell.attrs.has_flag(StyleFlags::ITALIC));
1865 assert_eq!(cell.attrs.link_id(), 999);
1866 }
1867
1868 #[test]
1869 fn cell_size_and_alignment_invariants() {
1870 assert_eq!(core::mem::size_of::<Cell>(), 16);
1872 assert_eq!(core::mem::align_of::<Cell>(), 16);
1873 assert_eq!(64 / core::mem::size_of::<Cell>(), 4);
1875 }
1876
1877 #[test]
1878 fn cell_content_size_invariant() {
1879 assert_eq!(core::mem::size_of::<CellContent>(), 4);
1880 }
1881
1882 #[test]
1883 fn cell_attrs_size_invariant() {
1884 assert_eq!(core::mem::size_of::<CellAttrs>(), 4);
1885 }
1886
1887 #[test]
1890 fn over_associativity_approximate() {
1891 let a = PackedRgba::rgba(200, 50, 100, 128);
1894 let b = PackedRgba::rgba(50, 200, 50, 128);
1895 let c = PackedRgba::rgba(100, 100, 200, 128);
1896
1897 let ab_c = a.over(b).over(c);
1898 let a_bc = a.over(b.over(c));
1899
1900 assert!(
1902 (ab_c.r() as i16 - a_bc.r() as i16).unsigned_abs() <= 1,
1903 "r: {} vs {}",
1904 ab_c.r(),
1905 a_bc.r()
1906 );
1907 assert!(
1908 (ab_c.g() as i16 - a_bc.g() as i16).unsigned_abs() <= 1,
1909 "g: {} vs {}",
1910 ab_c.g(),
1911 a_bc.g()
1912 );
1913 assert!(
1914 (ab_c.b() as i16 - a_bc.b() as i16).unsigned_abs() <= 1,
1915 "b: {} vs {}",
1916 ab_c.b(),
1917 a_bc.b()
1918 );
1919 assert!(
1920 (ab_c.a() as i16 - a_bc.a() as i16).unsigned_abs() <= 1,
1921 "a: {} vs {}",
1922 ab_c.a(),
1923 a_bc.a()
1924 );
1925 }
1926
1927 #[test]
1928 fn over_output_alpha_monotonic_with_src_alpha() {
1929 let dst = PackedRgba::rgba(0, 0, 255, 128);
1931 let mut prev_a = 0u8;
1932 for alpha in (0..=255).step_by(5) {
1933 let src = PackedRgba::rgba(255, 0, 0, alpha);
1934 let result = src.over(dst);
1935 assert!(
1936 result.a() >= prev_a,
1937 "alpha monotonicity violated at src_a={}: result_a={} < prev={}",
1938 alpha,
1939 result.a(),
1940 prev_a
1941 );
1942 prev_a = result.a();
1943 }
1944 }
1945
1946 #[test]
1947 fn over_sweep_matches_reference() {
1948 for alpha in (0..=255).step_by(17) {
1950 let src = PackedRgba::rgba(200, 100, 50, alpha);
1951 let dst = PackedRgba::rgba(50, 100, 200, 200);
1952 assert_eq!(
1953 src.over(dst),
1954 reference_over(src, dst),
1955 "mismatch at src_alpha={}",
1956 alpha
1957 );
1958 }
1959 }
1960}
1961
1962#[cfg(test)]
1967mod cell_proptests {
1968 use super::{Cell, CellAttrs, CellContent, GraphemeId, PackedRgba, StyleFlags};
1969 use proptest::prelude::*;
1970
1971 fn arb_packed_rgba() -> impl Strategy<Value = PackedRgba> {
1972 (any::<u8>(), any::<u8>(), any::<u8>(), any::<u8>())
1973 .prop_map(|(r, g, b, a)| PackedRgba::rgba(r, g, b, a))
1974 }
1975
1976 fn arb_grapheme_id() -> impl Strategy<Value = GraphemeId> {
1977 (
1978 0u32..=GraphemeId::MAX_SLOT,
1979 0u16..=GraphemeId::MAX_GENERATION,
1980 0u8..=GraphemeId::MAX_WIDTH,
1981 )
1982 .prop_map(|(slot, generation, width)| GraphemeId::new(slot, generation, width))
1983 }
1984
1985 fn arb_style_flags() -> impl Strategy<Value = StyleFlags> {
1986 any::<u8>().prop_map(StyleFlags::from_bits_truncate)
1987 }
1988
1989 proptest! {
1990 #[test]
1991 fn packed_rgba_roundtrips_all_components(tuple in (any::<u8>(), any::<u8>(), any::<u8>(), any::<u8>())) {
1992 let (r, g, b, a) = tuple;
1993 let c = PackedRgba::rgba(r, g, b, a);
1994 prop_assert_eq!(c.r(), r);
1995 prop_assert_eq!(c.g(), g);
1996 prop_assert_eq!(c.b(), b);
1997 prop_assert_eq!(c.a(), a);
1998 }
1999
2000 #[test]
2001 fn packed_rgba_rgb_always_opaque(tuple in (any::<u8>(), any::<u8>(), any::<u8>())) {
2002 let (r, g, b) = tuple;
2003 let c = PackedRgba::rgb(r, g, b);
2004 prop_assert_eq!(c.a(), 255);
2005 prop_assert_eq!(c.r(), r);
2006 prop_assert_eq!(c.g(), g);
2007 prop_assert_eq!(c.b(), b);
2008 }
2009
2010 #[test]
2011 fn packed_rgba_over_identity_transparent(dst in arb_packed_rgba()) {
2012 let result = PackedRgba::TRANSPARENT.over(dst);
2014 prop_assert_eq!(result, dst);
2015 }
2016
2017 #[test]
2018 fn packed_rgba_over_identity_opaque(tuple in (any::<u8>(), any::<u8>(), any::<u8>(), arb_packed_rgba())) {
2019 let (r, g, b, dst) = tuple;
2021 let src = PackedRgba::rgba(r, g, b, 255);
2022 let result = src.over(dst);
2023 prop_assert_eq!(result, src);
2024 }
2025
2026 #[test]
2027 fn grapheme_id_components_roundtrip(
2028 tuple in (
2029 0u32..=GraphemeId::MAX_SLOT,
2030 0u16..=GraphemeId::MAX_GENERATION,
2031 0u8..=GraphemeId::MAX_WIDTH,
2032 )
2033 ) {
2034 let (slot, generation, width) = tuple;
2035 let id = GraphemeId::new(slot, generation, width);
2036 prop_assert_eq!(id.slot(), slot as usize);
2037 prop_assert_eq!(id.generation(), generation);
2038 prop_assert_eq!(id.width(), width as usize);
2039 }
2040
2041 #[test]
2042 fn grapheme_id_raw_roundtrip(id in arb_grapheme_id()) {
2043 let raw = id.raw();
2044 let restored = GraphemeId::from_raw(raw);
2045 prop_assert_eq!(restored.slot(), id.slot());
2046 prop_assert_eq!(restored.width(), id.width());
2047 }
2048
2049 #[test]
2050 fn cell_content_char_roundtrip(c in (0x20u32..0xD800u32).prop_union(0xE000u32..0x110000u32)) {
2051 if let Some(ch) = char::from_u32(c) {
2052 let content = CellContent::from_char(ch);
2053 prop_assert_eq!(content.as_char(), Some(ch));
2054 prop_assert!(!content.is_grapheme());
2055 prop_assert!(!content.is_empty());
2056 prop_assert!(!content.is_continuation());
2057 }
2058 }
2059
2060 #[test]
2061 fn cell_content_grapheme_roundtrip(id in arb_grapheme_id()) {
2062 let content = CellContent::from_grapheme(id);
2063 prop_assert!(content.is_grapheme());
2064 prop_assert_eq!(content.grapheme_id(), Some(id));
2065 prop_assert_eq!(content.width_hint(), id.width());
2066 }
2067
2068 #[test]
2069 fn cell_bits_eq_is_reflexive(
2070 tuple in (
2071 (0x20u32..0x80u32).prop_map(|c| char::from_u32(c).unwrap()),
2072 any::<u8>(), any::<u8>(), any::<u8>(),
2073 arb_style_flags(),
2074 ),
2075 ) {
2076 let (c, r, g, b, flags) = tuple;
2077 let cell = Cell::from_char(c)
2078 .with_fg(PackedRgba::rgb(r, g, b))
2079 .with_attrs(CellAttrs::new(flags, 0));
2080 prop_assert!(cell.bits_eq(&cell));
2081 }
2082
2083 #[test]
2084 fn cell_bits_eq_detects_fg_difference(
2085 tuple in (
2086 (0x41u32..0x5Bu32).prop_map(|c| char::from_u32(c).unwrap()),
2087 any::<u8>(), any::<u8>(),
2088 ),
2089 ) {
2090 let (c, r1, r2) = tuple;
2091 prop_assume!(r1 != r2);
2092 let cell1 = Cell::from_char(c).with_fg(PackedRgba::rgb(r1, 0, 0));
2093 let cell2 = Cell::from_char(c).with_fg(PackedRgba::rgb(r2, 0, 0));
2094 prop_assert!(!cell1.bits_eq(&cell2));
2095 }
2096
2097 #[test]
2098 fn cell_attrs_flags_roundtrip(tuple in (arb_style_flags(), 0u32..CellAttrs::LINK_ID_MAX)) {
2099 let (flags, link) = tuple;
2100 let attrs = CellAttrs::new(flags, link);
2101 prop_assert_eq!(attrs.flags(), flags);
2102 prop_assert_eq!(attrs.link_id(), link);
2103 }
2104
2105 #[test]
2106 fn cell_attrs_with_flags_preserves_link(tuple in (arb_style_flags(), 0u32..CellAttrs::LINK_ID_MAX, arb_style_flags())) {
2107 let (flags, link, new_flags) = tuple;
2108 let attrs = CellAttrs::new(flags, link);
2109 let updated = attrs.with_flags(new_flags);
2110 prop_assert_eq!(updated.flags(), new_flags);
2111 prop_assert_eq!(updated.link_id(), link);
2112 }
2113
2114 #[test]
2115 fn cell_attrs_with_link_preserves_flags(tuple in (arb_style_flags(), 0u32..CellAttrs::LINK_ID_MAX, 0u32..CellAttrs::LINK_ID_MAX)) {
2116 let (flags, link1, link2) = tuple;
2117 let attrs = CellAttrs::new(flags, link1);
2118 let updated = attrs.with_link(link2);
2119 prop_assert_eq!(updated.flags(), flags);
2120 prop_assert_eq!(updated.link_id(), link2);
2121 }
2122
2123 #[test]
2126 fn cell_bits_eq_is_symmetric(
2127 tuple in (
2128 (0x41u32..0x5Bu32).prop_map(|c| char::from_u32(c).unwrap()),
2129 (0x41u32..0x5Bu32).prop_map(|c| char::from_u32(c).unwrap()),
2130 arb_packed_rgba(),
2131 arb_packed_rgba(),
2132 ),
2133 ) {
2134 let (c1, c2, fg1, fg2) = tuple;
2135 let cell_a = Cell::from_char(c1).with_fg(fg1);
2136 let cell_b = Cell::from_char(c2).with_fg(fg2);
2137 prop_assert_eq!(cell_a.bits_eq(&cell_b), cell_b.bits_eq(&cell_a),
2138 "bits_eq is not symmetric");
2139 }
2140
2141 #[test]
2142 fn cell_content_bit31_discriminates(id in arb_grapheme_id()) {
2143 let char_content = CellContent::from_char('A');
2145 prop_assert!(!char_content.is_grapheme());
2146 prop_assert!(char_content.as_char().is_some());
2147 prop_assert!(char_content.grapheme_id().is_none());
2148
2149 let grapheme_content = CellContent::from_grapheme(id);
2151 prop_assert!(grapheme_content.is_grapheme());
2152 prop_assert!(grapheme_content.grapheme_id().is_some());
2153 prop_assert!(grapheme_content.as_char().is_none());
2154 }
2155
2156 #[test]
2157 fn cell_from_char_width_matches_unicode(
2158 c in (0x20u32..0x7Fu32).prop_map(|c| char::from_u32(c).unwrap()),
2159 ) {
2160 let cell = Cell::from_char(c);
2161 prop_assert_eq!(cell.width_hint(), 1,
2162 "Cell width hint for '{}' should be 1 for ASCII", c);
2163 }
2164 }
2165
2166 #[test]
2169 fn cell_content_continuation_has_zero_width() {
2170 let cont = CellContent::CONTINUATION;
2171 assert_eq!(cont.width(), 0, "CONTINUATION cell should have width 0");
2172 assert!(cont.is_continuation());
2173 assert!(!cont.is_grapheme());
2174 }
2175
2176 #[test]
2177 fn cell_content_empty_has_zero_width() {
2178 let empty = CellContent::EMPTY;
2179 assert_eq!(empty.width(), 0, "EMPTY cell should have width 0");
2180 assert!(empty.is_empty());
2181 assert!(!empty.is_grapheme());
2182 assert!(!empty.is_continuation());
2183 }
2184
2185 #[test]
2186 fn cell_default_is_empty() {
2187 let cell = Cell::default();
2188 assert!(cell.is_empty());
2189 assert_eq!(cell.width_hint(), 0);
2190 }
2191}
2192
2193#[cfg(test)]
2194mod bit_layout_tests {
2195 use super::GraphemeId;
2196
2197 #[test]
2198 fn grapheme_id_bit_layout_verification() {
2199 let t1 = GraphemeId::new(0xFFFF, 0, 0);
2204 assert_eq!(t1.raw(), 0xFFFF, "Slot 0xFFFF should be 0xFFFF");
2205
2206 let t2 = GraphemeId::new(0, 0x7FF, 0);
2208 assert_eq!(t2.raw(), 0x07FF0000, "Gen 0x7FF should be 0x07FF0000");
2210
2211 let t3 = GraphemeId::new(0, 0, 0xF);
2213 assert_eq!(t3.raw(), 0x78000000, "Width 0xF should be 0x78000000");
2215
2216 let t4 = GraphemeId::new(0xFFFF, 0x7FF, 0xF);
2218 assert_eq!(
2220 t4.raw(),
2221 0x7FFFFFFF,
2222 "Combined max values should be 0x7FFFFFFF"
2223 );
2224
2225 assert_eq!(t4.raw() & 0x80000000, 0, "Bit 31 must be clear");
2227 }
2228}