use crate::base::Rgba;
use crate::text;
pub use super::attrs::Attrs;
pub const GLYPH_INLINE_CAP: usize = 10;
pub const GLYPH_POOL_CAP: usize = 4096;
const REPLACEMENT: &str = "\u{FFFD}";
const TAG_CONTINUATION: u8 = 0xFE;
const TAG_POOLED: u8 = 0xFF;
#[derive(Default, Clone, Debug)]
pub struct GlyphPool {
entries: Vec<Box<str>>,
dropped: u32,
}
impl GlyphPool {
pub fn intern(&mut self, cluster: &str) -> Option<u16> {
if let Some(i) = self.entries.iter().position(|e| &**e == cluster) {
return Some(i as u16);
}
if self.entries.len() >= GLYPH_POOL_CAP {
self.dropped = self.dropped.saturating_add(1);
return None;
}
self.entries.push(cluster.into());
Some((self.entries.len() - 1) as u16)
}
pub fn get(&self, id: u16) -> Option<&str> {
self.entries.get(id as usize).map(|s| &**s)
}
pub fn clear(&mut self) {
self.entries.clear();
self.dropped = 0;
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn dropped(&self) -> u32 {
self.dropped
}
}
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
pub struct Glyph {
data: [u8; GLYPH_INLINE_CAP],
len_or_tag: u8,
width: u8,
}
impl Glyph {
pub const EMPTY: Glyph = Glyph {
data: [0; GLYPH_INLINE_CAP],
len_or_tag: 0,
width: 1,
};
pub const CONTINUATION: Glyph = Glyph {
data: [0; GLYPH_INLINE_CAP],
len_or_tag: TAG_CONTINUATION,
width: 0,
};
pub const SPACE: Glyph = {
let mut data = [0u8; GLYPH_INLINE_CAP];
data[0] = b' ';
Glyph {
data,
len_or_tag: 1,
width: 1,
}
};
pub(crate) fn new(s: &str, pool: &mut GlyphPool) -> Option<Glyph> {
use unicode_segmentation::UnicodeSegmentation;
let cluster = s.graphemes(true).next()?;
let width = text::cluster_width(cluster);
if width <= 0 {
return None;
}
Some(Self::from_cluster_unchecked(cluster, width as u8, pool))
}
pub(crate) fn from_cluster_unchecked(cluster: &str, width: u8, pool: &mut GlyphPool) -> Glyph {
let bytes = cluster.as_bytes();
if bytes.len() <= GLYPH_INLINE_CAP {
let mut data = [0u8; GLYPH_INLINE_CAP];
data[..bytes.len()].copy_from_slice(bytes);
return Glyph {
data,
len_or_tag: bytes.len() as u8,
width,
};
}
match pool.intern(cluster) {
Some(id) => {
let mut data = [0u8; GLYPH_INLINE_CAP];
data[0..2].copy_from_slice(&id.to_le_bytes());
Glyph {
data,
len_or_tag: TAG_POOLED,
width,
}
}
None => {
let mut data = [0u8; GLYPH_INLINE_CAP];
data[..REPLACEMENT.len()].copy_from_slice(REPLACEMENT.as_bytes());
Glyph {
data,
len_or_tag: REPLACEMENT.len() as u8,
width: 1,
}
}
}
}
pub const fn is_empty(self) -> bool {
self.len_or_tag == 0
}
pub const fn is_continuation(self) -> bool {
self.len_or_tag == TAG_CONTINUATION
}
pub const fn is_pooled(self) -> bool {
self.len_or_tag == TAG_POOLED
}
pub const fn width(self) -> i32 {
self.width as i32
}
pub(crate) fn pool_id(self) -> u16 {
u16::from_le_bytes([self.data[0], self.data[1]])
}
pub(crate) fn as_str<'a>(&'a self, pool: &'a GlyphPool) -> &'a str {
match self.len_or_tag {
0 | TAG_CONTINUATION => "",
TAG_POOLED => pool.get(self.pool_id()).unwrap_or(REPLACEMENT),
n => std::str::from_utf8(&self.data[..n as usize]).unwrap_or(REPLACEMENT),
}
}
pub(crate) fn content_eq(
&self,
other: &Glyph,
own_pool: &GlyphPool,
other_pool: &GlyphPool,
) -> bool {
match (self.is_pooled(), other.is_pooled()) {
(false, false) => self == other,
(true, true) => {
self.width == other.width && self.as_str(own_pool) == other.as_str(other_pool)
}
_ => false,
}
}
}
impl std::fmt::Debug for Glyph {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.len_or_tag {
0 => write!(f, "Glyph(EMPTY)"),
TAG_CONTINUATION => write!(f, "Glyph(CONT)"),
TAG_POOLED => write!(f, "Glyph(pool#{} w{})", self.pool_id(), self.width),
n => {
let s = std::str::from_utf8(&self.data[..n as usize]).unwrap_or("<bad>");
write!(f, "Glyph({s:?} w{})", self.width)
}
}
}
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub struct Cell {
pub glyph: Glyph,
pub fg: Rgba,
pub bg: Rgba,
pub ul: Rgba,
pub attrs: Attrs,
pub link: u16,
}
const _: () = assert!(std::mem::size_of::<Cell>() == 28);
impl Cell {
pub const EMPTY: Cell = Cell {
glyph: Glyph::EMPTY,
fg: Rgba::TRANSPARENT,
bg: Rgba::TRANSPARENT,
ul: Rgba::TRANSPARENT,
attrs: Attrs::NONE,
link: 0,
};
pub const fn new(glyph: Glyph) -> Cell {
Cell {
glyph,
..Cell::EMPTY
}
}
pub const fn with_fg(self, fg: Rgba) -> Cell {
Cell { fg, ..self }
}
pub const fn with_bg(self, bg: Rgba) -> Cell {
Cell { bg, ..self }
}
pub const fn with_ul(self, ul: Rgba) -> Cell {
Cell { ul, ..self }
}
pub const fn with_attrs(self, attrs: Attrs) -> Cell {
Cell { attrs, ..self }
}
pub const fn with_link(self, link: u16) -> Cell {
Cell { link, ..self }
}
pub const fn is_continuation(&self) -> bool {
self.glyph.is_continuation()
}
pub const fn is_wide_leader(&self) -> bool {
self.glyph.width() >= 2
}
pub const fn blanked(&self) -> Cell {
Cell {
glyph: Glyph::SPACE,
..*self
}
}
pub(crate) const fn continuation_of(leader: &Cell) -> Cell {
Cell {
glyph: Glyph::CONTINUATION,
..*leader
}
}
}
impl Default for Cell {
fn default() -> Cell {
Cell::EMPTY
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cell_is_28_bytes() {
assert_eq!(std::mem::size_of::<Cell>(), 28);
assert_eq!(std::mem::size_of::<Glyph>(), 12);
}
#[test]
fn pool_caps_with_counted_drops() {
let mut pool = GlyphPool::default();
let mut refused = 0u32;
for i in 0..(GLYPH_POOL_CAP + 10) {
let cluster = format!("{i:010}\u{0301}");
if pool.intern(&cluster).is_none() {
refused += 1;
}
}
assert_eq!(pool.len(), GLYPH_POOL_CAP);
assert_eq!(refused, 10);
assert_eq!(pool.dropped(), 10);
assert!(pool.intern(&format!("{:010}\u{0301}", 0)).is_some());
pool.clear();
assert_eq!(pool.dropped(), 0);
}
#[test]
fn glyph_inline_roundtrip() {
let mut pool = GlyphPool::default();
let g = Glyph::new("é", &mut pool).unwrap();
assert!(!g.is_pooled());
assert_eq!(g.as_str(&pool), "é");
assert_eq!(g.width(), 1);
assert_eq!(pool.len(), 0);
let heart = Glyph::new("❤\u{FE0F}", &mut pool).unwrap();
assert!(!heart.is_pooled());
assert_eq!(heart.width(), 2);
}
#[test]
fn glyph_long_cluster_spills_and_dedups() {
let mut pool = GlyphPool::default();
let family = "👨\u{200D}👩\u{200D}👧\u{200D}👦"; assert!(family.len() > GLYPH_INLINE_CAP);
let a = Glyph::new(family, &mut pool).unwrap();
let b = Glyph::new(family, &mut pool).unwrap();
assert!(a.is_pooled());
assert_eq!(a, b, "dedup must return the same id");
assert_eq!(pool.len(), 1);
assert_eq!(a.as_str(&pool), family);
assert_eq!(a.width(), 2);
}
#[test]
fn glyph_rejects_control_and_zero_width() {
let mut pool = GlyphPool::default();
assert!(Glyph::new("\t", &mut pool).is_none());
assert!(Glyph::new("\u{200C}", &mut pool).is_none()); assert!(Glyph::new("", &mut pool).is_none());
}
#[test]
fn content_eq_across_pools() {
let mut pa = GlyphPool::default();
let mut pb = GlyphPool::default();
let long = "👩\u{200D}🚀🏽\u{200D}x"; pb.intern("padding-entry");
let a = Glyph::new(long, &mut pa).unwrap();
let b = Glyph::new(long, &mut pb).unwrap();
assert_ne!(a.pool_id(), b.pool_id());
assert!(a.content_eq(&b, &pa, &pb));
let space = Glyph::SPACE;
assert!(!a.content_eq(&space, &pa, &pb));
assert!(!space.content_eq(&Glyph::EMPTY, &pa, &pb));
}
#[test]
fn attrs_ops() {
let a = Attrs::BOLD | Attrs::UNDERLINE;
assert!(a.contains(Attrs::BOLD));
assert!(!a.contains(Attrs::DIM));
assert!(a.intersects(Attrs::UNDERLINE | Attrs::DIM));
assert_eq!((a - Attrs::BOLD).bits(), Attrs::UNDERLINE.bits());
assert_eq!(Attrs::from_bits_truncate(0xFFFF), Attrs::ALL);
assert_eq!(
format!("{:?}", Attrs::BOLD | Attrs::STRIKE),
"Attrs(BOLD|STRIKE)"
);
}
#[test]
fn blank_and_continuation_keep_style() {
let mut pool = GlyphPool::default();
let leader = Cell::new(Glyph::new("世", &mut pool).unwrap())
.with_fg(Rgba::rgb(1, 2, 3))
.with_attrs(Attrs::BOLD)
.with_link(7);
let cont = Cell::continuation_of(&leader);
assert!(cont.is_continuation());
assert_eq!(cont.fg, leader.fg);
assert_eq!(cont.link, 7);
let blank = leader.blanked();
assert_eq!(blank.glyph, Glyph::SPACE);
assert_eq!(blank.attrs, Attrs::BOLD);
}
}