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;
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
#[repr(u8)]
pub enum UnderlineStyle {
#[default]
None = 0,
Single = 1,
Double = 2,
Curly = 3,
Dotted = 4,
Dashed = 5,
}
impl UnderlineStyle {
fn from_bits(v: u32) -> Self {
match v {
0 => Self::None,
2 => Self::Double,
3 => Self::Curly,
4 => Self::Dotted,
5 => Self::Dashed,
_ => Self::Single,
}
}
}
const CF_USTYLE_SHIFT: u32 = 11;
const CF_USTYLE_MASK: u32 = 0b111 << CF_USTYLE_SHIFT;
impl CellFlags {
pub fn underline_style(self) -> UnderlineStyle {
UnderlineStyle::from_bits((self.bits() as u32 & CF_USTYLE_MASK) >> CF_USTYLE_SHIFT)
}
pub fn set_underline_style(&mut self, style: UnderlineStyle) {
let bits = (self.bits() as u32 & !CF_USTYLE_MASK) | ((style as u32) << CF_USTYLE_SHIFT);
*self = CellFlags::from_bits_retain(bits as u16);
self.set(CellFlags::UNDERLINE, style != UnderlineStyle::None);
}
}
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 C_LEADING_SPACER: u32 = 1 << 25;
const CONTENT_MARKER_MASK: u32 = C_WIDE | C_SPACER | C_WRAP;
const C_USTYLE_SHIFT: u32 = 26;
const C_USTYLE_MASK: u32 = 0b111 << C_USTYLE_SHIFT;
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_UCOLOR: u32 = 1 << 29;
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 mut style = UnderlineStyle::from_bits((f & CF_USTYLE_MASK) >> CF_USTYLE_SHIFT);
if style == UnderlineStyle::None && f & 0x0008 != 0 {
style = UnderlineStyle::Single;
}
let content = ((f & 0x0700) << 14) | ((style as u32) << C_USTYLE_SHIFT); let fg = ((f & 0x0001) << 27) | ((f & 0x0020) << 21) | ((f & 0x0010) << 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())
.field("ucolored", &self.is_ucolored())
.field("leading_spacer", &self.is_leading_spacer())
.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 | C_USTYLE_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 is_blank(&self) -> bool {
self.content & (CODEPOINT_MASK | CONTENT_MARKER_MASK | C_COMBINED | C_LEADING_SPACER)
== ' ' as u32
}
pub fn underline_style(&self) -> UnderlineStyle {
UnderlineStyle::from_bits((self.content & C_USTYLE_MASK) >> C_USTYLE_SHIFT)
}
pub fn fg(&self) -> Color {
unpack_color(self.fg)
}
pub fn bg(&self) -> Color {
unpack_color(self.bg)
}
pub fn flags(&self) -> CellFlags {
let style = (self.content & C_USTYLE_MASK) >> C_USTYLE_SHIFT;
let bits = ((self.content & CONTENT_MARKER_MASK) >> 14) | (style << CF_USTYLE_SHIFT) | if style == 0 { 0 } else { 0x0008 } | ((self.fg & FG_BOLD) >> 27) | ((self.fg & FG_INVERSE) >> 21) | ((self.fg & 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 is_ucolored(&self) -> bool {
self.bg & BG_UCOLOR != 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 set_ucolored(&mut self, on: bool) {
if on {
self.bg |= BG_UCOLOR;
} else {
self.bg &= !BG_UCOLOR;
}
}
pub fn insert_flags(&mut self, flags: CellFlags) {
let (content, fg, bg) = flag_words(flags.bits() as u32);
self.content |= content & !C_USTYLE_MASK;
self.fg |= fg;
self.bg |= bg;
let named = UnderlineStyle::from_bits((content & C_USTYLE_MASK) >> C_USTYLE_SHIFT);
if named != UnderlineStyle::None {
self.set_underline_style(named);
}
}
pub fn remove_flags(&mut self, flags: CellFlags) {
let (content, fg, bg) = flag_words(flags.bits() as u32);
self.content &= !(content & !C_USTYLE_MASK);
self.fg &= !fg;
self.bg &= !bg;
let named = UnderlineStyle::from_bits((content & C_USTYLE_MASK) >> C_USTYLE_SHIFT);
if named != UnderlineStyle::None {
self.set_underline_style(UnderlineStyle::None);
}
}
pub fn set_underline_style(&mut self, style: UnderlineStyle) {
self.content = (self.content & !C_USTYLE_MASK) | ((style as u32) << C_USTYLE_SHIFT);
}
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_leading_spacer(&self) -> bool {
self.content & C_LEADING_SPACER != 0
}
pub fn is_spacer(&self) -> bool {
self.content & (C_SPACER | C_LEADING_SPACER) != 0
}
pub fn clear_leading_spacer(&mut self) {
self.content &= !C_LEADING_SPACER;
}
pub fn set_leading_spacer(&mut self) {
self.content |= C_LEADING_SPACER;
}
pub fn is_wrapline(&self) -> bool {
self.content & C_WRAP != 0
}
}
#[cfg(test)]
mod tests {
use super::{Cell, CellFlags, UnderlineStyle};
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 mut normalised_underline = CellFlags::UNDERLINE;
normalised_underline.set_underline_style(UnderlineStyle::Single);
let all = CellFlags::all();
for bit in all.iter() {
let cell = Cell::from_parts('x', Color::Default, Color::Default, bit);
let expected = if bit == CellFlags::UNDERLINE {
normalised_underline
} else {
bit
};
assert_eq!(cell.flags(), expected, "single {bit:?}");
}
let cell = Cell::from_parts('x', Color::Default, Color::Default, all);
assert_eq!(
cell.flags(),
all.union(normalised_underline),
"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());
}
#[test]
fn the_ucolor_presence_bit_is_independent_of_the_link_bit_and_bg_colour() {
let mut cell = Cell::from_parts('u', Color::Default, Color::Rgb(1, 2, 3), CellFlags::DIM);
assert!(!cell.is_ucolored());
assert!(!cell.is_linked());
cell.set_ucolored(true);
cell.set_linked(true);
assert!(cell.is_ucolored() && cell.is_linked());
assert_eq!(cell.bg(), Color::Rgb(1, 2, 3));
assert!(cell.flags().contains(CellFlags::DIM));
cell.set_linked(false);
assert!(cell.is_ucolored() && !cell.is_linked());
cell.set_ucolored(false);
assert!(!cell.is_ucolored());
assert_eq!(
cell.bg(),
Color::Rgb(1, 2, 3),
"bg colour intact after clearing"
);
}
}