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