use lru::LruCache;
use std::hash::Hasher;
use std::num::NonZeroUsize;
use std::sync::Arc;
use tracing::debug;
use wyhash::WyHash;
const MAX_TEXT_RUN_CACHE_SIZE: usize = 4096;
#[derive(Clone, Debug)]
pub struct CachedTextRun {
pub glyphs: Arc<Vec<ShapedGlyph>>,
pub font_id: usize,
pub has_emoji: bool,
pub advance_width: f32,
pub font_size: f32,
}
#[derive(Clone, Debug)]
pub struct ShapedGlyph {
pub glyph_id: u32,
pub x_advance: f32,
pub y_advance: f32,
pub x_offset: f32,
pub y_offset: f32,
pub cluster: u32,
}
pub type TextRunKey = u64;
pub struct TextRunCache {
cache: LruCache<TextRunKey, CachedTextRun>,
}
impl TextRunCache {
pub fn new() -> Self {
Self {
cache: LruCache::new(NonZeroUsize::new(MAX_TEXT_RUN_CACHE_SIZE).unwrap()),
}
}
pub fn get(&mut self, key: &TextRunKey) -> Option<&CachedTextRun> {
self.cache.get(key)
}
pub fn insert(&mut self, key: TextRunKey, run: CachedTextRun) {
self.cache.put(key, run);
}
pub fn clear(&mut self) {
self.cache.clear();
debug!("TextRunCache cleared due to font change");
}
pub fn is_full(&self) -> bool {
self.cache.len() >= self.cache.cap().get()
}
pub fn utilization(&self) -> f64 {
self.cache.len() as f64 / self.cache.cap().get() as f64
}
pub fn resize(&mut self, new_capacity: usize) {
let new_cap = NonZeroUsize::new(new_capacity).unwrap();
self.cache.resize(new_cap);
}
pub fn capacity(&self) -> usize {
self.cache.cap().get()
}
pub fn len(&self) -> usize {
self.cache.len()
}
pub fn is_empty(&self) -> bool {
self.cache.is_empty()
}
pub fn peek(&self, key: &TextRunKey) -> Option<&CachedTextRun> {
self.cache.peek(key)
}
}
impl Default for TextRunCache {
fn default() -> Self {
Self::new()
}
}
pub fn create_text_run_key(text: &str, font_id: usize, font_size: f32) -> TextRunKey {
let mut hasher = WyHash::with_seed(0);
for (cluster, ch) in text.chars().enumerate() {
if ch == '\u{FE0E}' || ch == '\u{FE0F}' {
continue;
}
hasher.write_u32(ch as u32);
hasher.write_usize(cluster);
}
hasher.write_usize(text.chars().count());
hasher.write_usize(font_id);
let font_size_scaled = (font_size * 100.0) as u32;
hasher.write_u32(font_size_scaled);
hasher.finish()
}
pub fn create_cached_text_run(
glyphs: Vec<ShapedGlyph>,
font_id: usize,
font_size: f32,
has_emoji: bool,
) -> CachedTextRun {
let advance_width = glyphs.iter().map(|g| g.x_advance).sum();
CachedTextRun {
glyphs: Arc::new(glyphs),
font_id,
has_emoji,
advance_width,
font_size,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_unified_text_run_cache_basic() {
let mut cache = TextRunCache::new();
let key = create_text_run_key("hello world", 0, 12.0);
let run = create_cached_text_run(vec![], 0, 12.0, false);
assert!(cache.get(&key).is_none());
cache.insert(key, run.clone());
assert!(cache.get(&key).is_some());
assert_eq!(cache.len(), 1);
}
#[test]
fn test_position_independence() {
let mut cache = TextRunCache::new();
let key1 = create_text_run_key("hello", 0, 12.0);
let key2 = create_text_run_key("hello", 0, 12.0);
assert_eq!(key1, key2);
let run = create_cached_text_run(vec![], 0, 12.0, false);
cache.insert(key1, run);
assert!(cache.get(&key2).is_some());
}
#[test]
fn test_hash_consistency() {
let key1 = create_text_run_key("test", 0, 12.0);
let key2 = create_text_run_key("test", 0, 12.0);
assert_eq!(key1, key2);
let key3 = create_text_run_key("other", 0, 12.0);
assert_ne!(key1, key3);
let key4 = create_text_run_key("test", 1, 12.0);
assert_ne!(key1, key4);
let key5 = create_text_run_key("test", 0, 14.0);
assert_ne!(key1, key5);
}
#[test]
fn test_lru_eviction() {
let mut cache = TextRunCache::new();
let capacity = cache.capacity();
for i in 0..capacity + 1 {
let key = create_text_run_key(&format!("text{i}"), 0, 12.0);
let run = create_cached_text_run(vec![], 0, 12.0, false);
cache.insert(key, run);
}
assert_eq!(cache.len(), capacity);
let first_key = create_text_run_key("text0", 0, 12.0);
assert!(cache.get(&first_key).is_none());
let last_key = create_text_run_key(&format!("text{capacity}"), 0, 12.0);
assert!(cache.get(&last_key).is_some());
}
#[test]
fn test_cache_resize() {
let mut cache = TextRunCache::new();
for i in 0..10 {
let key = create_text_run_key(&format!("text{i}"), 0, 12.0);
let run = create_cached_text_run(vec![], 0, 12.0, false);
cache.insert(key, run);
}
let new_capacity = 5;
cache.resize(new_capacity);
assert_eq!(cache.capacity(), new_capacity);
assert!(cache.len() <= new_capacity);
}
#[test]
fn test_peek_functionality() {
let mut cache = TextRunCache::new();
let key = create_text_run_key("peek_test", 0, 12.0);
let run = create_cached_text_run(vec![], 0, 12.0, false);
cache.insert(key, run);
assert!(cache.peek(&key).is_some());
assert!(cache.get(&key).is_some());
}
#[test]
fn test_utilization() {
let mut cache = TextRunCache::new();
assert_eq!(cache.utilization(), 0.0);
for i in 0..5 {
let key = create_text_run_key(&format!("util_test{i}"), 0, 12.0);
let run = create_cached_text_run(vec![], 0, 12.0, false);
cache.insert(key, run);
}
let utilization = cache.utilization();
assert!(utilization > 0.0);
assert!(utilization <= 1.0);
}
#[test]
fn test_cache_empty_and_clear() {
let mut cache = TextRunCache::new();
assert!(cache.is_empty());
let key = create_text_run_key("test", 0, 12.0);
let run = create_cached_text_run(vec![], 0, 12.0, false);
cache.insert(key, run);
assert!(!cache.is_empty());
assert_eq!(cache.len(), 1);
cache.clear();
assert!(cache.is_empty());
assert_eq!(cache.len(), 0);
}
}