use crate::color::Rgba;
use crate::style::{Style, TextAttributes};
use std::borrow::Cow;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct GraphemeId(u32);
impl GraphemeId {
const WIDTH_SHIFT: u32 = 24;
const WIDTH_MASK: u32 = 0x7F << Self::WIDTH_SHIFT;
const ID_MASK: u32 = 0x00FF_FFFF;
pub const MAX_WIDTH: u8 = 127;
#[must_use]
pub const fn new(pool_id: u32, width: u8) -> Self {
let safe_width = if width > Self::MAX_WIDTH {
Self::MAX_WIDTH
} else {
width
};
Self((pool_id & Self::ID_MASK) | ((safe_width as u32) << Self::WIDTH_SHIFT))
}
#[must_use]
pub const fn placeholder(width: u8) -> Self {
Self::new(0, width)
}
#[must_use]
pub const fn pool_id(self) -> u32 {
self.0 & Self::ID_MASK
}
#[must_use]
pub const fn width(self) -> usize {
((self.0 & Self::WIDTH_MASK) >> Self::WIDTH_SHIFT) as usize
}
#[must_use]
pub const fn raw(self) -> u32 {
self.0
}
#[must_use]
pub const fn from_raw(raw: u32) -> Self {
Self(raw)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum CellContent {
Char(char),
Grapheme(GraphemeId),
#[default]
Empty,
Continuation,
}
impl CellContent {
#[must_use]
pub fn display_width(&self) -> usize {
match self {
Self::Char(c) => crate::unicode::display_width_char(*c),
Self::Grapheme(id) => id.width(),
Self::Empty => 1,
Self::Continuation => 0,
}
}
#[must_use]
pub fn is_continuation(&self) -> bool {
matches!(self, Self::Continuation)
}
#[must_use]
pub fn is_empty(&self) -> bool {
matches!(self, Self::Empty)
}
#[must_use]
pub fn is_grapheme(&self) -> bool {
matches!(self, Self::Grapheme(_))
}
#[must_use]
pub fn grapheme_id(&self) -> Option<GraphemeId> {
match self {
Self::Grapheme(id) => Some(*id),
_ => None,
}
}
#[must_use]
pub fn as_char(&self) -> Option<char> {
match self {
Self::Char(c) => Some(*c),
_ => None,
}
}
#[must_use]
pub fn as_str_without_pool(&self) -> Option<Cow<'static, str>> {
match self {
Self::Char(c) => {
let mut buf = [0u8; 4];
Some(Cow::Owned(c.encode_utf8(&mut buf).to_owned()))
}
Self::Grapheme(_) => None, Self::Empty => Some(Cow::Borrowed(" ")),
Self::Continuation => Some(Cow::Borrowed("")),
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Cell {
pub content: CellContent,
pub fg: Rgba,
pub bg: Rgba,
pub attributes: TextAttributes,
}
impl Cell {
#[must_use]
pub fn new(ch: char, style: Style) -> Self {
Self {
content: CellContent::Char(ch),
fg: style.fg.unwrap_or(Rgba::WHITE),
bg: style.bg.unwrap_or(Rgba::TRANSPARENT),
attributes: style.attributes,
}
}
#[must_use]
pub fn from_grapheme(s: &str, style: Style) -> Self {
let content = if s.chars().count() == 1 {
CellContent::Char(s.chars().next().unwrap())
} else {
let width = crate::unicode::display_width(s);
CellContent::Grapheme(GraphemeId::placeholder(width as u8))
};
Self {
content,
fg: style.fg.unwrap_or(Rgba::WHITE),
bg: style.bg.unwrap_or(Rgba::TRANSPARENT),
attributes: style.attributes,
}
}
#[must_use]
pub fn transparent() -> Self {
Self {
content: CellContent::Empty,
fg: Rgba::TRANSPARENT,
bg: Rgba::TRANSPARENT,
attributes: TextAttributes::empty(),
}
}
#[must_use]
pub fn clear(bg: Rgba) -> Self {
Self {
content: CellContent::Empty,
fg: Rgba::WHITE,
bg,
attributes: TextAttributes::empty(),
}
}
#[must_use]
pub fn continuation(bg: Rgba) -> Self {
Self {
content: CellContent::Continuation,
fg: Rgba::WHITE,
bg,
attributes: TextAttributes::empty(),
}
}
#[must_use]
pub fn display_width(&self) -> usize {
self.content.display_width()
}
#[must_use]
pub fn is_continuation(&self) -> bool {
self.content.is_continuation()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.content.is_empty()
}
pub fn write_content<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
match &self.content {
CellContent::Char(c) => write!(w, "{c}"),
CellContent::Grapheme(id) => {
for _ in 0..id.width() {
write!(w, " ")?;
}
Ok(())
}
CellContent::Empty => write!(w, " "),
CellContent::Continuation => Ok(()),
}
}
pub fn write_content_with_pool<W, F>(&self, w: &mut W, pool_lookup: F) -> std::io::Result<()>
where
W: std::io::Write,
F: Fn(GraphemeId) -> Option<String>,
{
match &self.content {
CellContent::Char(c) => write!(w, "{c}"),
CellContent::Grapheme(id) => {
if let Some(s) = pool_lookup(*id) {
write!(w, "{s}")
} else {
for _ in 0..id.width() {
write!(w, " ")?;
}
Ok(())
}
}
CellContent::Empty => write!(w, " "),
CellContent::Continuation => Ok(()),
}
}
pub fn apply_style(&mut self, style: Style) {
if let Some(fg) = style.fg {
self.fg = fg;
}
if let Some(bg) = style.bg {
self.bg = bg;
}
self.attributes = self.attributes.merge(style.attributes);
}
pub fn blend_with_opacity(&mut self, opacity: f32) {
self.fg = self.fg.multiply_alpha(opacity);
self.bg = self.bg.multiply_alpha(opacity);
}
#[inline]
#[must_use]
pub fn bits_eq(&self, other: &Self) -> bool {
self.content == other.content
&& self.fg.bits_eq(other.fg)
&& self.bg.bits_eq(other.bg)
&& self.attributes == other.attributes
}
#[must_use]
pub fn blend_over(self, background: &Cell) -> Cell {
let (content, attributes) = if self.content.is_empty() {
(background.content, background.attributes)
} else {
(self.content, self.attributes)
};
Cell {
content,
fg: self.fg.blend_over(background.fg),
bg: self.bg.blend_over(background.bg),
attributes,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_grapheme_id_encoding() {
let id = GraphemeId::new(0x0012_3456, 2);
assert_eq!(id.pool_id(), 0x0012_3456);
assert_eq!(id.width(), 2);
}
#[test]
fn test_grapheme_id_max_values() {
let id = GraphemeId::new(0x00FF_FFFF, 127);
assert_eq!(id.pool_id(), 0x00FF_FFFF);
assert_eq!(id.width(), 127);
}
#[test]
fn test_grapheme_id_overflow_masked() {
let id = GraphemeId::new(0x01FF_FFFF, 2);
assert_eq!(id.pool_id(), 0x00FF_FFFF); }
#[test]
fn test_grapheme_id_width_saturation() {
let id128 = GraphemeId::new(1, 128);
assert_eq!(id128.width(), 127, "width 128 should saturate to 127");
let id255 = GraphemeId::new(1, 255);
assert_eq!(id255.width(), 127, "width 255 should saturate to 127");
let id127 = GraphemeId::new(1, 127);
assert_eq!(id127.width(), 127);
let id0 = GraphemeId::new(1, 0);
assert_eq!(id0.width(), 0);
}
#[test]
fn test_grapheme_id_placeholder() {
let id = GraphemeId::placeholder(2);
assert_eq!(id.pool_id(), 0);
assert_eq!(id.width(), 2);
}
#[test]
fn test_grapheme_id_roundtrip() {
let id = GraphemeId::new(12345, 2);
let raw = id.raw();
let restored = GraphemeId::from_raw(raw);
assert_eq!(id, restored);
}
#[test]
fn test_grapheme_id_is_copy() {
let id = GraphemeId::new(1, 2);
let id2 = id; assert_eq!(id, id2);
}
#[test]
fn test_cell_content_is_copy() {
let content = CellContent::Char('A');
let content2 = content; assert_eq!(content, content2);
}
#[test]
fn test_cell_content_grapheme_width() {
let id = GraphemeId::new(42, 2);
let content = CellContent::Grapheme(id);
assert_eq!(content.display_width(), 2);
assert!(content.is_grapheme());
assert_eq!(content.grapheme_id(), Some(id));
}
#[test]
fn test_cell_content_as_str_without_pool() {
assert_eq!(
CellContent::Char('A').as_str_without_pool(),
Some(std::borrow::Cow::Owned("A".to_string()))
);
assert_eq!(
CellContent::Empty.as_str_without_pool(),
Some(std::borrow::Cow::Borrowed(" "))
);
assert_eq!(
CellContent::Continuation.as_str_without_pool(),
Some(std::borrow::Cow::Borrowed(""))
);
assert!(
CellContent::Grapheme(GraphemeId::placeholder(2))
.as_str_without_pool()
.is_none()
);
}
#[test]
fn test_cell_new() {
let cell = Cell::new('A', Style::fg(Rgba::RED));
assert!(matches!(cell.content, CellContent::Char('A')));
assert_eq!(cell.fg, Rgba::RED);
assert_eq!(cell.display_width(), 1);
}
#[test]
fn test_cell_is_copy() {
let cell = Cell::new('A', Style::NONE);
let cell2 = cell; assert_eq!(cell, cell2);
}
#[test]
fn test_cell_grapheme() {
let cell = Cell::from_grapheme("π¨βπ©βπ§", Style::NONE);
assert!(matches!(cell.content, CellContent::Grapheme(_)));
assert_eq!(cell.display_width(), 2);
}
#[test]
fn test_cell_grapheme_single_char_optimization() {
let cell = Cell::from_grapheme("A", Style::NONE);
assert!(matches!(cell.content, CellContent::Char('A')));
}
#[test]
fn test_blend_over_attributes_override_for_content() {
let bg = Cell::new('A', Style::bold());
let fg = Cell::new('B', Style::NONE);
let fg_attrs = fg.attributes;
let blended = fg.blend_over(&bg);
assert_eq!(blended.content, CellContent::Char('B'));
assert_eq!(blended.attributes, fg_attrs);
}
#[test]
fn test_blend_over_empty_preserves_background_attrs_and_link() {
let bg = Cell::new(
'A',
Style::builder()
.fg(Rgba::RED)
.bg(Rgba::BLACK)
.bold()
.link(7)
.build(),
);
let fg = Cell::transparent();
let blended = fg.blend_over(&bg);
assert_eq!(blended, bg);
}
#[test]
fn test_cell_clear() {
let cell = Cell::clear(Rgba::BLACK);
assert!(cell.is_empty());
assert_eq!(cell.bg, Rgba::BLACK);
}
#[test]
fn test_cell_continuation() {
let cell = Cell::continuation(Rgba::BLACK);
assert!(cell.is_continuation());
assert_eq!(cell.display_width(), 0);
}
#[test]
fn test_wide_char() {
let cell = Cell::new('ζΌ’', Style::NONE);
assert_eq!(cell.display_width(), 2);
}
#[test]
fn test_write_content_with_pool() {
let cell = Cell::new('A', Style::NONE);
let mut buf = Vec::new();
cell.write_content_with_pool(&mut buf, |_| None).unwrap();
assert_eq!(&buf, b"A");
let id = GraphemeId::new(42, 2);
let grapheme_cell = Cell {
content: CellContent::Grapheme(id),
fg: Rgba::WHITE,
bg: Rgba::BLACK,
attributes: TextAttributes::empty(),
};
buf.clear();
grapheme_cell
.write_content_with_pool(&mut buf, |gid| {
if gid.pool_id() == 42 {
Some("π".to_string())
} else {
None
}
})
.unwrap();
assert_eq!(String::from_utf8_lossy(&buf), "π");
}
#[test]
fn test_cell_default() {
let cell = Cell::default();
assert!(cell.content.is_empty());
assert_eq!(cell.fg, Rgba::default());
assert_eq!(cell.bg, Rgba::default());
assert_eq!(cell.attributes, TextAttributes::empty());
}
#[test]
fn test_cell_with_style() {
let style = Style::fg(Rgba::RED)
.with_bg(Rgba::BLUE)
.with_bold()
.with_italic();
let cell = Cell::new('X', style);
assert_eq!(cell.fg, Rgba::RED);
assert_eq!(cell.bg, Rgba::BLUE);
assert!(cell.attributes.contains(TextAttributes::BOLD));
assert!(cell.attributes.contains(TextAttributes::ITALIC));
}
#[test]
fn test_cell_with_fg_bg() {
let cell = Cell::new('A', Style::fg(Rgba::GREEN).with_bg(Rgba::BLACK));
assert_eq!(cell.fg, Rgba::GREEN);
assert_eq!(cell.bg, Rgba::BLACK);
}
#[test]
fn test_cell_eq_same() {
let cell1 = Cell::new('A', Style::fg(Rgba::RED));
let cell2 = Cell::new('A', Style::fg(Rgba::RED));
assert_eq!(cell1, cell2);
assert!(cell1.bits_eq(&cell2));
}
#[test]
fn test_cell_eq_different_char() {
let cell1 = Cell::new('A', Style::fg(Rgba::RED));
let cell2 = Cell::new('B', Style::fg(Rgba::RED));
assert_ne!(cell1, cell2);
assert!(!cell1.bits_eq(&cell2));
}
#[test]
fn test_cell_eq_different_style() {
let cell1 = Cell::new('A', Style::fg(Rgba::RED));
let cell2 = Cell::new('A', Style::fg(Rgba::BLUE));
assert_ne!(cell1, cell2);
assert!(!cell1.bits_eq(&cell2));
}
#[test]
fn test_cell_eq_different_attributes() {
let cell1 = Cell::new('A', Style::bold());
let cell2 = Cell::new('A', Style::italic());
assert_ne!(cell1, cell2);
assert!(!cell1.bits_eq(&cell2));
}
#[test]
fn test_cell_cjk_characters() {
assert_eq!(Cell::new('δΈ', Style::NONE).display_width(), 2);
assert_eq!(Cell::new('ζ₯', Style::NONE).display_width(), 2);
assert_eq!(Cell::new('ν', Style::NONE).display_width(), 2);
}
#[test]
fn test_cell_emoji_handling() {
let cell = Cell::from_grapheme("π", Style::NONE);
assert_eq!(cell.display_width(), 2);
let cell = Cell::from_grapheme("π", Style::NONE);
assert_eq!(cell.display_width(), 2);
}
#[test]
fn test_cell_combining_chars() {
let cell = Cell::from_grapheme("Γ©", Style::NONE); assert_eq!(cell.display_width(), 1);
let combined = Cell::from_grapheme("e\u{0301}", Style::NONE);
assert_eq!(combined.display_width(), 1);
}
#[test]
fn test_cell_content_display_width_all_variants() {
assert_eq!(CellContent::Char('a').display_width(), 1);
assert_eq!(CellContent::Char('δΈ').display_width(), 2);
assert_eq!(CellContent::Empty.display_width(), 1);
assert_eq!(CellContent::Continuation.display_width(), 0);
let id = GraphemeId::new(1, 3);
assert_eq!(CellContent::Grapheme(id).display_width(), 3);
}
#[test]
fn test_cell_content_is_empty() {
assert!(CellContent::Empty.is_empty());
assert!(!CellContent::Char('A').is_empty());
assert!(!CellContent::Continuation.is_empty());
assert!(!CellContent::Grapheme(GraphemeId::placeholder(2)).is_empty());
}
#[test]
fn test_cell_content_is_continuation() {
assert!(CellContent::Continuation.is_continuation());
assert!(!CellContent::Empty.is_continuation());
assert!(!CellContent::Char('A').is_continuation());
assert!(!CellContent::Grapheme(GraphemeId::placeholder(2)).is_continuation());
}
#[test]
fn test_cell_content_as_char() {
assert_eq!(CellContent::Char('A').as_char(), Some('A'));
assert_eq!(CellContent::Char('δΈ').as_char(), Some('δΈ'));
assert_eq!(CellContent::Empty.as_char(), None);
assert_eq!(CellContent::Continuation.as_char(), None);
assert_eq!(
CellContent::Grapheme(GraphemeId::placeholder(2)).as_char(),
None
);
}
#[test]
fn test_cell_apply_style() {
let mut cell = Cell::new('A', Style::NONE);
assert_eq!(cell.fg, Rgba::WHITE);
cell.apply_style(Style::fg(Rgba::RED).with_bold());
assert_eq!(cell.fg, Rgba::RED);
assert!(cell.attributes.contains(TextAttributes::BOLD));
}
#[test]
fn test_cell_apply_style_partial() {
let mut cell = Cell::new('A', Style::fg(Rgba::RED).with_bg(Rgba::BLUE));
cell.apply_style(Style::fg(Rgba::GREEN)); assert_eq!(cell.fg, Rgba::GREEN);
assert_eq!(cell.bg, Rgba::BLUE); }
#[test]
fn test_cell_blend_with_opacity() {
let mut cell = Cell::new('A', Style::fg(Rgba::WHITE).with_bg(Rgba::BLACK));
cell.blend_with_opacity(0.5);
assert!(cell.fg.a < 1.0);
assert!(cell.bg.a < 1.0);
}
#[test]
fn test_cell_bits_eq_vs_eq() {
let cell1 = Cell::new('A', Style::fg(Rgba::RED));
let cell2 = Cell::new('A', Style::fg(Rgba::RED));
assert_eq!(cell1 == cell2, cell1.bits_eq(&cell2));
let cell3 = Cell::new('B', Style::fg(Rgba::RED));
assert_eq!(cell1 == cell3, cell1.bits_eq(&cell3));
}
#[test]
fn test_cell_write_content_empty() {
let cell = Cell::clear(Rgba::BLACK);
let mut buf = Vec::new();
cell.write_content(&mut buf).unwrap();
assert_eq!(&buf, b" ");
}
#[test]
fn test_cell_write_content_continuation() {
let cell = Cell::continuation(Rgba::BLACK);
let mut buf = Vec::new();
cell.write_content(&mut buf).unwrap();
assert!(buf.is_empty()); }
#[test]
fn test_cell_write_content_grapheme_placeholder() {
let id = GraphemeId::new(42, 2);
let cell = Cell {
content: CellContent::Grapheme(id),
fg: Rgba::WHITE,
bg: Rgba::BLACK,
attributes: TextAttributes::empty(),
};
let mut buf = Vec::new();
cell.write_content(&mut buf).unwrap();
assert_eq!(&buf, b" "); }
#[test]
fn test_grapheme_cluster_storage() {
let cell = Cell::from_grapheme("π¨βπ©βπ§βπ¦", Style::NONE);
assert!(matches!(cell.content, CellContent::Grapheme(_)));
assert_eq!(cell.display_width(), 2);
let flag = Cell::from_grapheme("πΊπΈ", Style::NONE);
assert!(matches!(flag.content, CellContent::Grapheme(_)));
assert_eq!(flag.display_width(), 2);
}
#[test]
fn test_grapheme_width_calculation() {
let id = GraphemeId::new(100, 4);
assert_eq!(id.width(), 4);
let content = CellContent::Grapheme(id);
assert_eq!(content.display_width(), 4);
let cell = Cell {
content,
fg: Rgba::WHITE,
bg: Rgba::BLACK,
attributes: TextAttributes::empty(),
};
assert_eq!(cell.display_width(), 4);
}
#[test]
fn test_grapheme_id_default() {
let id = GraphemeId::default();
assert_eq!(id.pool_id(), 0);
assert_eq!(id.width(), 0);
}
#[test]
fn test_cell_zero_width_chars() {
let cell = Cell::new('\u{200B}', Style::NONE); assert_eq!(cell.display_width(), 0);
}
#[test]
fn test_cell_blend_over_transparent() {
let bg = Cell::new('A', Style::bg(Rgba::RED));
let fg = Cell::transparent();
let blended = fg.blend_over(&bg);
assert_eq!(blended.content, CellContent::Char('A'));
assert_eq!(blended.fg, bg.fg);
assert_eq!(blended.bg, bg.bg);
}
}