use beamterm_data::FontStyle;
use compact_str::CompactString;
use lru::LruCache;
use unicode_width::UnicodeWidthStr;
use crate::{
gl::atlas::{GlyphSlot, SlotId},
is_emoji,
};
pub(crate) const ASCII_SLOTS: u16 = 0x7E - 0x20 + 1;
pub(crate) const NORMAL_CAPACITY: usize = 2048;
pub(crate) const WIDE_CAPACITY: usize = 2048;
const WIDE_BASE: SlotId = NORMAL_CAPACITY as SlotId;
pub(crate) const DYNAMIC_EMOJI_FLAG: u16 = 0x8000;
pub(crate) type CacheKey = (CompactString, FontStyle);
pub(crate) struct GlyphCache {
normal: LruCache<CacheKey, GlyphSlot>,
wide: LruCache<CacheKey, GlyphSlot>,
normal_next: SlotId,
wide_next: SlotId,
}
impl GlyphCache {
pub(crate) fn new() -> Self {
Self {
normal: LruCache::unbounded(),
wide: LruCache::unbounded(),
normal_next: ASCII_SLOTS,
wide_next: WIDE_BASE,
}
}
pub(crate) fn get(&mut self, key: &str, style: FontStyle) -> Option<GlyphSlot> {
if key.len() == 1 && style == FontStyle::Normal {
Some(GlyphSlot::Normal(
(key.chars().next().unwrap() as SlotId).saturating_sub(0x20),
))
} else if key.len() == 1 {
let cache_key = (CompactString::new(key), style);
self.normal.get(&cache_key).copied()
} else if is_emoji(key) {
let cache_key = (CompactString::new(key), FontStyle::Normal);
self.wide.get(&cache_key).copied()
} else {
let cache_key = (CompactString::new(key), style);
if key.width() == 2 {
self.wide.get(&cache_key).copied()
} else {
self.normal.get(&cache_key).copied()
}
}
}
#[cfg(test)]
fn insert(&mut self, key: &str, style: FontStyle) -> (GlyphSlot, Option<CacheKey>) {
self.insert_ex(key, style, false)
}
pub(crate) fn insert_ex(
&mut self,
key: &str,
style: FontStyle,
force_wide: bool,
) -> (GlyphSlot, Option<CacheKey>) {
if key.len() == 1 && style == FontStyle::Normal && !force_wide {
let slot =
GlyphSlot::Normal((key.chars().next().unwrap() as SlotId).saturating_sub(0x20));
return (slot, None);
}
let cache_key = (CompactString::new(key), style);
let is_emoji = is_emoji(key);
if is_emoji || key.width() == 2 || force_wide {
if let Some(&slot) = self.wide.get(&cache_key) {
return (slot, None);
}
let (idx, evicted) =
if (self.wide_next as usize) < (NORMAL_CAPACITY + WIDE_CAPACITY * 2) {
let idx = self.wide_next;
self.wide_next += 2;
(idx, None)
} else {
let (evicted_key, evicted_slot) = self
.wide
.pop_lru()
.expect("wide cache should not be empty when full");
(evicted_slot.slot_id(), Some(evicted_key))
};
let slot = if is_emoji {
GlyphSlot::Emoji(idx | DYNAMIC_EMOJI_FLAG)
} else {
GlyphSlot::Wide(idx)
};
self.wide.put(cache_key, slot);
(slot, evicted)
} else {
if let Some(&slot) = self.normal.get(&cache_key) {
return (slot, None);
}
let (slot, evicted) = if (self.normal_next as usize) < NORMAL_CAPACITY {
let slot = self.normal_next;
self.normal_next += 1;
(GlyphSlot::Normal(slot), None)
} else {
let (evicted_key, evicted_slot) = self
.normal
.pop_lru()
.expect("normal cache should not be empty when full");
(evicted_slot, Some(evicted_key))
};
self.normal.put(cache_key, slot);
(slot, evicted)
}
}
pub(crate) fn len(&self) -> usize {
self.normal.len() + self.wide.len()
}
pub(crate) fn clear(&mut self) {
self.normal.clear();
self.wide.clear();
self.normal_next = ASCII_SLOTS;
self.wide_next = WIDE_BASE;
}
}
impl Default for GlyphCache {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for GlyphCache {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("GlyphCache")
.field("normal", &self.normal.len())
.field("wide", &self.wide.len())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
const S: FontStyle = FontStyle::Normal;
const FIRST_NORMAL_SLOT: SlotId = ASCII_SLOTS;
const EMOJI_SLOT_BASE: SlotId = WIDE_BASE | DYNAMIC_EMOJI_FLAG;
#[test]
fn test_ascii_fast_path() {
let mut cache = GlyphCache::new();
assert_eq!(cache.get("A", S), Some(GlyphSlot::Normal(33)));
assert_eq!(cache.get(" ", S), Some(GlyphSlot::Normal(0)));
assert_eq!(cache.get("~", S), Some(GlyphSlot::Normal(94)));
}
#[test]
fn test_normal_insert_get() {
let mut cache = GlyphCache::new();
let (slot, evicted) = cache.insert("\u{2192}", S);
assert_eq!(slot, GlyphSlot::Normal(FIRST_NORMAL_SLOT));
assert!(evicted.is_none());
assert_eq!(
cache.get("\u{2192}", S),
Some(GlyphSlot::Normal(FIRST_NORMAL_SLOT))
);
assert!(cache.get("\u{2190}", S).is_none());
}
#[test]
fn test_wide_insert_get() {
let mut cache = GlyphCache::new();
let (slot1, _) = cache.insert("\u{1F680}", S);
let (slot2, _) = cache.insert("\u{1F3AE}", S);
assert_eq!(slot1, GlyphSlot::Emoji(EMOJI_SLOT_BASE));
assert_eq!(slot2, GlyphSlot::Emoji(EMOJI_SLOT_BASE + 2));
assert_eq!(
cache.get("\u{1F680}", S),
Some(GlyphSlot::Emoji(EMOJI_SLOT_BASE))
);
assert_eq!(
cache.get("\u{1F3AE}", S),
Some(GlyphSlot::Emoji(EMOJI_SLOT_BASE + 2))
);
}
#[test]
fn test_wide_cjk() {
let mut cache = GlyphCache::new();
let (slot1, _) = cache.insert("\u{4E2D}", S);
let (slot2, _) = cache.insert("\u{6587}", S);
assert_eq!(slot1, GlyphSlot::Wide(WIDE_BASE));
assert_eq!(slot2, GlyphSlot::Wide(WIDE_BASE + 2));
assert_eq!(cache.get("\u{4E2D}", S), Some(GlyphSlot::Wide(WIDE_BASE)));
assert_eq!(
cache.get("\u{6587}", S),
Some(GlyphSlot::Wide(WIDE_BASE + 2))
);
}
#[test]
fn test_mixed_insert() {
let mut cache = GlyphCache::new();
let (s1, _) = cache.insert("\u{2192}", S);
let (s2, _) = cache.insert("\u{1F680}", S);
let (s3, _) = cache.insert("\u{2190}", S);
assert_eq!(s1, GlyphSlot::Normal(FIRST_NORMAL_SLOT));
assert_eq!(s2, GlyphSlot::Emoji(EMOJI_SLOT_BASE));
assert_eq!(s3, GlyphSlot::Normal(FIRST_NORMAL_SLOT + 1));
assert_eq!(
cache.get("\u{2192}", S),
Some(GlyphSlot::Normal(FIRST_NORMAL_SLOT))
);
assert_eq!(
cache.get("\u{1F680}", S),
Some(GlyphSlot::Emoji(EMOJI_SLOT_BASE))
);
assert_eq!(
cache.get("\u{2190}", S),
Some(GlyphSlot::Normal(FIRST_NORMAL_SLOT + 1))
);
}
#[test]
fn test_style_differentiation() {
let mut cache = GlyphCache::new();
let (slot1, _) = cache.insert("A", FontStyle::Normal);
let (slot2, _) = cache.insert("A", FontStyle::Bold);
assert_eq!(slot1, GlyphSlot::Normal(33));
assert_eq!(slot2, GlyphSlot::Normal(FIRST_NORMAL_SLOT));
assert_eq!(
cache.get("A", FontStyle::Normal),
Some(GlyphSlot::Normal(33))
);
assert_eq!(
cache.get("A", FontStyle::Bold),
Some(GlyphSlot::Normal(FIRST_NORMAL_SLOT))
);
}
#[test]
fn test_reinsert_existing() {
let mut cache = GlyphCache::new();
let (slot1, _) = cache.insert("\u{2192}", S);
let (slot2, evicted) = cache.insert("\u{2192}", S);
assert_eq!(slot1, slot2);
assert!(evicted.is_none());
assert_eq!(cache.len(), 1);
assert_eq!(
cache.get("\u{2192}", S),
Some(GlyphSlot::Normal(FIRST_NORMAL_SLOT))
);
}
}