use compact_str::CompactString;
use unicode_width::UnicodeWidthStr;
use super::{Color, Modifier};
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CellDiff {
#[default]
Normal,
Skip,
AlwaysUpdate,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Cell {
symbol: Option<CompactString>,
pub fg: Color,
pub bg: Color,
pub modifier: Modifier,
pub diff: CellDiff,
pub link: Option<Box<str>>,
}
impl Cell {
pub const EMPTY: Cell = Cell {
symbol: None,
fg: Color::Reset,
bg: Color::Reset,
modifier: Modifier::empty(),
diff: CellDiff::Normal,
link: None,
};
pub fn new(symbol: impl Into<CompactString>) -> Self {
Cell {
symbol: Some(symbol.into()),
..Self::EMPTY
}
}
pub fn symbol(&self) -> &str {
self.symbol.as_deref().unwrap_or(" ")
}
pub fn raw_symbol(&self) -> Option<&str> {
self.symbol.as_deref()
}
pub fn is_spacer(&self) -> bool {
matches!(self.symbol.as_deref(), Some(""))
}
pub fn is_blank(&self) -> bool {
self.symbol.is_none()
}
pub fn cell_width(&self) -> u16 {
match self.symbol.as_deref() {
None => 1,
Some("") => 0,
Some(s) => UnicodeWidthStr::width(s).max(1) as u16,
}
}
pub fn set_symbol(&mut self, symbol: impl Into<CompactString>) -> &mut Self {
self.symbol = Some(symbol.into());
self
}
pub fn set_blank(&mut self) -> &mut Self {
self.symbol = None;
self
}
pub fn set_spacer(&mut self) -> &mut Self {
self.symbol = Some(CompactString::const_new(""));
self
}
pub fn set_fg(&mut self, fg: Color) -> &mut Self {
self.fg = fg;
self
}
pub fn set_bg(&mut self, bg: Color) -> &mut Self {
self.bg = bg;
self
}
pub fn set_modifier(&mut self, modifier: Modifier) -> &mut Self {
self.modifier = modifier;
self
}
pub fn set_link(&mut self, link: Option<&str>) -> &mut Self {
self.link = link.map(|s| s.to_string().into_boxed_str());
self
}
pub fn link(&self) -> Option<&str> {
self.link.as_deref()
}
pub fn apply_style(&mut self, style: super::Style) -> &mut Self {
if let Some(fg) = style.fg {
self.fg = fg;
}
if let Some(bg) = style.bg {
self.bg = bg;
}
self.modifier |= style.add_modifier;
self.modifier.remove(style.sub_modifier);
self
}
pub fn reset(&mut self) -> &mut Self {
let diff = self.diff;
*self = Self::EMPTY;
self.diff = diff;
self
}
}
impl Default for Cell {
fn default() -> Self {
Self::EMPTY
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_is_blank_reset() {
let c = Cell::EMPTY;
assert!(c.is_blank());
assert_eq!(c.fg, Color::Reset);
assert_eq!(c.bg, Color::Reset);
assert_eq!(c.modifier, Modifier::empty());
assert_eq!(c.symbol(), " ");
}
#[test]
fn default_matches_empty() {
assert_eq!(Cell::default(), Cell::EMPTY);
}
#[test]
fn new_stores_symbol() {
let c = Cell::new("A");
assert_eq!(c.symbol(), "A");
assert!(!c.is_blank());
assert!(!c.is_spacer());
}
#[test]
fn ascii_cell_width_1() {
let c = Cell::new("A");
assert_eq!(c.cell_width(), 1);
}
#[test]
fn cjk_cell_width_2() {
let c = Cell::new("中");
assert_eq!(c.cell_width(), 2);
}
#[test]
fn single_emoji_cell_width_2() {
let c = Cell::new("🦀");
assert_eq!(c.cell_width(), 2);
}
#[test]
fn zwj_family_cell_width_2() {
let c = Cell::new("👨\u{200D}👩\u{200D}👧");
assert_eq!(c.cell_width(), 2);
}
#[test]
fn combining_grapheme_width_1() {
let c = Cell::new("e\u{0301}");
assert_eq!(c.cell_width(), 1);
}
#[test]
fn blank_cell_width_1() {
assert_eq!(Cell::EMPTY.cell_width(), 1);
}
#[test]
fn spacer_cell_width_0() {
let mut c = Cell::new("中");
c.set_spacer();
assert_eq!(c.cell_width(), 0);
assert!(c.is_spacer());
}
#[test]
fn set_blank_clears_symbol() {
let mut c = Cell::new("A");
c.set_blank();
assert!(c.is_blank());
assert_eq!(c.symbol(), " ");
}
#[test]
fn set_spacer_sets_empty_string_symbol() {
let mut c = Cell::EMPTY;
c.set_spacer();
assert_eq!(c.raw_symbol(), Some(""));
assert!(c.is_spacer());
}
#[test]
fn spacer_is_not_blank() {
let mut c = Cell::EMPTY;
c.set_spacer();
assert!(!c.is_blank());
assert!(c.is_spacer());
}
#[test]
fn apply_style_writes_fg_bg() {
let mut c = Cell::new("A");
let style = super::super::Style::new()
.fg(Color::Rgb(255, 0, 0))
.bg(Color::Rgb(0, 0, 255));
c.apply_style(style);
assert_eq!(c.fg, Color::Rgb(255, 0, 0));
assert_eq!(c.bg, Color::Rgb(0, 0, 255));
}
#[test]
fn apply_style_none_leaves_fields_alone() {
let mut c = Cell::new("A");
c.set_fg(Color::Rgb(255, 0, 0));
let style = super::super::Style::new(); c.apply_style(style);
assert_eq!(c.fg, Color::Rgb(255, 0, 0)); }
#[test]
fn apply_style_accumulates_add_removes_sub_modifier() {
let mut c = Cell::new("A");
c.set_modifier(Modifier::BOLD);
let style = super::super::Style::new()
.add_modifier(Modifier::ITALIC)
.remove_modifier(Modifier::BOLD);
c.apply_style(style);
assert!(c.modifier.contains(Modifier::ITALIC));
assert!(!c.modifier.contains(Modifier::BOLD));
}
#[test]
fn reset_preserves_diff_marker() {
let mut c = Cell::new("A");
c.fg = Color::Rgb(255, 0, 0);
c.diff = CellDiff::AlwaysUpdate;
c.reset();
assert!(c.is_blank());
assert_eq!(c.fg, Color::Reset);
assert_eq!(c.diff, CellDiff::AlwaysUpdate);
}
#[test]
fn size_budget() {
assert!(
std::mem::size_of::<Cell>() <= 64,
"Cell is {} bytes, over budget",
std::mem::size_of::<Cell>()
);
}
#[test]
fn clone_independent() {
let a = Cell::new("A");
let mut b = a.clone();
b.set_symbol("B");
assert_eq!(a.symbol(), "A");
assert_eq!(b.symbol(), "B");
}
#[test]
fn eq_structural() {
let a = Cell::new("X");
let b = Cell::new("X");
assert_eq!(a, b);
let mut c = a.clone();
c.fg = Color::Rgb(255, 0, 0);
assert_ne!(a, c);
}
#[test]
fn regional_indicator_flag_cell_width_2() {
let c = Cell::new("🇺🇸");
assert_eq!(c.cell_width(), 2);
}
#[test]
fn compact_string_inlines_short_symbols() {
let _ = Cell::new("A"); let _ = Cell::new("中"); let _ = Cell::new("🦀"); let _ = Cell::new("🇺🇸"); let _ = Cell::new("👨\u{200D}👩\u{200D}👧"); }
}