#![forbid(unsafe_code)]
use crate::char_width;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
#[repr(transparent)]
pub struct GraphemeId(u32);
impl GraphemeId {
pub const MAX_SLOT: u32 = 0xFFFF;
pub const MAX_WIDTH: u8 = 15;
pub const MAX_GENERATION: u16 = 2047;
#[inline]
pub const fn new(slot: u32, generation: u16, width: u8) -> Self {
debug_assert!(slot <= Self::MAX_SLOT, "slot overflow");
debug_assert!(generation <= Self::MAX_GENERATION, "generation overflow");
debug_assert!(width <= Self::MAX_WIDTH, "width overflow");
Self(
(slot & Self::MAX_SLOT)
| (((generation as u32) & 0x7FF) << 16)
| ((width as u32) << 27),
)
}
#[inline]
pub const fn slot(self) -> usize {
(self.0 & Self::MAX_SLOT) as usize
}
#[inline]
pub const fn generation(self) -> u16 {
((self.0 >> 16) & 0x7FF) as u16
}
#[inline]
pub const fn width(self) -> usize {
((self.0 >> 27) & 0x0F) as usize
}
#[inline]
pub const fn raw(self) -> u32 {
self.0
}
#[inline]
pub const fn from_raw(raw: u32) -> Self {
Self(raw)
}
}
impl core::fmt::Debug for GraphemeId {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("GraphemeId")
.field("slot", &self.slot())
.field("gen", &self.generation())
.field("width", &self.width())
.finish()
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
#[repr(transparent)]
pub struct CellContent(u32);
impl CellContent {
pub const EMPTY: Self = Self(0);
pub const CONTINUATION: Self = Self(0x7FFF_FFFF);
#[inline]
pub const fn from_char(c: char) -> Self {
if c == '\t' {
Self(' ' as u32)
} else {
Self(c as u32)
}
}
#[inline]
pub const fn from_grapheme(id: GraphemeId) -> Self {
Self(0x8000_0000 | id.raw())
}
#[inline]
pub const fn is_grapheme(self) -> bool {
self.0 & 0x8000_0000 != 0
}
#[inline]
pub const fn is_continuation(self) -> bool {
self.0 == Self::CONTINUATION.0
}
#[inline]
pub const fn is_empty(self) -> bool {
self.0 == Self::EMPTY.0
}
#[inline]
pub const fn is_default(self) -> bool {
self.0 == Self::EMPTY.0
}
#[inline]
pub fn as_char(self) -> Option<char> {
if self.is_grapheme() || self.0 == Self::EMPTY.0 || self.0 == Self::CONTINUATION.0 {
None
} else {
char::from_u32(self.0)
}
}
#[inline]
pub const fn grapheme_id(self) -> Option<GraphemeId> {
if self.is_grapheme() {
Some(GraphemeId::from_raw(self.0 & !0x8000_0000))
} else {
None
}
}
#[inline]
pub const fn width_hint(self) -> usize {
if self.is_empty() || self.is_continuation() {
0
} else if self.is_grapheme() {
((self.0 >> 27) & 0x0F) as usize
} else {
1
}
}
#[inline]
pub fn width(self) -> usize {
if self.is_empty() || self.is_continuation() {
0
} else if self.is_grapheme() {
((self.0 >> 27) & 0x0F) as usize
} else {
let Some(c) = self.as_char() else {
return 1;
};
char_width(c)
}
}
#[inline]
pub const fn raw(self) -> u32 {
self.0
}
}
impl Default for CellContent {
fn default() -> Self {
Self::EMPTY
}
}
impl core::fmt::Debug for CellContent {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
if self.is_empty() {
write!(f, "CellContent::EMPTY")
} else if self.is_continuation() {
write!(f, "CellContent::CONTINUATION")
} else if let Some(c) = self.as_char() {
write!(f, "CellContent::Char({c:?})")
} else if let Some(id) = self.grapheme_id() {
write!(f, "CellContent::Grapheme({id:?})")
} else {
write!(f, "CellContent(0x{:08x})", self.0)
}
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
#[repr(C, align(16))]
pub struct Cell {
pub content: CellContent,
pub fg: PackedRgba,
pub bg: PackedRgba,
pub attrs: CellAttrs,
}
const _: () = assert!(core::mem::size_of::<Cell>() == 16);
impl Cell {
pub const CONTINUATION: Self = Self {
content: CellContent::CONTINUATION,
fg: PackedRgba::TRANSPARENT,
bg: PackedRgba::TRANSPARENT,
attrs: CellAttrs::NONE,
};
#[inline]
pub const fn new(content: CellContent) -> Self {
Self {
content,
fg: PackedRgba::WHITE,
bg: PackedRgba::TRANSPARENT,
attrs: CellAttrs::NONE,
}
}
#[inline]
pub const fn from_char(c: char) -> Self {
Self::new(CellContent::from_char(c))
}
#[inline]
pub const fn is_continuation(&self) -> bool {
self.content.is_continuation()
}
#[inline]
pub const fn is_empty(&self) -> bool {
self.content.is_empty()
}
#[inline]
pub const fn width_hint(&self) -> usize {
self.content.width_hint()
}
#[inline(always)]
pub fn bits_eq(&self, other: &Self) -> bool {
(self.content.raw() == other.content.raw())
& (self.fg == other.fg)
& (self.bg == other.bg)
& (self.attrs == other.attrs)
}
#[inline]
#[must_use]
pub const fn with_char(mut self, c: char) -> Self {
self.content = CellContent::from_char(c);
self
}
#[inline]
#[must_use]
pub const fn with_fg(mut self, fg: PackedRgba) -> Self {
self.fg = fg;
self
}
#[inline]
#[must_use]
pub const fn with_bg(mut self, bg: PackedRgba) -> Self {
self.bg = bg;
self
}
#[inline]
#[must_use]
pub const fn with_attrs(mut self, attrs: CellAttrs) -> Self {
self.attrs = attrs;
self
}
}
impl Default for Cell {
fn default() -> Self {
Self {
content: CellContent::EMPTY,
fg: PackedRgba::WHITE,
bg: PackedRgba::TRANSPARENT,
attrs: CellAttrs::NONE,
}
}
}
impl core::fmt::Debug for Cell {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Cell")
.field("content", &self.content)
.field("fg", &self.fg)
.field("bg", &self.bg)
.field("attrs", &self.attrs)
.finish()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
#[repr(transparent)]
pub struct PackedRgba(pub u32);
impl PackedRgba {
pub const TRANSPARENT: Self = Self(0);
pub const BLACK: Self = Self::rgb(0, 0, 0);
pub const WHITE: Self = Self::rgb(255, 255, 255);
pub const RED: Self = Self::rgb(255, 0, 0);
pub const GREEN: Self = Self::rgb(0, 255, 0);
pub const BLUE: Self = Self::rgb(0, 0, 255);
#[inline]
pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
Self::rgba(r, g, b, 255)
}
#[inline]
pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
Self(((r as u32) << 24) | ((g as u32) << 16) | ((b as u32) << 8) | (a as u32))
}
#[inline]
pub const fn r(self) -> u8 {
(self.0 >> 24) as u8
}
#[inline]
pub const fn g(self) -> u8 {
(self.0 >> 16) as u8
}
#[inline]
pub const fn b(self) -> u8 {
(self.0 >> 8) as u8
}
#[inline]
pub const fn a(self) -> u8 {
self.0 as u8
}
#[inline]
const fn div_round_u8(numer: u64, denom: u64) -> u8 {
debug_assert!(denom != 0);
let v = (numer + (denom / 2)) / denom;
if v > 255 { 255 } else { v as u8 }
}
#[inline]
#[must_use]
pub fn over(self, dst: Self) -> Self {
let s_a = self.a() as u64;
if s_a == 255 {
return self;
}
if s_a == 0 {
return dst;
}
let d_a = dst.a() as u64;
let inv_s_a = 255 - s_a;
let numer_a = 255 * s_a + d_a * inv_s_a;
if numer_a == 0 {
return Self::TRANSPARENT;
}
let out_a = Self::div_round_u8(numer_a, 255);
let r = Self::div_round_u8(
(self.r() as u64) * s_a * 255 + (dst.r() as u64) * d_a * inv_s_a,
numer_a,
);
let g = Self::div_round_u8(
(self.g() as u64) * s_a * 255 + (dst.g() as u64) * d_a * inv_s_a,
numer_a,
);
let b = Self::div_round_u8(
(self.b() as u64) * s_a * 255 + (dst.b() as u64) * d_a * inv_s_a,
numer_a,
);
Self::rgba(r, g, b, out_a)
}
#[inline]
#[must_use]
pub fn with_opacity(self, opacity: f32) -> Self {
let opacity = opacity.clamp(0.0, 1.0);
let a = ((self.a() as f32) * opacity).round().clamp(0.0, 255.0) as u8;
Self::rgba(self.r(), self.g(), self.b(), a)
}
}
bitflags::bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct StyleFlags: u8 {
const BOLD = 0b0000_0001;
const DIM = 0b0000_0010;
const ITALIC = 0b0000_0100;
const UNDERLINE = 0b0000_1000;
const BLINK = 0b0001_0000;
const REVERSE = 0b0010_0000;
const STRIKETHROUGH = 0b0100_0000;
const HIDDEN = 0b1000_0000;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
#[repr(transparent)]
pub struct CellAttrs(u32);
impl CellAttrs {
pub const NONE: Self = Self(0);
pub const LINK_ID_NONE: u32 = 0;
pub const LINK_ID_MAX: u32 = 0x00FF_FFFE;
#[inline]
pub fn new(flags: StyleFlags, link_id: u32) -> Self {
debug_assert!(
link_id <= Self::LINK_ID_MAX,
"link_id overflow: {link_id} (max={})",
Self::LINK_ID_MAX
);
Self(((flags.bits() as u32) << 24) | (link_id & 0x00FF_FFFF))
}
#[inline]
pub fn flags(self) -> StyleFlags {
StyleFlags::from_bits_truncate((self.0 >> 24) as u8)
}
#[inline]
pub fn link_id(self) -> u32 {
self.0 & 0x00FF_FFFF
}
#[inline]
#[must_use]
pub fn with_flags(self, flags: StyleFlags) -> Self {
Self((self.0 & 0x00FF_FFFF) | ((flags.bits() as u32) << 24))
}
#[inline]
#[must_use]
pub fn with_link(self, link_id: u32) -> Self {
debug_assert!(
link_id <= Self::LINK_ID_MAX,
"link_id overflow: {link_id} (max={})",
Self::LINK_ID_MAX
);
Self((self.0 & 0xFF00_0000) | (link_id & 0x00FF_FFFF))
}
#[inline]
#[must_use]
pub fn merged_flags(self, extra: StyleFlags) -> Self {
let combined = self.flags().union(extra);
Self((self.0 & 0x00FF_FFFF) | ((combined.bits() as u32) << 24))
}
#[inline]
pub fn has_flag(self, flag: StyleFlags) -> bool {
self.flags().contains(flag)
}
}
#[cfg(test)]
mod tests {
use super::{Cell, CellAttrs, CellContent, GraphemeId, PackedRgba, StyleFlags};
fn reference_over(src: PackedRgba, dst: PackedRgba) -> PackedRgba {
let sr = src.r() as f64 / 255.0;
let sg = src.g() as f64 / 255.0;
let sb = src.b() as f64 / 255.0;
let sa = src.a() as f64 / 255.0;
let dr = dst.r() as f64 / 255.0;
let dg = dst.g() as f64 / 255.0;
let db = dst.b() as f64 / 255.0;
let da = dst.a() as f64 / 255.0;
let out_a = sa + da * (1.0 - sa);
if out_a <= 0.0 {
return PackedRgba::TRANSPARENT;
}
let out_r = (sr * sa + dr * da * (1.0 - sa)) / out_a;
let out_g = (sg * sa + dg * da * (1.0 - sa)) / out_a;
let out_b = (sb * sa + db * da * (1.0 - sa)) / out_a;
let to_u8 = |x: f64| -> u8 { (x * 255.0).round().clamp(0.0, 255.0) as u8 };
PackedRgba::rgba(to_u8(out_r), to_u8(out_g), to_u8(out_b), to_u8(out_a))
}
#[test]
fn packed_rgba_is_4_bytes() {
assert_eq!(core::mem::size_of::<PackedRgba>(), 4);
}
#[test]
fn rgb_sets_alpha_to_255() {
let c = PackedRgba::rgb(1, 2, 3);
assert_eq!(c.r(), 1);
assert_eq!(c.g(), 2);
assert_eq!(c.b(), 3);
assert_eq!(c.a(), 255);
}
#[test]
fn rgba_round_trips_components() {
let c = PackedRgba::rgba(10, 20, 30, 40);
assert_eq!(c.r(), 10);
assert_eq!(c.g(), 20);
assert_eq!(c.b(), 30);
assert_eq!(c.a(), 40);
}
#[test]
fn over_with_opaque_src_returns_src() {
let src = PackedRgba::rgba(1, 2, 3, 255);
let dst = PackedRgba::rgba(9, 8, 7, 200);
assert_eq!(src.over(dst), src);
}
#[test]
fn over_with_transparent_src_returns_dst() {
let src = PackedRgba::TRANSPARENT;
let dst = PackedRgba::rgba(9, 8, 7, 200);
assert_eq!(src.over(dst), dst);
}
#[test]
fn over_blends_correctly_for_half_alpha_over_opaque() {
let src = PackedRgba::rgba(255, 0, 0, 128);
let dst = PackedRgba::rgba(0, 0, 255, 255);
assert_eq!(src.over(dst), PackedRgba::rgba(128, 0, 127, 255));
}
#[test]
fn over_matches_reference_for_partial_alpha_cases() {
let cases = [
(
PackedRgba::rgba(200, 10, 10, 64),
PackedRgba::rgba(10, 200, 10, 128),
),
(
PackedRgba::rgba(1, 2, 3, 1),
PackedRgba::rgba(250, 251, 252, 254),
),
(
PackedRgba::rgba(100, 0, 200, 200),
PackedRgba::rgba(0, 120, 30, 50),
),
];
for (src, dst) in cases {
assert_eq!(src.over(dst), reference_over(src, dst));
}
}
#[test]
fn with_opacity_scales_alpha() {
let c = PackedRgba::rgba(10, 20, 30, 255);
assert_eq!(c.with_opacity(0.5).a(), 128);
assert_eq!(c.with_opacity(-1.0).a(), 0);
assert_eq!(c.with_opacity(2.0).a(), 255);
}
#[test]
fn cell_attrs_is_4_bytes() {
assert_eq!(core::mem::size_of::<CellAttrs>(), 4);
}
#[test]
fn cell_attrs_none_has_no_flags_and_no_link() {
assert!(CellAttrs::NONE.flags().is_empty());
assert_eq!(CellAttrs::NONE.link_id(), 0);
}
#[test]
fn cell_attrs_new_stores_flags_and_link() {
let flags = StyleFlags::BOLD | StyleFlags::ITALIC;
let a = CellAttrs::new(flags, 42);
assert_eq!(a.flags(), flags);
assert_eq!(a.link_id(), 42);
}
#[test]
fn cell_attrs_with_flags_preserves_link_id() {
let a = CellAttrs::new(StyleFlags::BOLD, 123);
let b = a.with_flags(StyleFlags::UNDERLINE);
assert_eq!(b.flags(), StyleFlags::UNDERLINE);
assert_eq!(b.link_id(), 123);
}
#[test]
fn cell_attrs_merged_flags_ors_without_clearing() {
let a = CellAttrs::new(StyleFlags::BOLD, 42);
let b = a.merged_flags(StyleFlags::ITALIC);
assert_eq!(b.flags(), StyleFlags::BOLD | StyleFlags::ITALIC);
assert_eq!(b.link_id(), 42, "link_id must be preserved");
}
#[test]
fn cell_attrs_merged_flags_noop_for_empty() {
let a = CellAttrs::new(StyleFlags::BOLD, 7);
let b = a.merged_flags(StyleFlags::empty());
assert_eq!(b.flags(), StyleFlags::BOLD);
assert_eq!(b.link_id(), 7);
}
#[test]
fn cell_attrs_with_link_preserves_flags() {
let a = CellAttrs::new(StyleFlags::BOLD | StyleFlags::ITALIC, 1);
let b = a.with_link(999);
assert_eq!(b.flags(), StyleFlags::BOLD | StyleFlags::ITALIC);
assert_eq!(b.link_id(), 999);
}
#[test]
fn cell_attrs_flag_combinations_work() {
let flags = StyleFlags::BOLD | StyleFlags::ITALIC;
let a = CellAttrs::new(flags, 0);
assert!(a.has_flag(StyleFlags::BOLD));
assert!(a.has_flag(StyleFlags::ITALIC));
assert!(!a.has_flag(StyleFlags::UNDERLINE));
}
#[test]
fn cell_attrs_link_id_max_boundary() {
let a = CellAttrs::new(StyleFlags::empty(), CellAttrs::LINK_ID_MAX);
assert_eq!(a.link_id(), CellAttrs::LINK_ID_MAX);
}
#[test]
fn grapheme_id_is_4_bytes() {
assert_eq!(core::mem::size_of::<GraphemeId>(), 4);
}
#[test]
fn grapheme_id_encoding_roundtrip() {
let id = GraphemeId::new(12345, 42, 2);
assert_eq!(id.slot(), 12345);
assert_eq!(id.generation(), 42);
assert_eq!(id.width(), 2);
}
#[test]
fn grapheme_id_max_values() {
let id = GraphemeId::new(
GraphemeId::MAX_SLOT,
GraphemeId::MAX_GENERATION,
GraphemeId::MAX_WIDTH,
);
assert_eq!(id.slot(), 0xFFFF);
assert_eq!(id.generation(), GraphemeId::MAX_GENERATION);
assert_eq!(id.width(), GraphemeId::MAX_WIDTH as usize);
}
#[test]
fn grapheme_id_zero_values() {
let id = GraphemeId::new(0, 0, 0);
assert_eq!(id.slot(), 0);
assert_eq!(id.generation(), 0);
assert_eq!(id.width(), 0);
}
#[test]
fn grapheme_id_raw_roundtrip() {
let id = GraphemeId::new(999, 128, 5);
let raw = id.raw();
let restored = GraphemeId::from_raw(raw);
assert_eq!(restored.slot(), 999);
assert_eq!(restored.generation(), 128);
assert_eq!(restored.width(), 5);
}
#[test]
fn cell_content_is_4_bytes() {
assert_eq!(core::mem::size_of::<CellContent>(), 4);
}
#[test]
fn cell_content_empty_properties() {
assert!(CellContent::EMPTY.is_empty());
assert!(!CellContent::EMPTY.is_continuation());
assert!(!CellContent::EMPTY.is_grapheme());
assert_eq!(CellContent::EMPTY.width_hint(), 0);
}
#[test]
fn cell_content_continuation_properties() {
assert!(CellContent::CONTINUATION.is_continuation());
assert!(!CellContent::CONTINUATION.is_empty());
assert!(!CellContent::CONTINUATION.is_grapheme());
assert_eq!(CellContent::CONTINUATION.width_hint(), 0);
}
#[test]
fn cell_content_from_char_ascii() {
let c = CellContent::from_char('A');
assert!(!c.is_grapheme());
assert!(!c.is_empty());
assert!(!c.is_continuation());
assert_eq!(c.as_char(), Some('A'));
assert_eq!(c.width_hint(), 1);
}
#[test]
fn cell_content_from_char_unicode() {
let c = CellContent::from_char('日');
assert_eq!(c.as_char(), Some('日'));
assert!(!c.is_grapheme());
let c2 = CellContent::from_char('🎉');
assert_eq!(c2.as_char(), Some('🎉'));
assert!(!c2.is_grapheme());
}
#[test]
fn cell_content_from_grapheme() {
let id = GraphemeId::new(42, 0, 2);
let c = CellContent::from_grapheme(id);
assert!(c.is_grapheme());
assert!(!c.is_empty());
assert!(!c.is_continuation());
assert_eq!(c.grapheme_id(), Some(id));
assert_eq!(c.as_char(), None);
assert_eq!(c.width_hint(), 2);
}
#[test]
fn cell_content_width_for_chars() {
let ascii = CellContent::from_char('A');
assert_eq!(ascii.width(), 1);
let wide = CellContent::from_char('日');
assert_eq!(wide.width(), 2);
let emoji = CellContent::from_char('🎉');
assert_eq!(emoji.width(), 2);
let bolt = CellContent::from_char('⚡');
assert_eq!(bolt.width(), 2, "bolt is Wide, always width 2");
let gear = CellContent::from_char('⚙');
let heart = CellContent::from_char('❤');
assert!(
[1, 2].contains(&gear.width()),
"gear should be 1 (non-CJK) or 2 (CJK), got {}",
gear.width()
);
assert_eq!(
gear.width(),
heart.width(),
"gear and heart should have same width (both Neutral)"
);
}
#[test]
fn cell_content_width_for_grapheme() {
let id = GraphemeId::new(7, 0, 3);
let c = CellContent::from_grapheme(id);
assert_eq!(c.width(), 3);
}
#[test]
fn cell_content_width_empty_is_zero() {
assert_eq!(CellContent::EMPTY.width(), 0);
assert_eq!(CellContent::CONTINUATION.width(), 0);
}
#[test]
fn cell_content_grapheme_discriminator_bit() {
let char_content = CellContent::from_char('X');
assert_eq!(char_content.raw() & 0x8000_0000, 0);
let grapheme_content = CellContent::from_grapheme(GraphemeId::new(1, 0, 1));
assert_ne!(grapheme_content.raw() & 0x8000_0000, 0);
}
#[test]
fn cell_is_16_bytes() {
assert_eq!(core::mem::size_of::<Cell>(), 16);
}
#[test]
fn cell_alignment_is_16() {
assert_eq!(core::mem::align_of::<Cell>(), 16);
}
#[test]
fn cell_default_properties() {
let cell = Cell::default();
assert!(cell.is_empty());
assert!(!cell.is_continuation());
assert_eq!(cell.fg, PackedRgba::WHITE);
assert_eq!(cell.bg, PackedRgba::TRANSPARENT);
assert_eq!(cell.attrs, CellAttrs::NONE);
}
#[test]
fn cell_continuation_constant() {
assert!(Cell::CONTINUATION.is_continuation());
assert!(!Cell::CONTINUATION.is_empty());
}
#[test]
fn cell_from_char() {
let cell = Cell::from_char('X');
assert_eq!(cell.content.as_char(), Some('X'));
assert_eq!(cell.fg, PackedRgba::WHITE);
assert_eq!(cell.bg, PackedRgba::TRANSPARENT);
}
#[test]
fn cell_builder_methods() {
let cell = Cell::from_char('A')
.with_fg(PackedRgba::rgb(255, 0, 0))
.with_bg(PackedRgba::rgb(0, 0, 255))
.with_attrs(CellAttrs::new(StyleFlags::BOLD, 0));
assert_eq!(cell.content.as_char(), Some('A'));
assert_eq!(cell.fg, PackedRgba::rgb(255, 0, 0));
assert_eq!(cell.bg, PackedRgba::rgb(0, 0, 255));
assert!(cell.attrs.has_flag(StyleFlags::BOLD));
}
#[test]
fn cell_bits_eq_same_cells() {
let cell1 = Cell::from_char('X').with_fg(PackedRgba::rgb(1, 2, 3));
let cell2 = Cell::from_char('X').with_fg(PackedRgba::rgb(1, 2, 3));
assert!(cell1.bits_eq(&cell2));
}
#[test]
fn cell_bits_eq_different_cells() {
let cell1 = Cell::from_char('X');
let cell2 = Cell::from_char('Y');
assert!(!cell1.bits_eq(&cell2));
let cell3 = Cell::from_char('X').with_fg(PackedRgba::rgb(1, 2, 3));
assert!(!cell1.bits_eq(&cell3));
}
#[test]
fn cell_width_hint() {
let empty = Cell::default();
assert_eq!(empty.width_hint(), 0);
let cont = Cell::CONTINUATION;
assert_eq!(cont.width_hint(), 0);
let ascii = Cell::from_char('A');
assert_eq!(ascii.width_hint(), 1);
}
#[test]
fn packed_rgba_named_constants() {
assert_eq!(PackedRgba::TRANSPARENT, PackedRgba(0));
assert_eq!(PackedRgba::TRANSPARENT.a(), 0);
assert_eq!(PackedRgba::BLACK.r(), 0);
assert_eq!(PackedRgba::BLACK.g(), 0);
assert_eq!(PackedRgba::BLACK.b(), 0);
assert_eq!(PackedRgba::BLACK.a(), 255);
assert_eq!(PackedRgba::WHITE.r(), 255);
assert_eq!(PackedRgba::WHITE.g(), 255);
assert_eq!(PackedRgba::WHITE.b(), 255);
assert_eq!(PackedRgba::WHITE.a(), 255);
assert_eq!(PackedRgba::RED, PackedRgba::rgb(255, 0, 0));
assert_eq!(PackedRgba::GREEN, PackedRgba::rgb(0, 255, 0));
assert_eq!(PackedRgba::BLUE, PackedRgba::rgb(0, 0, 255));
}
#[test]
fn packed_rgba_default_is_transparent() {
assert_eq!(PackedRgba::default(), PackedRgba::TRANSPARENT);
}
#[test]
fn over_both_transparent_returns_transparent() {
let result = PackedRgba::TRANSPARENT.over(PackedRgba::TRANSPARENT);
assert_eq!(result, PackedRgba::TRANSPARENT);
}
#[test]
fn over_partial_alpha_over_transparent_dst() {
let src = PackedRgba::rgba(200, 100, 50, 128);
let result = src.over(PackedRgba::TRANSPARENT);
assert_eq!(result.a(), 128);
assert_eq!(result.r(), 200);
assert_eq!(result.g(), 100);
assert_eq!(result.b(), 50);
}
#[test]
fn over_very_low_alpha() {
let src = PackedRgba::rgba(255, 0, 0, 1);
let dst = PackedRgba::rgba(0, 0, 255, 255);
let result = src.over(dst);
assert_eq!(result.a(), 255);
assert!(result.b() > 250, "b={} should be near 255", result.b());
assert!(result.r() < 5, "r={} should be near 0", result.r());
}
#[test]
fn with_opacity_exact_zero() {
let c = PackedRgba::rgba(10, 20, 30, 200);
let result = c.with_opacity(0.0);
assert_eq!(result.a(), 0);
assert_eq!(result.r(), 10); assert_eq!(result.g(), 20);
assert_eq!(result.b(), 30);
}
#[test]
fn with_opacity_exact_one() {
let c = PackedRgba::rgba(10, 20, 30, 200);
let result = c.with_opacity(1.0);
assert_eq!(result.a(), 200); assert_eq!(result.r(), 10);
}
#[test]
fn with_opacity_preserves_rgb() {
let c = PackedRgba::rgba(42, 84, 168, 255);
let result = c.with_opacity(0.25);
assert_eq!(result.r(), 42);
assert_eq!(result.g(), 84);
assert_eq!(result.b(), 168);
assert_eq!(result.a(), 64); }
#[test]
fn cell_content_as_char_none_for_empty() {
assert_eq!(CellContent::EMPTY.as_char(), None);
}
#[test]
fn cell_content_as_char_none_for_continuation() {
assert_eq!(CellContent::CONTINUATION.as_char(), None);
}
#[test]
fn cell_content_as_char_none_for_grapheme() {
let id = GraphemeId::new(1, 2, 1);
let c = CellContent::from_grapheme(id);
assert_eq!(c.as_char(), None);
}
#[test]
fn cell_content_grapheme_id_none_for_char() {
let c = CellContent::from_char('A');
assert_eq!(c.grapheme_id(), None);
}
#[test]
fn cell_content_grapheme_id_none_for_empty() {
assert_eq!(CellContent::EMPTY.grapheme_id(), None);
}
#[test]
fn cell_content_width_control_chars() {
let tab = CellContent::from_char('\t');
assert_eq!(tab.width(), 1);
let bel = CellContent::from_char('\x07');
assert_eq!(bel.width(), 0);
}
#[test]
fn cell_content_width_hint_always_1_for_chars() {
let wide = CellContent::from_char('日');
assert_eq!(wide.width_hint(), 1); assert_eq!(wide.width(), 2); }
#[test]
fn cell_content_default_is_empty() {
assert_eq!(CellContent::default(), CellContent::EMPTY);
}
#[test]
fn cell_content_debug_empty() {
let s = format!("{:?}", CellContent::EMPTY);
assert_eq!(s, "CellContent::EMPTY");
}
#[test]
fn cell_content_debug_continuation() {
let s = format!("{:?}", CellContent::CONTINUATION);
assert_eq!(s, "CellContent::CONTINUATION");
}
#[test]
fn cell_content_debug_char() {
let s = format!("{:?}", CellContent::from_char('X'));
assert!(s.starts_with("CellContent::Char("), "got: {s}");
}
#[test]
fn cell_content_debug_grapheme() {
let id = GraphemeId::new(1, 2, 1);
let s = format!("{:?}", CellContent::from_grapheme(id));
assert!(s.starts_with("CellContent::Grapheme("), "got: {s}");
}
#[test]
fn cell_content_raw_value() {
let c = CellContent::from_char('A');
assert_eq!(c.raw(), 'A' as u32);
let g = CellContent::from_grapheme(GraphemeId::new(5, 2, 1));
assert_ne!(g.raw() & 0x8000_0000, 0);
}
#[test]
fn cell_attrs_default_is_none() {
assert_eq!(CellAttrs::default(), CellAttrs::NONE);
}
#[test]
fn cell_attrs_each_flag_isolated() {
let all_flags = [
StyleFlags::BOLD,
StyleFlags::DIM,
StyleFlags::ITALIC,
StyleFlags::UNDERLINE,
StyleFlags::BLINK,
StyleFlags::REVERSE,
StyleFlags::STRIKETHROUGH,
StyleFlags::HIDDEN,
];
for &flag in &all_flags {
let a = CellAttrs::new(flag, 0);
assert!(a.has_flag(flag), "flag {:?} should be set", flag);
for &other in &all_flags {
if other != flag {
assert!(
!a.has_flag(other),
"flag {:?} should NOT be set when only {:?} is",
other,
flag
);
}
}
}
}
#[test]
fn cell_attrs_all_flags_combined() {
let all = StyleFlags::BOLD
| StyleFlags::DIM
| StyleFlags::ITALIC
| StyleFlags::UNDERLINE
| StyleFlags::BLINK
| StyleFlags::REVERSE
| StyleFlags::STRIKETHROUGH
| StyleFlags::HIDDEN;
let a = CellAttrs::new(all, 42);
assert_eq!(a.flags(), all);
assert!(a.has_flag(StyleFlags::BOLD));
assert!(a.has_flag(StyleFlags::HIDDEN));
assert_eq!(a.link_id(), 42);
}
#[test]
fn cell_attrs_link_id_zero() {
let a = CellAttrs::new(StyleFlags::BOLD, CellAttrs::LINK_ID_NONE);
assert_eq!(a.link_id(), 0);
assert!(a.has_flag(StyleFlags::BOLD));
}
#[test]
fn cell_attrs_with_link_to_none() {
let a = CellAttrs::new(StyleFlags::ITALIC, 500);
let b = a.with_link(CellAttrs::LINK_ID_NONE);
assert_eq!(b.link_id(), 0);
assert!(b.has_flag(StyleFlags::ITALIC));
}
#[test]
fn cell_attrs_with_flags_to_empty() {
let a = CellAttrs::new(StyleFlags::BOLD | StyleFlags::ITALIC, 123);
let b = a.with_flags(StyleFlags::empty());
assert!(b.flags().is_empty());
assert_eq!(b.link_id(), 123);
}
#[test]
fn cell_bits_eq_detects_bg_difference() {
let cell1 = Cell::from_char('X');
let cell2 = Cell::from_char('X').with_bg(PackedRgba::RED);
assert!(!cell1.bits_eq(&cell2));
}
#[test]
fn cell_bits_eq_detects_attrs_difference() {
let cell1 = Cell::from_char('X');
let cell2 = Cell::from_char('X').with_attrs(CellAttrs::new(StyleFlags::BOLD, 0));
assert!(!cell1.bits_eq(&cell2));
}
#[test]
fn cell_with_char_preserves_colors_and_attrs() {
let cell = Cell::from_char('A')
.with_fg(PackedRgba::RED)
.with_bg(PackedRgba::BLUE)
.with_attrs(CellAttrs::new(StyleFlags::BOLD, 42));
let updated = cell.with_char('Z');
assert_eq!(updated.content.as_char(), Some('Z'));
assert_eq!(updated.fg, PackedRgba::RED);
assert_eq!(updated.bg, PackedRgba::BLUE);
assert!(updated.attrs.has_flag(StyleFlags::BOLD));
assert_eq!(updated.attrs.link_id(), 42);
}
#[test]
fn cell_new_vs_from_char() {
let a = Cell::new(CellContent::from_char('A'));
let b = Cell::from_char('A');
assert!(a.bits_eq(&b));
}
#[test]
fn cell_continuation_has_transparent_colors() {
assert_eq!(Cell::CONTINUATION.fg, PackedRgba::TRANSPARENT);
assert_eq!(Cell::CONTINUATION.bg, PackedRgba::TRANSPARENT);
assert_eq!(Cell::CONTINUATION.attrs, CellAttrs::NONE);
}
#[test]
fn cell_debug_format() {
let cell = Cell::from_char('A');
let s = format!("{:?}", cell);
assert!(s.contains("Cell"), "got: {s}");
assert!(s.contains("content"), "got: {s}");
assert!(s.contains("fg"), "got: {s}");
assert!(s.contains("bg"), "got: {s}");
assert!(s.contains("attrs"), "got: {s}");
}
#[test]
fn cell_is_empty_for_various() {
assert!(Cell::default().is_empty());
assert!(!Cell::from_char('A').is_empty());
assert!(!Cell::CONTINUATION.is_empty());
}
#[test]
fn cell_is_continuation_for_various() {
assert!(!Cell::default().is_continuation());
assert!(!Cell::from_char('A').is_continuation());
assert!(Cell::CONTINUATION.is_continuation());
}
#[test]
fn cell_width_hint_for_grapheme() {
let id = GraphemeId::new(100, 0, 3);
let cell = Cell::new(CellContent::from_grapheme(id));
assert_eq!(cell.width_hint(), 3);
}
#[test]
fn grapheme_id_default() {
let id = GraphemeId::default();
assert_eq!(id.slot(), 0);
assert_eq!(id.generation(), 0);
assert_eq!(id.width(), 0);
}
#[test]
fn grapheme_id_debug_format() {
let id = GraphemeId::new(42, 5, 2);
let s = format!("{:?}", id);
assert!(s.contains("GraphemeId"), "got: {s}");
assert!(s.contains("42"), "got: {s}");
assert!(s.contains("5"), "got: {s}");
assert!(s.contains("2"), "got: {s}");
}
#[test]
fn grapheme_id_width_isolated_from_slot() {
let id = GraphemeId::new(GraphemeId::MAX_SLOT, 0, 0);
assert_eq!(id.width(), 0);
assert_eq!(id.slot(), 0xFFFF);
let id2 = GraphemeId::new(0, 0, GraphemeId::MAX_WIDTH);
assert_eq!(id2.slot(), 0);
assert_eq!(id2.width(), GraphemeId::MAX_WIDTH as usize);
}
#[test]
fn style_flags_empty_has_no_bits() {
assert!(StyleFlags::empty().is_empty());
assert_eq!(StyleFlags::empty().bits(), 0);
}
#[test]
fn style_flags_all_has_all_bits() {
let all = StyleFlags::all();
assert!(all.contains(StyleFlags::BOLD));
assert!(all.contains(StyleFlags::DIM));
assert!(all.contains(StyleFlags::ITALIC));
assert!(all.contains(StyleFlags::UNDERLINE));
assert!(all.contains(StyleFlags::BLINK));
assert!(all.contains(StyleFlags::REVERSE));
assert!(all.contains(StyleFlags::STRIKETHROUGH));
assert!(all.contains(StyleFlags::HIDDEN));
}
#[test]
fn style_flags_union_and_intersection() {
let a = StyleFlags::BOLD | StyleFlags::ITALIC;
let b = StyleFlags::ITALIC | StyleFlags::UNDERLINE;
assert_eq!(
a | b,
StyleFlags::BOLD | StyleFlags::ITALIC | StyleFlags::UNDERLINE
);
assert_eq!(a & b, StyleFlags::ITALIC);
}
#[test]
fn style_flags_from_bits_truncate() {
let all = StyleFlags::from_bits_truncate(0xFF);
assert_eq!(all, StyleFlags::all());
let none = StyleFlags::from_bits_truncate(0x00);
assert!(none.is_empty());
}
#[test]
fn over_not_commutative() {
let red_half = PackedRgba::rgba(255, 0, 0, 128);
let blue_half = PackedRgba::rgba(0, 0, 255, 128);
let a_over_b = red_half.over(blue_half);
let b_over_a = blue_half.over(red_half);
assert_ne!(a_over_b, b_over_a);
}
#[test]
fn over_opaque_self_compositing_is_idempotent() {
let c = PackedRgba::rgba(42, 84, 168, 255);
assert_eq!(c.over(c), c);
}
#[test]
fn over_near_opaque_src_alpha_254() {
let src = PackedRgba::rgba(255, 0, 0, 254);
let dst = PackedRgba::rgba(0, 0, 255, 255);
let result = src.over(dst);
assert_eq!(result.a(), 255);
assert!(result.r() >= 253, "r={}", result.r());
assert!(result.b() <= 2, "b={}", result.b());
}
#[test]
fn over_both_partial_alpha_symmetric_colors() {
let c = PackedRgba::rgba(200, 100, 50, 128);
let result = c.over(c);
let ref_result = reference_over(c, c);
assert_eq!(result, ref_result);
assert!(result.a() >= 190 && result.a() <= 194, "a={}", result.a());
}
#[test]
fn over_both_alpha_1_minimal() {
let src = PackedRgba::rgba(255, 255, 255, 1);
let dst = PackedRgba::rgba(0, 0, 0, 1);
let result = src.over(dst);
let ref_result = reference_over(src, dst);
assert_eq!(result, ref_result);
assert!(result.a() <= 3, "a={}", result.a());
}
#[test]
fn over_white_alpha_0_over_opaque_is_dst() {
let src = PackedRgba::rgba(255, 255, 255, 0);
let dst = PackedRgba::rgba(100, 50, 25, 255);
assert_eq!(src.over(dst), dst);
}
#[test]
fn with_opacity_nan_clamps_to_zero() {
let c = PackedRgba::rgba(10, 20, 30, 200);
let result = c.with_opacity(f32::NAN);
assert_eq!(result.r(), 10);
assert_eq!(result.g(), 20);
assert_eq!(result.b(), 30);
}
#[test]
fn with_opacity_negative_infinity_clamps_to_zero() {
let c = PackedRgba::rgba(10, 20, 30, 200);
let result = c.with_opacity(f32::NEG_INFINITY);
assert_eq!(result.a(), 0);
}
#[test]
fn with_opacity_positive_infinity_clamps_to_original() {
let c = PackedRgba::rgba(10, 20, 30, 200);
let result = c.with_opacity(f32::INFINITY);
assert_eq!(result.a(), 200);
}
#[test]
fn with_opacity_on_transparent_stays_transparent() {
let c = PackedRgba::TRANSPARENT;
assert_eq!(c.with_opacity(0.5).a(), 0);
assert_eq!(c.with_opacity(1.0).a(), 0);
}
#[test]
fn packed_rgba_extreme_channel_values() {
let all_max = PackedRgba::rgba(255, 255, 255, 255);
assert_eq!(all_max.r(), 255);
assert_eq!(all_max.g(), 255);
assert_eq!(all_max.b(), 255);
assert_eq!(all_max.a(), 255);
let all_zero = PackedRgba::rgba(0, 0, 0, 0);
assert_eq!(all_zero, PackedRgba::TRANSPARENT);
}
#[test]
fn packed_rgba_hash_differs_for_different_values() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(PackedRgba::RED);
set.insert(PackedRgba::GREEN);
set.insert(PackedRgba::BLUE);
set.insert(PackedRgba::RED); assert_eq!(set.len(), 3);
}
#[test]
fn packed_rgba_channel_isolation() {
let base = PackedRgba::rgba(10, 20, 30, 40);
let different_r = PackedRgba::rgba(99, 20, 30, 40);
assert_ne!(base, different_r);
assert_eq!(base.g(), different_r.g());
assert_eq!(base.b(), different_r.b());
assert_eq!(base.a(), different_r.a());
}
#[test]
fn cell_content_nul_char_equals_empty() {
let nul = CellContent::from_char('\0');
assert_eq!(nul.raw(), CellContent::EMPTY.raw());
assert!(nul.is_empty());
assert_eq!(nul.as_char(), None); }
#[test]
fn cell_content_soh_char_is_not_continuation() {
let soh = CellContent::from_char('\x01');
assert_eq!(soh.raw(), 1);
assert!(!soh.is_empty());
assert!(!soh.is_continuation());
assert_eq!(soh.as_char(), Some('\x01'));
}
#[test]
fn cell_content_max_unicode_codepoint() {
let max = CellContent::from_char('\u{10FFFF}');
assert_eq!(max.as_char(), Some('\u{10FFFF}'));
assert!(!max.is_grapheme());
assert_eq!(max.width_hint(), 1);
}
#[test]
fn cell_content_bmp_boundary_chars() {
let last_before_surrogates = CellContent::from_char('\u{D7FF}');
assert_eq!(last_before_surrogates.as_char(), Some('\u{D7FF}'));
let first_after_surrogates = CellContent::from_char('\u{E000}');
assert_eq!(first_after_surrogates.as_char(), Some('\u{E000}'));
let supplementary = CellContent::from_char('\u{10000}');
assert_eq!(supplementary.as_char(), Some('\u{10000}'));
assert!(!supplementary.is_grapheme()); }
#[test]
fn cell_content_grapheme_with_zero_width() {
let id = GraphemeId::new(42, 0, 0);
let c = CellContent::from_grapheme(id);
assert_eq!(c.width_hint(), 0);
assert_eq!(c.width(), 0);
assert!(c.is_grapheme());
}
#[test]
fn cell_content_grapheme_with_max_width() {
let id = GraphemeId::new(1, 0, GraphemeId::MAX_WIDTH);
let c = CellContent::from_grapheme(id);
assert_eq!(c.width_hint(), GraphemeId::MAX_WIDTH as usize);
assert_eq!(c.width(), GraphemeId::MAX_WIDTH as usize);
}
#[test]
fn cell_content_continuation_value_is_max_i31() {
assert_eq!(CellContent::CONTINUATION.raw(), 0x7FFF_FFFF);
assert!(!CellContent::CONTINUATION.is_grapheme()); assert!(CellContent::CONTINUATION.is_continuation());
}
#[test]
fn cell_content_empty_and_continuation_are_distinct() {
assert_ne!(CellContent::EMPTY, CellContent::CONTINUATION);
assert!(CellContent::EMPTY.is_empty());
assert!(!CellContent::EMPTY.is_continuation());
assert!(!CellContent::CONTINUATION.is_empty());
assert!(CellContent::CONTINUATION.is_continuation());
}
#[test]
fn cell_content_grapheme_id_strips_high_bit() {
let id = GraphemeId::new(
GraphemeId::MAX_SLOT,
GraphemeId::MAX_GENERATION,
GraphemeId::MAX_WIDTH,
);
let c = CellContent::from_grapheme(id);
let extracted = c.grapheme_id().unwrap();
assert_eq!(extracted.slot(), id.slot());
assert_eq!(extracted.generation(), id.generation());
assert_eq!(extracted.width(), id.width());
}
#[test]
fn grapheme_id_slot_one_width_one() {
let id = GraphemeId::new(1, 0, 1);
assert_eq!(id.slot(), 1);
assert_eq!(id.width(), 1);
}
#[test]
fn grapheme_id_hash_eq_consistency() {
use std::collections::HashSet;
let a = GraphemeId::new(42, 0, 2);
let b = GraphemeId::new(42, 0, 2);
let c = GraphemeId::new(42, 0, 3);
let d = GraphemeId::new(42, 1, 2);
assert_eq!(a, b);
assert_ne!(a, c);
assert_ne!(a, d);
let mut set = HashSet::new();
set.insert(a);
assert!(set.contains(&b));
assert!(!set.contains(&c));
assert!(!set.contains(&d));
}
#[test]
fn grapheme_id_adjacent_slots_differ() {
let a = GraphemeId::new(0, 0, 1);
let b = GraphemeId::new(1, 0, 1);
assert_ne!(a, b);
assert_ne!(a.slot(), b.slot());
assert_eq!(a.width(), b.width());
}
#[test]
fn cell_attrs_link_id_masks_overflow() {
let a = CellAttrs::new(StyleFlags::empty(), 0x00FF_FFFE);
assert_eq!(a.link_id(), 0x00FF_FFFE);
}
#[test]
fn cell_attrs_chained_mutations() {
let a = CellAttrs::new(StyleFlags::BOLD, 100)
.with_flags(StyleFlags::ITALIC)
.with_link(200)
.with_flags(StyleFlags::UNDERLINE | StyleFlags::DIM)
.with_link(300);
assert_eq!(a.flags(), StyleFlags::UNDERLINE | StyleFlags::DIM);
assert_eq!(a.link_id(), 300);
}
#[test]
fn cell_attrs_all_flags_max_link() {
let all_flags = StyleFlags::all();
let a = CellAttrs::new(all_flags, CellAttrs::LINK_ID_MAX);
assert_eq!(a.flags(), all_flags);
assert_eq!(a.link_id(), CellAttrs::LINK_ID_MAX);
assert_eq!(a.flags().bits(), 0xFF);
assert_eq!(a.link_id(), 0x00FF_FFFE);
}
#[test]
fn cell_attrs_link_id_none_is_zero() {
assert_eq!(CellAttrs::LINK_ID_NONE, 0);
}
#[test]
fn cell_eq_matches_bits_eq() {
let pairs = [
(Cell::default(), Cell::default()),
(Cell::from_char('A'), Cell::from_char('A')),
(Cell::from_char('A'), Cell::from_char('B')),
(Cell::CONTINUATION, Cell::CONTINUATION),
(
Cell::from_char('X').with_fg(PackedRgba::RED),
Cell::from_char('X').with_fg(PackedRgba::BLUE),
),
];
for (a, b) in &pairs {
assert_eq!(
a == b,
a.bits_eq(b),
"PartialEq and bits_eq disagree for {:?} vs {:?}",
a,
b
);
}
}
#[test]
fn cell_from_grapheme_content() {
let id = GraphemeId::new(42, 0, 2);
let cell = Cell::new(CellContent::from_grapheme(id));
assert!(cell.content.is_grapheme());
assert_eq!(cell.width_hint(), 2);
assert!(!cell.is_empty());
assert!(!cell.is_continuation());
}
#[test]
fn cell_with_char_on_continuation() {
let cell = Cell::CONTINUATION.with_char('A');
assert_eq!(cell.content.as_char(), Some('A'));
assert!(!cell.is_continuation());
assert_eq!(cell.fg, PackedRgba::TRANSPARENT);
assert_eq!(cell.bg, PackedRgba::TRANSPARENT);
}
#[test]
fn cell_default_bits_eq_self() {
let cell = Cell::default();
assert!(cell.bits_eq(&cell));
}
#[test]
fn cell_new_empty_equals_default() {
let a = Cell::new(CellContent::EMPTY);
let b = Cell::default();
assert!(a.bits_eq(&b));
}
#[test]
fn cell_all_builder_methods_chain() {
let cell = Cell::default()
.with_char('Z')
.with_fg(PackedRgba::rgba(1, 2, 3, 4))
.with_bg(PackedRgba::rgba(5, 6, 7, 8))
.with_attrs(CellAttrs::new(
StyleFlags::BOLD | StyleFlags::STRIKETHROUGH,
999,
));
assert_eq!(cell.content.as_char(), Some('Z'));
assert_eq!(cell.fg.r(), 1);
assert_eq!(cell.bg.a(), 8);
assert!(cell.attrs.has_flag(StyleFlags::BOLD));
assert!(cell.attrs.has_flag(StyleFlags::STRIKETHROUGH));
assert!(!cell.attrs.has_flag(StyleFlags::ITALIC));
assert_eq!(cell.attrs.link_id(), 999);
}
#[test]
fn cell_size_and_alignment_invariants() {
assert_eq!(core::mem::size_of::<Cell>(), 16);
assert_eq!(core::mem::align_of::<Cell>(), 16);
assert_eq!(64 / core::mem::size_of::<Cell>(), 4);
}
#[test]
fn cell_content_size_invariant() {
assert_eq!(core::mem::size_of::<CellContent>(), 4);
}
#[test]
fn cell_attrs_size_invariant() {
assert_eq!(core::mem::size_of::<CellAttrs>(), 4);
}
#[test]
fn over_associativity_approximate() {
let a = PackedRgba::rgba(200, 50, 100, 128);
let b = PackedRgba::rgba(50, 200, 50, 128);
let c = PackedRgba::rgba(100, 100, 200, 128);
let ab_c = a.over(b).over(c);
let a_bc = a.over(b.over(c));
assert!(
(ab_c.r() as i16 - a_bc.r() as i16).unsigned_abs() <= 1,
"r: {} vs {}",
ab_c.r(),
a_bc.r()
);
assert!(
(ab_c.g() as i16 - a_bc.g() as i16).unsigned_abs() <= 1,
"g: {} vs {}",
ab_c.g(),
a_bc.g()
);
assert!(
(ab_c.b() as i16 - a_bc.b() as i16).unsigned_abs() <= 1,
"b: {} vs {}",
ab_c.b(),
a_bc.b()
);
assert!(
(ab_c.a() as i16 - a_bc.a() as i16).unsigned_abs() <= 1,
"a: {} vs {}",
ab_c.a(),
a_bc.a()
);
}
#[test]
fn over_output_alpha_monotonic_with_src_alpha() {
let dst = PackedRgba::rgba(0, 0, 255, 128);
let mut prev_a = 0u8;
for alpha in (0..=255).step_by(5) {
let src = PackedRgba::rgba(255, 0, 0, alpha);
let result = src.over(dst);
assert!(
result.a() >= prev_a,
"alpha monotonicity violated at src_a={}: result_a={} < prev={}",
alpha,
result.a(),
prev_a
);
prev_a = result.a();
}
}
#[test]
fn over_sweep_matches_reference() {
for alpha in (0..=255).step_by(17) {
let src = PackedRgba::rgba(200, 100, 50, alpha);
let dst = PackedRgba::rgba(50, 100, 200, 200);
assert_eq!(
src.over(dst),
reference_over(src, dst),
"mismatch at src_alpha={}",
alpha
);
}
}
}
#[cfg(test)]
mod cell_proptests {
use super::{Cell, CellAttrs, CellContent, GraphemeId, PackedRgba, StyleFlags};
use proptest::prelude::*;
fn arb_packed_rgba() -> impl Strategy<Value = PackedRgba> {
(any::<u8>(), any::<u8>(), any::<u8>(), any::<u8>())
.prop_map(|(r, g, b, a)| PackedRgba::rgba(r, g, b, a))
}
fn arb_grapheme_id() -> impl Strategy<Value = GraphemeId> {
(
0u32..=GraphemeId::MAX_SLOT,
0u16..=GraphemeId::MAX_GENERATION,
0u8..=GraphemeId::MAX_WIDTH,
)
.prop_map(|(slot, generation, width)| GraphemeId::new(slot, generation, width))
}
fn arb_style_flags() -> impl Strategy<Value = StyleFlags> {
any::<u8>().prop_map(StyleFlags::from_bits_truncate)
}
proptest! {
#[test]
fn packed_rgba_roundtrips_all_components(tuple in (any::<u8>(), any::<u8>(), any::<u8>(), any::<u8>())) {
let (r, g, b, a) = tuple;
let c = PackedRgba::rgba(r, g, b, a);
prop_assert_eq!(c.r(), r);
prop_assert_eq!(c.g(), g);
prop_assert_eq!(c.b(), b);
prop_assert_eq!(c.a(), a);
}
#[test]
fn packed_rgba_rgb_always_opaque(tuple in (any::<u8>(), any::<u8>(), any::<u8>())) {
let (r, g, b) = tuple;
let c = PackedRgba::rgb(r, g, b);
prop_assert_eq!(c.a(), 255);
prop_assert_eq!(c.r(), r);
prop_assert_eq!(c.g(), g);
prop_assert_eq!(c.b(), b);
}
#[test]
fn packed_rgba_over_identity_transparent(dst in arb_packed_rgba()) {
let result = PackedRgba::TRANSPARENT.over(dst);
prop_assert_eq!(result, dst);
}
#[test]
fn packed_rgba_over_identity_opaque(tuple in (any::<u8>(), any::<u8>(), any::<u8>(), arb_packed_rgba())) {
let (r, g, b, dst) = tuple;
let src = PackedRgba::rgba(r, g, b, 255);
let result = src.over(dst);
prop_assert_eq!(result, src);
}
#[test]
fn grapheme_id_components_roundtrip(
tuple in (
0u32..=GraphemeId::MAX_SLOT,
0u16..=GraphemeId::MAX_GENERATION,
0u8..=GraphemeId::MAX_WIDTH,
)
) {
let (slot, generation, width) = tuple;
let id = GraphemeId::new(slot, generation, width);
prop_assert_eq!(id.slot(), slot as usize);
prop_assert_eq!(id.generation(), generation);
prop_assert_eq!(id.width(), width as usize);
}
#[test]
fn grapheme_id_raw_roundtrip(id in arb_grapheme_id()) {
let raw = id.raw();
let restored = GraphemeId::from_raw(raw);
prop_assert_eq!(restored.slot(), id.slot());
prop_assert_eq!(restored.width(), id.width());
}
#[test]
fn cell_content_char_roundtrip(c in (0x20u32..0xD800u32).prop_union(0xE000u32..0x110000u32)) {
if let Some(ch) = char::from_u32(c) {
let content = CellContent::from_char(ch);
prop_assert_eq!(content.as_char(), Some(ch));
prop_assert!(!content.is_grapheme());
prop_assert!(!content.is_empty());
prop_assert!(!content.is_continuation());
}
}
#[test]
fn cell_content_grapheme_roundtrip(id in arb_grapheme_id()) {
let content = CellContent::from_grapheme(id);
prop_assert!(content.is_grapheme());
prop_assert_eq!(content.grapheme_id(), Some(id));
prop_assert_eq!(content.width_hint(), id.width());
}
#[test]
fn cell_bits_eq_is_reflexive(
tuple in (
(0x20u32..0x80u32).prop_map(|c| char::from_u32(c).unwrap()),
any::<u8>(), any::<u8>(), any::<u8>(),
arb_style_flags(),
),
) {
let (c, r, g, b, flags) = tuple;
let cell = Cell::from_char(c)
.with_fg(PackedRgba::rgb(r, g, b))
.with_attrs(CellAttrs::new(flags, 0));
prop_assert!(cell.bits_eq(&cell));
}
#[test]
fn cell_bits_eq_detects_fg_difference(
tuple in (
(0x41u32..0x5Bu32).prop_map(|c| char::from_u32(c).unwrap()),
any::<u8>(), any::<u8>(),
),
) {
let (c, r1, r2) = tuple;
prop_assume!(r1 != r2);
let cell1 = Cell::from_char(c).with_fg(PackedRgba::rgb(r1, 0, 0));
let cell2 = Cell::from_char(c).with_fg(PackedRgba::rgb(r2, 0, 0));
prop_assert!(!cell1.bits_eq(&cell2));
}
#[test]
fn cell_attrs_flags_roundtrip(tuple in (arb_style_flags(), 0u32..CellAttrs::LINK_ID_MAX)) {
let (flags, link) = tuple;
let attrs = CellAttrs::new(flags, link);
prop_assert_eq!(attrs.flags(), flags);
prop_assert_eq!(attrs.link_id(), link);
}
#[test]
fn cell_attrs_with_flags_preserves_link(tuple in (arb_style_flags(), 0u32..CellAttrs::LINK_ID_MAX, arb_style_flags())) {
let (flags, link, new_flags) = tuple;
let attrs = CellAttrs::new(flags, link);
let updated = attrs.with_flags(new_flags);
prop_assert_eq!(updated.flags(), new_flags);
prop_assert_eq!(updated.link_id(), link);
}
#[test]
fn cell_attrs_with_link_preserves_flags(tuple in (arb_style_flags(), 0u32..CellAttrs::LINK_ID_MAX, 0u32..CellAttrs::LINK_ID_MAX)) {
let (flags, link1, link2) = tuple;
let attrs = CellAttrs::new(flags, link1);
let updated = attrs.with_link(link2);
prop_assert_eq!(updated.flags(), flags);
prop_assert_eq!(updated.link_id(), link2);
}
#[test]
fn cell_bits_eq_is_symmetric(
tuple in (
(0x41u32..0x5Bu32).prop_map(|c| char::from_u32(c).unwrap()),
(0x41u32..0x5Bu32).prop_map(|c| char::from_u32(c).unwrap()),
arb_packed_rgba(),
arb_packed_rgba(),
),
) {
let (c1, c2, fg1, fg2) = tuple;
let cell_a = Cell::from_char(c1).with_fg(fg1);
let cell_b = Cell::from_char(c2).with_fg(fg2);
prop_assert_eq!(cell_a.bits_eq(&cell_b), cell_b.bits_eq(&cell_a),
"bits_eq is not symmetric");
}
#[test]
fn cell_content_bit31_discriminates(id in arb_grapheme_id()) {
let char_content = CellContent::from_char('A');
prop_assert!(!char_content.is_grapheme());
prop_assert!(char_content.as_char().is_some());
prop_assert!(char_content.grapheme_id().is_none());
let grapheme_content = CellContent::from_grapheme(id);
prop_assert!(grapheme_content.is_grapheme());
prop_assert!(grapheme_content.grapheme_id().is_some());
prop_assert!(grapheme_content.as_char().is_none());
}
#[test]
fn cell_from_char_width_matches_unicode(
c in (0x20u32..0x7Fu32).prop_map(|c| char::from_u32(c).unwrap()),
) {
let cell = Cell::from_char(c);
prop_assert_eq!(cell.width_hint(), 1,
"Cell width hint for '{}' should be 1 for ASCII", c);
}
}
#[test]
fn cell_content_continuation_has_zero_width() {
let cont = CellContent::CONTINUATION;
assert_eq!(cont.width(), 0, "CONTINUATION cell should have width 0");
assert!(cont.is_continuation());
assert!(!cont.is_grapheme());
}
#[test]
fn cell_content_empty_has_zero_width() {
let empty = CellContent::EMPTY;
assert_eq!(empty.width(), 0, "EMPTY cell should have width 0");
assert!(empty.is_empty());
assert!(!empty.is_grapheme());
assert!(!empty.is_continuation());
}
#[test]
fn cell_default_is_empty() {
let cell = Cell::default();
assert!(cell.is_empty());
assert_eq!(cell.width_hint(), 0);
}
}
#[cfg(test)]
mod bit_layout_tests {
use super::GraphemeId;
#[test]
fn grapheme_id_bit_layout_verification() {
let t1 = GraphemeId::new(0xFFFF, 0, 0);
assert_eq!(t1.raw(), 0xFFFF, "Slot 0xFFFF should be 0xFFFF");
let t2 = GraphemeId::new(0, 0x7FF, 0);
assert_eq!(t2.raw(), 0x07FF0000, "Gen 0x7FF should be 0x07FF0000");
let t3 = GraphemeId::new(0, 0, 0xF);
assert_eq!(t3.raw(), 0x78000000, "Width 0xF should be 0x78000000");
let t4 = GraphemeId::new(0xFFFF, 0x7FF, 0xF);
assert_eq!(
t4.raw(),
0x7FFFFFFF,
"Combined max values should be 0x7FFFFFFF"
);
assert_eq!(t4.raw() & 0x80000000, 0, "Bit 31 must be clear");
}
}