1use lru::LruCache;
22use oxitext_core::ShapedRun;
23use std::num::NonZeroUsize;
24use std::sync::{Arc, RwLock};
25
26pub type FontId = u64;
28
29#[derive(Debug, Clone, PartialEq, Eq, Hash)]
31pub struct ShapeKey {
32 pub font_id: FontId,
34 pub text: String,
36 pub axis_values_hash: u64,
39}
40
41impl ShapeKey {
42 pub fn new(font_data: &Arc<[u8]>, text: &str, axis_values_hash: u64) -> Self {
46 Self {
47 font_id: Arc::as_ptr(font_data) as *const u8 as u64,
48 text: text.to_owned(),
49 axis_values_hash,
50 }
51 }
52}
53
54pub struct ShapeCache {
60 inner: RwLock<LruCache<ShapeKey, Arc<ShapedRun>>>,
61}
62
63impl ShapeCache {
64 pub fn new(capacity: usize) -> Self {
69 let cap = NonZeroUsize::new(capacity).unwrap_or(NonZeroUsize::MIN);
70 ShapeCache {
71 inner: RwLock::new(LruCache::new(cap)),
72 }
73 }
74
75 pub fn get(&self, key: &ShapeKey) -> Option<Arc<ShapedRun>> {
80 self.inner.write().ok()?.get(key).cloned()
81 }
82
83 pub fn insert(&self, key: ShapeKey, run: Arc<ShapedRun>) {
88 if let Ok(mut cache) = self.inner.write() {
89 cache.put(key, run);
90 }
91 }
92
93 pub fn len(&self) -> usize {
95 self.inner.read().ok().map_or(0, |g| g.len())
96 }
97
98 pub fn is_empty(&self) -> bool {
100 self.len() == 0
101 }
102}
103
104impl std::fmt::Debug for ShapeCache {
105 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106 let len = self.len();
107 f.debug_struct("ShapeCache").field("len", &len).finish()
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114 use oxitext_core::{ShapedGlyph, ShapedRun};
115
116 fn dummy_run(font_data: Arc<[u8]>) -> Arc<ShapedRun> {
117 Arc::new(ShapedRun {
118 glyphs: smallvec::smallvec![ShapedGlyph {
119 gid: 1,
120 x_advance: 10.0,
121 ..Default::default()
122 }],
123 font_data,
124 })
125 }
126
127 #[test]
128 fn shape_cache_miss_then_hit() {
129 let cache = ShapeCache::new(16);
130 let font: Arc<[u8]> = Arc::from(vec![0u8; 4]);
131 let key = ShapeKey::new(&font, "hello", 0);
132
133 assert!(cache.get(&key).is_none(), "expected miss on empty cache");
134 assert_eq!(cache.len(), 0);
135
136 let run = dummy_run(Arc::clone(&font));
137 cache.insert(key.clone(), Arc::clone(&run));
138
139 let hit = cache.get(&key).expect("expected hit after insert");
140 assert_eq!(hit.glyphs[0].gid, 1);
141 assert_eq!(cache.len(), 1);
142 }
143
144 #[test]
145 fn shape_cache_eviction_at_capacity_one() {
146 let cache = ShapeCache::new(1);
147 let font: Arc<[u8]> = Arc::from(vec![0u8; 4]);
148
149 let key_a = ShapeKey::new(&font, "aaa", 0);
150 let key_b = ShapeKey::new(&font, "bbb", 0);
151
152 let run_a = dummy_run(Arc::clone(&font));
153 let run_b = dummy_run(Arc::clone(&font));
154
155 cache.insert(key_a.clone(), run_a);
156 assert_eq!(cache.len(), 1);
157
158 cache.insert(key_b.clone(), run_b);
159 assert_eq!(
160 cache.len(),
161 1,
162 "capacity 1 — still one entry after second insert"
163 );
164
165 assert!(cache.get(&key_a).is_none(), "key_a should be evicted");
167 assert!(cache.get(&key_b).is_some(), "key_b should be present");
168 }
169
170 #[test]
171 fn shape_cache_zero_capacity_fallback() {
172 let cache = ShapeCache::new(0);
174 let font: Arc<[u8]> = Arc::from(vec![0u8; 4]);
175 let key = ShapeKey::new(&font, "x", 0);
176 let run = dummy_run(Arc::clone(&font));
177 cache.insert(key.clone(), run);
178 assert!(cache.get(&key).is_some());
179 }
180
181 #[test]
182 fn shape_key_identity_uses_arc_pointer() {
183 let bytes = vec![1u8, 2u8, 3u8];
185 let arc1: Arc<[u8]> = Arc::from(bytes.clone());
186 let arc2: Arc<[u8]> = Arc::from(bytes);
187 let k1 = ShapeKey::new(&arc1, "hi", 0);
188 let k2 = ShapeKey::new(&arc2, "hi", 0);
189 assert_ne!(
190 k1, k2,
191 "different Arc allocations must produce different keys"
192 );
193 }
194}