use bitflags::bitflags;
use std::hash::{Hash, Hasher};
#[repr(C)]
#[derive(Clone, Copy, PartialEq, Eq, Default, Hash)]
pub struct Rgb {
pub r: u8,
pub g: u8,
pub b: u8,
}
impl Rgb {
#[inline]
pub const fn new(r: u8, g: u8, b: u8) -> Self {
Self { r, g, b }
}
pub const BLACK: Self = Self::new(0, 0, 0);
pub const WHITE: Self = Self::new(255, 255, 255);
pub const DEFAULT_FG: Self = Self::WHITE;
pub const DEFAULT_BG: Self = Self::BLACK;
#[inline]
pub const fn from_u32(hex: u32) -> Self {
Self::new(
((hex >> 16) & 0xFF) as u8,
((hex >> 8) & 0xFF) as u8,
(hex & 0xFF) as u8,
)
}
}
impl std::fmt::Debug for Rgb {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "#{:02x}{:02x}{:02x}", self.r, self.g, self.b)
}
}
impl From<(u8, u8, u8)> for Rgb {
#[inline]
fn from((r, g, b): (u8, u8, u8)) -> Self {
Self::new(r, g, b)
}
}
impl From<u32> for Rgb {
#[inline]
fn from(hex: u32) -> Self {
Self::new(
((hex >> 16) & 0xFF) as u8,
((hex >> 8) & 0xFF) as u8,
(hex & 0xFF) as u8,
)
}
}
bitflags! {
#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct Modifiers: u8 {
const BOLD = 0b0000_0001;
const DIM = 0b0000_0010;
const ITALIC = 0b0000_0100;
const UNDERLINE = 0b0000_1000;
const BLINK = 0b0001_0000;
const REVERSED = 0b0010_0000;
const HIDDEN = 0b0100_0000;
const STRIKETHROUGH = 0b1000_0000;
}
}
impl std::fmt::Debug for Modifiers {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
bitflags::parser::to_writer(self, f)
}
}
bitflags! {
#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct CellFlags: u8 {
const OVERFLOW = 0b0000_0001;
const DIRTY = 0b0000_0010;
const WIDE_CONTINUATION = 0b0000_0100;
}
}
impl std::fmt::Debug for CellFlags {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
bitflags::parser::to_writer(self, f)
}
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct Cell {
grapheme: [u8; 4],
grapheme_len: u8,
display_width: u8,
fg: Rgb,
bg: Rgb,
modifiers: Modifiers,
flags: CellFlags,
_padding: [u8; 2],
}
const _: () = assert!(
std::mem::size_of::<Cell>() == 16,
"Cell must be exactly 16 bytes for cache efficiency"
);
impl Default for Cell {
fn default() -> Self {
Self::EMPTY
}
}
impl Cell {
pub const EMPTY: Self = Self {
grapheme: [b' ', 0, 0, 0],
grapheme_len: 1,
display_width: 1,
fg: Rgb::DEFAULT_FG,
bg: Rgb::DEFAULT_BG,
modifiers: Modifiers::empty(),
flags: CellFlags::empty(),
_padding: [0, 0],
};
#[inline]
pub fn new(c: char) -> Self {
debug_assert!(c.is_ascii(), "Use Cell::from_char for non-ASCII");
Self {
grapheme: [c as u8, 0, 0, 0],
grapheme_len: 1,
display_width: 1,
fg: Rgb::DEFAULT_FG,
bg: Rgb::DEFAULT_BG,
modifiers: Modifiers::empty(),
flags: CellFlags::empty(),
_padding: [0, 0],
}
}
#[inline]
#[allow(clippy::missing_panics_doc)]
pub fn from_char(c: char) -> Self {
let mut grapheme = [0u8; 4];
let s = c.encode_utf8(&mut grapheme);
let len = u8::try_from(s.len()).unwrap();
let width = u8::try_from(unicode_width::UnicodeWidthChar::width(c).unwrap_or(0)).unwrap();
Self {
grapheme,
grapheme_len: len,
display_width: width,
fg: Rgb::DEFAULT_FG,
bg: Rgb::DEFAULT_BG,
modifiers: Modifiers::empty(),
flags: CellFlags::empty(),
_padding: [0, 0],
}
}
#[inline]
#[allow(clippy::missing_panics_doc)]
pub fn from_grapheme(s: &str) -> Option<Self> {
let bytes = s.as_bytes();
if bytes.len() > 4 {
return None;
}
let mut grapheme = [0u8; 4];
grapheme[..bytes.len()].copy_from_slice(bytes);
let width = u8::try_from(unicode_width::UnicodeWidthStr::width(s)).unwrap_or(1);
Some(Self {
grapheme,
grapheme_len: u8::try_from(bytes.len()).unwrap(),
display_width: width,
fg: Rgb::DEFAULT_FG,
bg: Rgb::DEFAULT_BG,
modifiers: Modifiers::empty(),
flags: CellFlags::empty(),
_padding: [0, 0],
})
}
#[inline]
pub const fn overflow(index: u32, display_width: u8) -> Self {
Self {
grapheme: index.to_le_bytes(),
grapheme_len: 0, display_width,
fg: Rgb::DEFAULT_FG,
bg: Rgb::DEFAULT_BG,
modifiers: Modifiers::empty(),
flags: CellFlags::OVERFLOW,
_padding: [0, 0],
}
}
#[inline]
pub const fn wide_continuation() -> Self {
Self {
grapheme: [0, 0, 0, 0],
grapheme_len: 0,
display_width: 0,
fg: Rgb::DEFAULT_FG,
bg: Rgb::DEFAULT_BG,
modifiers: Modifiers::empty(),
flags: CellFlags::WIDE_CONTINUATION,
_padding: [0, 0],
}
}
#[inline]
#[allow(unsafe_code)]
pub fn grapheme(&self) -> Option<&str> {
if self.flags.contains(CellFlags::OVERFLOW) {
return None;
}
Some(unsafe {
std::str::from_utf8_unchecked(&self.grapheme[..self.grapheme_len as usize])
})
}
#[inline]
pub const fn overflow_index(&self) -> Option<u32> {
if self.flags.contains(CellFlags::OVERFLOW) {
Some(u32::from_le_bytes(self.grapheme))
} else {
None
}
}
#[inline]
pub const fn is_overflow(&self) -> bool {
self.flags.contains(CellFlags::OVERFLOW)
}
#[inline]
pub const fn is_wide_continuation(&self) -> bool {
self.flags.contains(CellFlags::WIDE_CONTINUATION)
}
#[inline]
pub const fn display_width(&self) -> u8 {
self.display_width
}
#[inline]
pub const fn fg(&self) -> Rgb {
self.fg
}
#[inline]
pub const fn bg(&self) -> Rgb {
self.bg
}
#[inline]
pub const fn modifiers(&self) -> Modifiers {
self.modifiers
}
#[inline]
pub const fn flags(&self) -> CellFlags {
self.flags
}
#[inline]
pub const fn set_fg(&mut self, fg: Rgb) -> &mut Self {
self.fg = fg;
self
}
#[inline]
pub const fn set_bg(&mut self, bg: Rgb) -> &mut Self {
self.bg = bg;
self
}
#[inline]
pub const fn set_modifiers(&mut self, modifiers: Modifiers) -> &mut Self {
self.modifiers = modifiers;
self
}
#[inline]
#[must_use]
pub const fn with_fg(mut self, fg: Rgb) -> Self {
self.fg = fg;
self
}
#[inline]
#[must_use]
pub const fn with_bg(mut self, bg: Rgb) -> Self {
self.bg = bg;
self
}
#[inline]
#[must_use]
pub const fn with_modifiers(mut self, modifiers: Modifiers) -> Self {
self.modifiers = modifiers;
self
}
#[inline]
pub const fn reset(&mut self) {
*self = Self::EMPTY;
}
}
impl PartialEq for Cell {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.grapheme == other.grapheme
&& self.grapheme_len == other.grapheme_len
&& self.fg == other.fg
&& self.bg == other.bg
&& self.modifiers == other.modifiers
&& self.flags == other.flags
&& self.display_width == other.display_width
}
}
impl Eq for Cell {}
impl Hash for Cell {
fn hash<H: Hasher>(&self, state: &mut H) {
self.grapheme.hash(state);
self.grapheme_len.hash(state);
self.display_width.hash(state);
self.fg.hash(state);
self.bg.hash(state);
self.modifiers.hash(state);
self.flags.hash(state);
}
}
impl std::fmt::Debug for Cell {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let grapheme = self.grapheme().unwrap_or("<overflow>");
f.debug_struct("Cell")
.field("grapheme", &grapheme)
.field("width", &self.display_width)
.field("fg", &self.fg)
.field("bg", &self.bg)
.field("modifiers", &self.modifiers)
.field("flags", &self.flags)
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cell_size() {
assert_eq!(std::mem::size_of::<Cell>(), 16);
}
#[test]
fn test_rgb_from_tuple() {
let rgb: Rgb = (255, 128, 0).into();
assert_eq!(rgb.r, 255);
assert_eq!(rgb.g, 128);
assert_eq!(rgb.b, 0);
}
#[test]
fn test_rgb_from_hex() {
let rgb: Rgb = 0xFF8000.into();
assert_eq!(rgb.r, 255);
assert_eq!(rgb.g, 128);
assert_eq!(rgb.b, 0);
}
#[test]
fn test_cell_new_ascii() {
let cell = Cell::new('A');
assert_eq!(cell.grapheme(), Some("A"));
assert_eq!(cell.display_width(), 1);
}
#[test]
fn test_cell_from_char_unicode() {
let cell = Cell::from_char('日');
assert_eq!(cell.grapheme(), Some("日"));
assert_eq!(cell.display_width(), 2); }
#[test]
fn test_cell_from_grapheme_fits() {
let cell = Cell::from_grapheme("é").unwrap();
assert_eq!(cell.grapheme(), Some("é"));
assert_eq!(cell.display_width(), 1);
}
#[test]
fn test_cell_from_grapheme_overflow() {
let result = Cell::from_grapheme("👨👩👧");
assert!(result.is_none());
}
#[test]
fn test_cell_overflow() {
let cell = Cell::overflow(42, 2);
assert!(cell.is_overflow());
assert_eq!(cell.overflow_index(), Some(42));
assert_eq!(cell.grapheme(), None);
}
#[test]
fn test_cell_equality() {
let a = Cell::new('A').with_fg(Rgb::new(255, 0, 0));
let b = Cell::new('A').with_fg(Rgb::new(255, 0, 0));
let c = Cell::new('A').with_fg(Rgb::new(0, 255, 0));
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn test_cell_builder_pattern() {
let cell = Cell::new('X')
.with_fg(Rgb::new(255, 0, 0))
.with_bg(Rgb::new(0, 0, 255))
.with_modifiers(Modifiers::BOLD | Modifiers::ITALIC);
assert_eq!(cell.fg(), Rgb::new(255, 0, 0));
assert_eq!(cell.bg(), Rgb::new(0, 0, 255));
assert!(cell.modifiers().contains(Modifiers::BOLD));
assert!(cell.modifiers().contains(Modifiers::ITALIC));
}
#[test]
fn test_modifiers_bitflags() {
let mods = Modifiers::BOLD | Modifiers::UNDERLINE;
assert!(mods.contains(Modifiers::BOLD));
assert!(mods.contains(Modifiers::UNDERLINE));
assert!(!mods.contains(Modifiers::ITALIC));
}
#[test]
fn test_cell_reset() {
let mut cell = Cell::new('X').with_fg(Rgb::new(255, 0, 0));
cell.reset();
assert_eq!(cell, Cell::EMPTY);
}
#[test]
fn test_wide_continuation() {
let cont = Cell::wide_continuation();
assert!(cont.is_wide_continuation());
assert_eq!(cont.display_width(), 0);
}
}