use std::collections::{HashMap, VecDeque};
use crate::ShapedText;
#[derive(Hash, Eq, PartialEq, Clone, Debug)]
pub struct CacheKey {
pub text: String,
pub font_family: Option<String>,
pub font_size_bits: u32,
pub max_width_bits: u32,
}
pub struct ShapingCache {
capacity: usize,
entries: HashMap<CacheKey, ShapedText>,
order: VecDeque<CacheKey>,
hits: u64,
misses: u64,
evictions: u64,
}
impl ShapingCache {
pub fn new(capacity: usize) -> Self {
Self {
capacity,
entries: HashMap::new(),
order: VecDeque::new(),
hits: 0,
misses: 0,
evictions: 0,
}
}
pub fn get(&mut self, key: &CacheKey) -> Option<&ShapedText> {
if self.entries.contains_key(key) {
self.hits += 1;
if let Some(pos) = self.order.iter().position(|k| k == key) {
self.order.remove(pos);
}
self.order.push_front(key.clone());
self.entries.get(key)
} else {
self.misses += 1;
None
}
}
pub fn insert(&mut self, key: CacheKey, value: ShapedText) {
if self.capacity == 0 {
return;
}
if self.entries.contains_key(&key) {
if let Some(pos) = self.order.iter().position(|k| k == &key) {
self.order.remove(pos);
}
self.order.push_front(key.clone());
self.entries.insert(key, value);
return;
}
while self.entries.len() >= self.capacity {
self.evict_lru();
}
self.order.push_front(key.clone());
self.entries.insert(key, value);
}
pub fn hit_rate(&self) -> f64 {
let total = self.hits + self.misses;
if total == 0 {
0.0
} else {
self.hits as f64 / total as f64
}
}
pub fn stats(&self) -> (u64, u64, u64) {
(self.hits, self.misses, self.evictions)
}
pub fn clear(&mut self) {
self.entries.clear();
self.order.clear();
self.hits = 0;
self.misses = 0;
self.evictions = 0;
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
fn evict_lru(&mut self) {
if let Some(lru_key) = self.order.pop_back() {
self.entries.remove(&lru_key);
self.evictions += 1;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_key(text: &str) -> CacheKey {
CacheKey {
text: text.to_owned(),
font_family: None,
font_size_bits: 16.0_f32.to_bits(),
max_width_bits: 0.0_f32.to_bits(),
}
}
fn shaped(w: f32, h: f32) -> ShapedText {
ShapedText {
lines: Vec::new(),
total_width: w,
total_height: h,
}
}
#[test]
fn cache_hit_after_insert() {
let mut cache = ShapingCache::new(8);
let k = make_key("hello");
cache.insert(k.clone(), shaped(60.0, 16.0));
assert!(cache.get(&k).is_some());
}
#[test]
fn cache_miss_after_eviction() {
let mut cache = ShapingCache::new(1);
let a = make_key("A");
let b = make_key("B");
cache.insert(a.clone(), shaped(10.0, 16.0));
cache.insert(b.clone(), shaped(10.0, 16.0));
assert!(cache.get(&a).is_none(), "A should have been evicted");
assert!(cache.get(&b).is_some(), "B should still be present");
}
#[test]
fn cache_hit_rate_tracking() {
let mut cache = ShapingCache::new(8);
let k = make_key("x");
cache.insert(k.clone(), shaped(5.0, 16.0));
cache.get(&k);
cache.get(&k);
cache.get(&k);
cache.get(&make_key("missing1"));
cache.get(&make_key("missing2"));
let rate = cache.hit_rate();
assert!(
(rate - 0.6).abs() < 1e-9,
"hit rate should be 0.6, got {rate}"
);
}
#[test]
fn cache_clear_resets_everything() {
let mut cache = ShapingCache::new(4);
let k = make_key("hello");
cache.insert(k.clone(), shaped(10.0, 16.0));
cache.get(&k);
cache.clear();
assert!(cache.is_empty());
let (h, m, e) = cache.stats();
assert_eq!((h, m, e), (0, 0, 0));
assert!(cache.get(&k).is_none());
}
#[test]
fn cache_lru_order_promotion() {
let mut cache = ShapingCache::new(2);
let a = make_key("A");
let b = make_key("B");
let c = make_key("C");
cache.insert(a.clone(), shaped(1.0, 1.0));
cache.insert(b.clone(), shaped(2.0, 2.0));
cache.get(&a);
cache.insert(c.clone(), shaped(3.0, 3.0));
assert!(
cache.get(&a).is_some(),
"A was promoted, must still be present"
);
assert!(
cache.get(&b).is_none(),
"B is LRU after A was promoted, must be evicted"
);
assert!(
cache.get(&c).is_some(),
"C was just inserted, must be present"
);
}
#[test]
fn cache_zero_capacity_never_stores() {
let mut cache = ShapingCache::new(0);
let k = make_key("hello");
cache.insert(k.clone(), shaped(10.0, 16.0));
assert!(cache.get(&k).is_none());
assert!(cache.is_empty());
}
#[test]
fn cache_evictions_count() {
let mut cache = ShapingCache::new(1);
cache.insert(make_key("A"), shaped(1.0, 1.0));
cache.insert(make_key("B"), shaped(1.0, 1.0));
cache.insert(make_key("C"), shaped(1.0, 1.0));
let (_, _, e) = cache.stats();
assert_eq!(e, 2, "two evictions should have occurred");
}
}