use crate::color::Color;
bitflags::bitflags! {
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub struct CellFlags: u16 {
const BOLD = 1 << 0;
const DIM = 1 << 1;
const ITALIC = 1 << 2;
const UNDERLINE = 1 << 3;
const BLINK = 1 << 4;
const INVERSE = 1 << 5;
const HIDDEN = 1 << 6;
const STRIKETHROUGH = 1 << 7;
const WIDE_CHAR = 1 << 8;
const WIDE_CHAR_SPACER = 1 << 9;
const WRAPLINE = 1 << 10;
}
}
const CODEPOINT_MASK: u32 = 0x001F_FFFF; const C_COMBINED: u32 = 1 << 21; const C_WIDE: u32 = 1 << 22;
const C_SPACER: u32 = 1 << 23;
const C_WRAP: u32 = 1 << 24;
const CONTENT_MARKER_MASK: u32 = C_WIDE | C_SPACER | C_WRAP;
const COLOR_VALUE_MASK: u32 = 0x00FF_FFFF; const COLOR_MODE_SHIFT: u32 = 24; const CM_DEFAULT: u32 = 0;
const CM_INDEXED: u32 = 1;
const CM_RGB: u32 = 2;
const FG_INVERSE: u32 = 1 << 26;
const FG_BOLD: u32 = 1 << 27;
const FG_UNDERLINE: u32 = 1 << 28;
const FG_BLINK: u32 = 1 << 29;
const FG_HIDDEN: u32 = 1 << 30;
const FG_STRIKE: u32 = 1 << 31;
const FG_FLAG_MASK: u32 = FG_INVERSE | FG_BOLD | FG_UNDERLINE | FG_BLINK | FG_HIDDEN | FG_STRIKE;
const BG_ITALIC: u32 = 1 << 26;
const BG_DIM: u32 = 1 << 27;
const BG_LINK: u32 = 1 << 28;
const BG_FLAG_MASK: u32 = BG_ITALIC | BG_DIM;
fn pack_color(c: Color) -> u32 {
match c {
Color::Default => CM_DEFAULT << COLOR_MODE_SHIFT,
Color::Indexed(i) => (CM_INDEXED << COLOR_MODE_SHIFT) | i as u32,
Color::Rgb(r, g, b) => {
(CM_RGB << COLOR_MODE_SHIFT) | (r as u32) << 16 | (g as u32) << 8 | b as u32
}
}
}
fn unpack_color(w: u32) -> Color {
match (w >> COLOR_MODE_SHIFT) & 0b11 {
CM_INDEXED => Color::Indexed((w & 0xFF) as u8),
CM_RGB => Color::Rgb((w >> 16) as u8, (w >> 8) as u8, w as u8),
_ => Color::Default, }
}
#[inline]
fn flag_words(f: u32) -> (u32, u32, u32) {
let content = (f & 0x0700) << 14; let fg = ((f & 0x0001) << 27) | ((f & 0x0020) << 21) | ((f & 0x0018) << 25) | ((f & 0x00C0) << 24); let bg = ((f & 0x0004) << 24) | ((f & 0x0002) << 26); (content, fg, bg)
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct Cell {
content: u32,
fg: u32,
bg: u32,
}
impl Default for Cell {
fn default() -> Self {
Cell {
content: ' ' as u32,
fg: 0,
bg: 0,
}
}
}
impl core::fmt::Debug for Cell {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Cell")
.field("c", &self.c())
.field("fg", &self.fg())
.field("bg", &self.bg())
.field("flags", &self.flags())
.field("combined", &self.is_combined())
.field("linked", &self.is_linked())
.finish()
}
}
impl Cell {
pub fn from_parts(c: char, fg: Color, bg: Color, flags: CellFlags) -> Self {
let mut cell = Cell {
content: c as u32, fg: pack_color(fg),
bg: pack_color(bg),
};
cell.store_flags(flags);
cell
}
fn store_flags(&mut self, flags: CellFlags) {
let (content, fg, bg) = flag_words(flags.bits() as u32);
self.content = (self.content & !CONTENT_MARKER_MASK) | content;
self.fg = (self.fg & !FG_FLAG_MASK) | fg;
self.bg = (self.bg & !BG_FLAG_MASK) | bg;
}
pub fn c(&self) -> char {
char::from_u32(self.content & CODEPOINT_MASK)
.expect("codepoint bits always hold a valid char")
}
pub fn fg(&self) -> Color {
unpack_color(self.fg)
}
pub fn bg(&self) -> Color {
unpack_color(self.bg)
}
pub fn flags(&self) -> CellFlags {
let bits = ((self.content & CONTENT_MARKER_MASK) >> 14) | ((self.fg & FG_BOLD) >> 27) | ((self.fg & FG_INVERSE) >> 21) | ((self.fg & (FG_UNDERLINE | FG_BLINK)) >> 25) | ((self.fg & (FG_HIDDEN | FG_STRIKE)) >> 24) | ((self.bg & BG_ITALIC) >> 24) | ((self.bg & BG_DIM) >> 26); CellFlags::from_bits_retain(bits as u16)
}
pub fn is_combined(&self) -> bool {
self.content & C_COMBINED != 0
}
pub fn is_linked(&self) -> bool {
self.bg & BG_LINK != 0
}
pub fn set_c(&mut self, c: char) {
self.content = (self.content & !CODEPOINT_MASK) | c as u32;
}
pub fn set_bg(&mut self, bg: Color) {
self.bg = pack_color(bg) | (self.bg & !(COLOR_VALUE_MASK | (0b11 << COLOR_MODE_SHIFT)));
}
pub fn set_combined(&mut self, on: bool) {
if on {
self.content |= C_COMBINED;
} else {
self.content &= !C_COMBINED;
}
}
pub fn set_linked(&mut self, on: bool) {
if on {
self.bg |= BG_LINK;
} else {
self.bg &= !BG_LINK;
}
}
pub fn insert_flags(&mut self, flags: CellFlags) {
let (content, fg, bg) = flag_words(flags.bits() as u32);
self.content |= content;
self.fg |= fg;
self.bg |= bg;
}
pub fn remove_flags(&mut self, flags: CellFlags) {
let (content, fg, bg) = flag_words(flags.bits() as u32);
self.content &= !content;
self.fg &= !fg;
self.bg &= !bg;
}
pub fn reset(&mut self) {
*self = Cell::default();
}
pub fn is_wide(&self) -> bool {
self.content & C_WIDE != 0
}
pub fn is_wide_spacer(&self) -> bool {
self.content & C_SPACER != 0
}
pub fn is_wrapline(&self) -> bool {
self.content & C_WRAP != 0
}
}
#[cfg(test)]
mod tests {
use super::{Cell, CellFlags};
use crate::color::Color;
#[test]
fn cell_is_12_bytes() {
assert_eq!(std::mem::size_of::<Cell>(), 12);
}
#[test]
fn every_colour_round_trips_in_both_words() {
let colours = [
Color::Default,
Color::Indexed(0),
Color::Indexed(255),
Color::Rgb(0, 0, 0),
Color::Rgb(255, 128, 1),
];
for &fg in &colours {
for &bg in &colours {
let cell = Cell::from_parts('x', fg, bg, CellFlags::empty());
assert_eq!(cell.fg(), fg, "fg {fg:?} / bg {bg:?}");
assert_eq!(cell.bg(), bg, "fg {fg:?} / bg {bg:?}");
}
}
}
#[test]
fn every_flag_round_trips() {
let all = CellFlags::all();
for bit in all.iter() {
let cell = Cell::from_parts('x', Color::Default, Color::Default, bit);
assert_eq!(cell.flags(), bit, "single {bit:?}");
}
let cell = Cell::from_parts('x', Color::Default, Color::Default, all);
assert_eq!(cell.flags(), all, "all flags at once");
}
#[test]
fn codepoint_round_trips_to_the_unicode_max() {
for c in ['a', ' ', '한', '🦀', '\u{10FFFF}'] {
let cell = Cell::from_parts(c, Color::Default, Color::Default, CellFlags::WIDE_CHAR);
assert_eq!(cell.c(), c, "codepoint {c:?}");
assert!(cell.flags().contains(CellFlags::WIDE_CHAR));
}
}
#[test]
fn combined_and_linked_bits_are_independent() {
let mut cell = Cell::from_parts(
'e',
Color::Indexed(3),
Color::Rgb(1, 2, 3),
CellFlags::WIDE_CHAR | CellFlags::DIM,
);
assert!(!cell.is_combined());
assert!(!cell.is_linked());
cell.set_combined(true);
cell.set_linked(true);
assert!(cell.is_combined() && cell.is_linked());
assert_eq!(cell.c(), 'e');
assert_eq!(cell.fg(), Color::Indexed(3));
assert_eq!(
cell.bg(),
Color::Rgb(1, 2, 3),
"link bit shares the bg word"
);
assert!(cell.flags().contains(CellFlags::WIDE_CHAR | CellFlags::DIM));
cell.set_linked(false);
assert!(cell.is_combined() && !cell.is_linked());
cell.set_combined(false);
assert!(!cell.is_combined() && !cell.is_linked());
assert_eq!(
cell.bg(),
Color::Rgb(1, 2, 3),
"bg colour intact after clearing"
);
let spacer = Cell::from_parts(
' ',
Color::Default,
Color::Default,
CellFlags::WIDE_CHAR_SPACER,
);
assert!(spacer.is_wide_spacer());
assert!(!Cell::default().is_wide_spacer());
}
}