1use std::collections::{HashMap, VecDeque};
8
9use crate::ShapedText;
10
11#[derive(Hash, Eq, PartialEq, Clone, Debug)]
15pub struct CacheKey {
16 pub text: String,
18 pub font_family: Option<String>,
20 pub font_size_bits: u32,
22 pub max_width_bits: u32,
24}
25
26pub struct ShapingCache {
33 capacity: usize,
34 entries: HashMap<CacheKey, ShapedText>,
35 order: VecDeque<CacheKey>,
37 hits: u64,
38 misses: u64,
39 evictions: u64,
40}
41
42impl ShapingCache {
43 pub fn new(capacity: usize) -> Self {
48 Self {
49 capacity,
50 entries: HashMap::new(),
51 order: VecDeque::new(),
52 hits: 0,
53 misses: 0,
54 evictions: 0,
55 }
56 }
57
58 pub fn get(&mut self, key: &CacheKey) -> Option<&ShapedText> {
64 if self.entries.contains_key(key) {
65 self.hits += 1;
66 if let Some(pos) = self.order.iter().position(|k| k == key) {
68 self.order.remove(pos);
69 }
70 self.order.push_front(key.clone());
71 self.entries.get(key)
72 } else {
73 self.misses += 1;
74 None
75 }
76 }
77
78 pub fn insert(&mut self, key: CacheKey, value: ShapedText) {
83 if self.capacity == 0 {
84 return;
85 }
86 if self.entries.contains_key(&key) {
88 if let Some(pos) = self.order.iter().position(|k| k == &key) {
89 self.order.remove(pos);
90 }
91 self.order.push_front(key.clone());
92 self.entries.insert(key, value);
93 return;
94 }
95 while self.entries.len() >= self.capacity {
97 self.evict_lru();
98 }
99 self.order.push_front(key.clone());
100 self.entries.insert(key, value);
101 }
102
103 pub fn hit_rate(&self) -> f64 {
106 let total = self.hits + self.misses;
107 if total == 0 {
108 0.0
109 } else {
110 self.hits as f64 / total as f64
111 }
112 }
113
114 pub fn stats(&self) -> (u64, u64, u64) {
116 (self.hits, self.misses, self.evictions)
117 }
118
119 pub fn clear(&mut self) {
121 self.entries.clear();
122 self.order.clear();
123 self.hits = 0;
124 self.misses = 0;
125 self.evictions = 0;
126 }
127
128 pub fn len(&self) -> usize {
130 self.entries.len()
131 }
132
133 pub fn is_empty(&self) -> bool {
135 self.entries.is_empty()
136 }
137
138 fn evict_lru(&mut self) {
140 if let Some(lru_key) = self.order.pop_back() {
141 self.entries.remove(&lru_key);
142 self.evictions += 1;
143 }
144 }
145}
146
147#[cfg(test)]
150mod tests {
151 use super::*;
152
153 fn make_key(text: &str) -> CacheKey {
154 CacheKey {
155 text: text.to_owned(),
156 font_family: None,
157 font_size_bits: 16.0_f32.to_bits(),
158 max_width_bits: 0.0_f32.to_bits(),
159 }
160 }
161
162 fn shaped(w: f32, h: f32) -> ShapedText {
163 ShapedText {
164 lines: Vec::new(),
165 total_width: w,
166 total_height: h,
167 }
168 }
169
170 #[test]
171 fn cache_hit_after_insert() {
172 let mut cache = ShapingCache::new(8);
173 let k = make_key("hello");
174 cache.insert(k.clone(), shaped(60.0, 16.0));
175 assert!(cache.get(&k).is_some());
176 }
177
178 #[test]
179 fn cache_miss_after_eviction() {
180 let mut cache = ShapingCache::new(1);
182 let a = make_key("A");
183 let b = make_key("B");
184 cache.insert(a.clone(), shaped(10.0, 16.0));
185 cache.insert(b.clone(), shaped(10.0, 16.0));
186 assert!(cache.get(&a).is_none(), "A should have been evicted");
187 assert!(cache.get(&b).is_some(), "B should still be present");
188 }
189
190 #[test]
191 fn cache_hit_rate_tracking() {
192 let mut cache = ShapingCache::new(8);
193 let k = make_key("x");
194 cache.insert(k.clone(), shaped(5.0, 16.0));
195
196 cache.get(&k);
198 cache.get(&k);
199 cache.get(&k);
200 cache.get(&make_key("missing1"));
202 cache.get(&make_key("missing2"));
203
204 let rate = cache.hit_rate();
205 assert!(
206 (rate - 0.6).abs() < 1e-9,
207 "hit rate should be 0.6, got {rate}"
208 );
209 }
210
211 #[test]
212 fn cache_clear_resets_everything() {
213 let mut cache = ShapingCache::new(4);
214 let k = make_key("hello");
215 cache.insert(k.clone(), shaped(10.0, 16.0));
216 cache.get(&k);
217 cache.clear();
218 assert!(cache.is_empty());
219 let (h, m, e) = cache.stats();
220 assert_eq!((h, m, e), (0, 0, 0));
221 assert!(cache.get(&k).is_none());
222 }
223
224 #[test]
225 fn cache_lru_order_promotion() {
226 let mut cache = ShapingCache::new(2);
228 let a = make_key("A");
229 let b = make_key("B");
230 let c = make_key("C");
231
232 cache.insert(a.clone(), shaped(1.0, 1.0));
233 cache.insert(b.clone(), shaped(2.0, 2.0));
234 cache.get(&a);
236 cache.insert(c.clone(), shaped(3.0, 3.0));
238
239 assert!(
240 cache.get(&a).is_some(),
241 "A was promoted, must still be present"
242 );
243 assert!(
244 cache.get(&b).is_none(),
245 "B is LRU after A was promoted, must be evicted"
246 );
247 assert!(
248 cache.get(&c).is_some(),
249 "C was just inserted, must be present"
250 );
251 }
252
253 #[test]
254 fn cache_zero_capacity_never_stores() {
255 let mut cache = ShapingCache::new(0);
256 let k = make_key("hello");
257 cache.insert(k.clone(), shaped(10.0, 16.0));
258 assert!(cache.get(&k).is_none());
259 assert!(cache.is_empty());
260 }
261
262 #[test]
263 fn cache_evictions_count() {
264 let mut cache = ShapingCache::new(1);
265 cache.insert(make_key("A"), shaped(1.0, 1.0));
266 cache.insert(make_key("B"), shaped(1.0, 1.0));
267 cache.insert(make_key("C"), shaped(1.0, 1.0));
268 let (_, _, e) = cache.stats();
269 assert_eq!(e, 2, "two evictions should have occurred");
270 }
271}