1use beamterm_data::{FontStyle, Glyph};
8use compact_str::CompactString;
9use lru::LruCache;
10use unicode_width::UnicodeWidthStr;
11
12use crate::{
13 gl::atlas::{GlyphSlot, SlotId},
14 is_emoji,
15};
16
17pub const ASCII_SLOTS: u16 = 0x7E - 0x20 + 1; const NORMAL_CAPACITY: usize = 2048;
22const WIDE_CAPACITY: usize = 1024;
24const WIDE_BASE: SlotId = NORMAL_CAPACITY as SlotId;
25
26pub(crate) type CacheKey = (CompactString, FontStyle);
27
28pub struct GlyphCache {
33 normal: LruCache<CacheKey, GlyphSlot>,
35 wide: LruCache<CacheKey, GlyphSlot>,
37 normal_next: SlotId,
39 wide_next: SlotId,
41}
42
43impl GlyphCache {
44 pub fn new() -> Self {
45 Self {
46 normal: LruCache::unbounded(),
47 wide: LruCache::unbounded(),
48 normal_next: ASCII_SLOTS,
49 wide_next: WIDE_BASE,
50 }
51 }
52
53 pub fn get(&mut self, key: &str, style: FontStyle) -> Option<GlyphSlot> {
55 let cache_key = (CompactString::new(key), style);
56
57 match () {
58 _ if key.len() == 1 && style == FontStyle::Normal => Some(GlyphSlot::Normal(
60 (key.chars().next().unwrap() as SlotId).saturating_sub(0x20),
61 )),
62
63 _ if key.len() == 1 => self.normal.get(&cache_key).copied(),
65
66 _ if is_emoji(key) => self
68 .wide
69 .get(&(CompactString::new(key), FontStyle::Normal))
70 .copied(),
71
72 _ if key.width() == 2 => self.wide.get(&cache_key).copied(),
74
75 _ => self.normal.get(&cache_key).copied(),
77 }
78 }
79
80 pub fn insert(&mut self, key: &str, style: FontStyle) -> (GlyphSlot, Option<CacheKey>) {
82 if key.len() == 1 && style == FontStyle::Normal {
84 let slot =
85 GlyphSlot::Normal((key.chars().next().unwrap() as SlotId).saturating_sub(0x20));
86 return (slot, None);
87 }
88
89 let cache_key = (CompactString::new(key), style);
90 let is_emoji = is_emoji(key);
91
92 if is_emoji || key.width() == 2 {
93 if let Some(&slot) = self.wide.get(&cache_key) {
95 return (slot, None);
96 }
97
98 let (idx, evicted) =
100 if (self.wide_next as usize) < (NORMAL_CAPACITY + WIDE_CAPACITY * 2) {
101 let idx = self.wide_next;
102 self.wide_next += 2;
103 (idx, None)
104 } else {
105 let (evicted_key, evicted_slot) = self
106 .wide
107 .pop_lru()
108 .expect("wide cache should not be empty when full");
109 (evicted_slot.slot_id(), Some(evicted_key))
110 };
111
112 let slot = if is_emoji {
113 GlyphSlot::Emoji(idx | Glyph::EMOJI_FLAG)
114 } else {
115 GlyphSlot::Wide(idx)
116 };
117
118 self.wide.put(cache_key, slot);
119
120 (slot, evicted)
121 } else {
122 if let Some(&slot) = self.normal.get(&cache_key) {
124 return (slot, None);
125 }
126
127 let (slot, evicted) = if (self.normal_next as usize) < NORMAL_CAPACITY {
129 let slot = self.normal_next;
130 self.normal_next += 1;
131 (GlyphSlot::Normal(slot), None)
132 } else {
133 let (evicted_key, evicted_slot) = self
134 .normal
135 .pop_lru()
136 .expect("normal cache should not be empty when full");
137 (evicted_slot, Some(evicted_key))
138 };
139
140 self.normal.put(cache_key, slot);
141 (slot, evicted)
142 }
143 }
144
145 pub fn len(&self) -> usize {
147 self.normal.len() + self.wide.len()
148 }
149
150 pub fn is_empty(&self) -> bool {
151 self.normal.is_empty() && self.wide.is_empty()
152 }
153
154 pub fn clear(&mut self) {
156 self.normal.clear();
157 self.wide.clear();
158
159 self.normal_next = ASCII_SLOTS;
160 self.wide_next = WIDE_BASE;
161 }
162}
163
164impl Default for GlyphCache {
165 fn default() -> Self {
166 Self::new()
167 }
168}
169
170impl std::fmt::Debug for GlyphCache {
171 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172 f.debug_struct("GlyphCache")
173 .field("normal", &self.normal.len())
174 .field("wide", &self.wide.len())
175 .finish()
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 const S: FontStyle = FontStyle::Normal;
184
185 const FIRST_NORMAL_SLOT: SlotId = ASCII_SLOTS; const EMOJI_SLOT_BASE: SlotId = WIDE_BASE | Glyph::EMOJI_FLAG; #[test]
192 fn test_ascii_fast_path() {
193 let mut cache = GlyphCache::new();
196
197 assert_eq!(cache.get("A", S), Some(GlyphSlot::Normal(33)));
199 assert_eq!(cache.get(" ", S), Some(GlyphSlot::Normal(0)));
201 assert_eq!(cache.get("~", S), Some(GlyphSlot::Normal(94)));
203 }
204
205 #[test]
206 fn test_normal_insert_get() {
207 let mut cache = GlyphCache::new();
208
209 let (slot, evicted) = cache.insert("\u{2192}", S);
211 assert_eq!(slot, GlyphSlot::Normal(FIRST_NORMAL_SLOT));
212 assert!(evicted.is_none());
213
214 assert_eq!(
215 cache.get("\u{2192}", S),
216 Some(GlyphSlot::Normal(FIRST_NORMAL_SLOT))
217 );
218 assert!(cache.get("\u{2190}", S).is_none());
219 }
220
221 #[test]
222 fn test_wide_insert_get() {
223 let mut cache = GlyphCache::new();
224
225 let (slot1, _) = cache.insert("\u{1F680}", S);
226 let (slot2, _) = cache.insert("\u{1F3AE}", S);
227
228 assert_eq!(slot1, GlyphSlot::Emoji(EMOJI_SLOT_BASE));
230 assert_eq!(slot2, GlyphSlot::Emoji(EMOJI_SLOT_BASE + 2));
231
232 assert_eq!(
233 cache.get("\u{1F680}", S),
234 Some(GlyphSlot::Emoji(EMOJI_SLOT_BASE))
235 );
236 assert_eq!(
237 cache.get("\u{1F3AE}", S),
238 Some(GlyphSlot::Emoji(EMOJI_SLOT_BASE + 2))
239 );
240 }
241
242 #[test]
243 fn test_wide_cjk() {
244 let mut cache = GlyphCache::new();
245
246 let (slot1, _) = cache.insert("\u{4E2D}", S);
247 let (slot2, _) = cache.insert("\u{6587}", S);
248
249 assert_eq!(slot1, GlyphSlot::Wide(WIDE_BASE));
251 assert_eq!(slot2, GlyphSlot::Wide(WIDE_BASE + 2));
252
253 assert_eq!(cache.get("\u{4E2D}", S), Some(GlyphSlot::Wide(WIDE_BASE)));
254 assert_eq!(
255 cache.get("\u{6587}", S),
256 Some(GlyphSlot::Wide(WIDE_BASE + 2))
257 );
258 }
259
260 #[test]
261 fn test_mixed_insert() {
262 let mut cache = GlyphCache::new();
263
264 let (s1, _) = cache.insert("\u{2192}", S);
266 let (s2, _) = cache.insert("\u{1F680}", S);
267 let (s3, _) = cache.insert("\u{2190}", S);
268
269 assert_eq!(s1, GlyphSlot::Normal(FIRST_NORMAL_SLOT));
270 assert_eq!(s2, GlyphSlot::Emoji(EMOJI_SLOT_BASE));
271 assert_eq!(s3, GlyphSlot::Normal(FIRST_NORMAL_SLOT + 1));
272
273 assert_eq!(
274 cache.get("\u{2192}", S),
275 Some(GlyphSlot::Normal(FIRST_NORMAL_SLOT))
276 );
277 assert_eq!(
278 cache.get("\u{1F680}", S),
279 Some(GlyphSlot::Emoji(EMOJI_SLOT_BASE))
280 );
281 assert_eq!(
282 cache.get("\u{2190}", S),
283 Some(GlyphSlot::Normal(FIRST_NORMAL_SLOT + 1))
284 );
285 }
286
287 #[test]
288 fn test_style_differentiation() {
289 let mut cache = GlyphCache::new();
290
291 let (slot1, _) = cache.insert("A", FontStyle::Normal);
293 let (slot2, _) = cache.insert("A", FontStyle::Bold);
295
296 assert_eq!(slot1, GlyphSlot::Normal(33));
298 assert_eq!(slot2, GlyphSlot::Normal(FIRST_NORMAL_SLOT));
300
301 assert_eq!(
303 cache.get("A", FontStyle::Normal),
304 Some(GlyphSlot::Normal(33))
305 );
306 assert_eq!(
308 cache.get("A", FontStyle::Bold),
309 Some(GlyphSlot::Normal(FIRST_NORMAL_SLOT))
310 );
311 }
312
313 #[test]
314 fn test_reinsert_existing() {
315 let mut cache = GlyphCache::new();
316
317 let (slot1, _) = cache.insert("\u{2192}", S);
319 let (slot2, evicted) = cache.insert("\u{2192}", S);
320
321 assert_eq!(slot1, slot2);
322 assert!(evicted.is_none());
323 assert_eq!(cache.len(), 1);
324 assert_eq!(
325 cache.get("\u{2192}", S),
326 Some(GlyphSlot::Normal(FIRST_NORMAL_SLOT))
327 );
328 }
329}