1use std::collections::hash_map::DefaultHasher;
46use std::collections::{HashMap, VecDeque};
47use std::hash::{Hash, Hasher};
48
49use crate::cascade::ComputedStyle;
50use crate::media::MediaContext;
51use crate::node::{Position, State};
52use crate::selector::NodeIdentity;
53
54pub struct ComputeCache {
71 entries: HashMap<u64, ComputedStyle>,
72 order: VecDeque<u64>,
73 capacity: usize,
74 generation: u64,
78}
79
80impl ComputeCache {
81 pub fn new(capacity: usize) -> Self {
84 Self {
85 entries: HashMap::new(),
86 order: VecDeque::new(),
87 capacity,
88 generation: 0,
89 }
90 }
91
92 fn check_generation(&mut self, current_gen: u64) {
97 if self.generation != current_gen {
98 self.clear();
99 self.generation = current_gen;
100 }
101 }
102
103 pub fn get(&mut self, sig: u64, current_gen: u64) -> Option<ComputedStyle> {
116 self.check_generation(current_gen);
117 if self.entries.contains_key(&sig) {
118 if let Some(pos) = self.order.iter().position(|&k| k == sig) {
120 self.order.remove(pos);
121 }
122 self.order.push_back(sig);
123 }
124 self.entries.get(&sig).cloned()
125 }
126
127 pub fn insert(&mut self, sig: u64, value: ComputedStyle, current_gen: u64) {
133 self.check_generation(current_gen);
134 if self.capacity == 0 {
135 return;
136 }
137 if let Some(existing) = self.entries.get_mut(&sig) {
138 *existing = value;
140 if let Some(pos) = self.order.iter().position(|&k| k == sig) {
141 self.order.remove(pos);
142 }
143 self.order.push_back(sig);
144 return;
145 }
146 while self.entries.len() >= self.capacity {
148 if let Some(evicted) = self.order.pop_front() {
149 self.entries.remove(&evicted);
150 } else {
151 break;
152 }
153 }
154 self.entries.insert(sig, value);
155 self.order.push_back(sig);
156 }
157
158 pub fn clear(&mut self) {
160 self.entries.clear();
161 self.order.clear();
162 }
163
164 pub fn len(&self) -> usize {
166 self.entries.len()
167 }
168
169 pub fn is_empty(&self) -> bool {
171 self.entries.is_empty()
172 }
173}
174
175impl Default for ComputeCache {
176 fn default() -> Self {
177 Self::new(0)
178 }
179}
180
181pub(crate) fn node_signature(
204 node_id: &NodeIdentity,
205 parent_sig: Option<u64>,
206 siblings: &[NodeIdentity],
207 media: &MediaContext,
208) -> u64 {
209 let mut h = DefaultHasher::new();
210
211 0xC5_C4_06_14u64.hash(&mut h);
214
215 parent_sig.hash(&mut h);
217
218 node_id.type_name.hash(&mut h);
220 node_id.id.hash(&mut h);
221
222 let mut sorted: Vec<&str> = node_id.classes.iter().map(String::as_str).collect();
224 sorted.sort_unstable();
225 sorted.len().hash(&mut h);
226 for c in sorted {
227 c.hash(&mut h);
228 }
229
230 hash_state(&mut h, node_id.state);
231 hash_position(&mut h, &node_id.position);
232
233 siblings.len().hash(&mut h);
237 for sib in siblings {
238 sib.type_name.hash(&mut h);
239 sib.id.hash(&mut h);
240 let mut sib_classes: Vec<&str> = sib.classes.iter().map(String::as_str).collect();
241 sib_classes.sort_unstable();
242 sib_classes.len().hash(&mut h);
243 for c in sib_classes {
244 c.hash(&mut h);
245 }
246 hash_state(&mut h, sib.state);
247 hash_position(&mut h, &sib.position);
248 }
249
250 media.cols.hash(&mut h);
252 media.rows.hash(&mut h);
253 media.truecolor.hash(&mut h);
254 media.no_color.hash(&mut h);
255
256 h.finish()
257}
258
259fn hash_state(h: &mut DefaultHasher, state: State) {
260 state.focus.hash(h);
261 state.hover.hash(h);
262 state.disabled.hash(h);
263 state.checked.hash(h);
264 state.active.hash(h);
265}
266
267fn hash_position(h: &mut DefaultHasher, pos: &Position) {
268 pos.index.hash(h);
269 pos.sibling_count.hash(h);
270 pos.parent_type.hash(h);
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276 use crate::cascade::ComputedStyle;
277 use crate::node::Position;
278 use crate::selector::NodeIdentity;
279 use ratatui::style::Color as RC;
280
281 fn nid(type_name: &str) -> NodeIdentity {
284 NodeIdentity {
285 type_name: type_name.to_string(),
286 id: None,
287 classes: Vec::new(),
288 state: State::empty(),
289 position: Position::default(),
290 }
291 }
292
293 fn nid_with_classes(type_name: &str, classes: &[&str]) -> NodeIdentity {
294 NodeIdentity {
295 type_name: type_name.to_string(),
296 id: None,
297 classes: classes.iter().map(|s| s.to_string()).collect(),
298 state: State::empty(),
299 position: Position::default(),
300 }
301 }
302
303 fn default_media() -> MediaContext {
304 MediaContext::default()
305 }
306
307 #[test]
308 fn signature_identical_inputs_match() {
309 let a = nid("Button");
310 let b = nid("Button");
311 let m = default_media();
312 assert_eq!(
313 node_signature(&a, None, &[], &m),
314 node_signature(&b, None, &[], &m)
315 );
316 }
317
318 #[test]
319 fn signature_differs_on_type() {
320 let m = default_media();
321 let a = nid("Button");
322 let b = nid("Text");
323 assert_ne!(node_signature(&a, None, &[], &m), node_signature(&b, None, &[], &m));
324 }
325
326 #[test]
327 fn signature_differs_on_id() {
328 let m = default_media();
329 let mut a = nid("Button");
330 a.id = Some("save".to_string());
331 let b = nid("Button");
332 assert_ne!(node_signature(&a, None, &[], &m), node_signature(&b, None, &[], &m));
333 }
334
335 #[test]
336 fn signature_classes_are_order_independent() {
337 let m = default_media();
338 let a = nid_with_classes("Button", &["primary", "large"]);
339 let b = nid_with_classes("Button", &["large", "primary"]);
340 assert_eq!(node_signature(&a, None, &[], &m), node_signature(&b, None, &[], &m));
341 }
342
343 #[test]
344 fn signature_differs_on_classes() {
345 let m = default_media();
346 let a = nid_with_classes("Button", &["primary"]);
347 let b = nid_with_classes("Button", &["secondary"]);
348 assert_ne!(node_signature(&a, None, &[], &m), node_signature(&b, None, &[], &m));
349 }
350
351 #[test]
352 fn signature_differs_on_state() {
353 let m = default_media();
354 let mut a = nid("Button");
355 a.state = State::focus();
356 let b = nid("Button");
357 assert_ne!(node_signature(&a, None, &[], &m), node_signature(&b, None, &[], &m));
358 }
359
360 #[test]
361 fn signature_differs_on_position() {
362 let m = default_media();
363 let mut a = nid("Item");
364 a.position = Position::new(0, 3);
365 let mut b = nid("Item");
366 b.position = Position::new(1, 3);
367 assert_ne!(node_signature(&a, None, &[], &m), node_signature(&b, None, &[], &m));
368 }
369
370 #[test]
371 fn signature_differs_on_parent_sig() {
372 let m = default_media();
373 let n = nid("Text");
374 let s_none = node_signature(&n, None, &[], &m);
375 let s_some = node_signature(&n, Some(42), &[], &m);
376 assert_ne!(s_none, s_some);
377 }
378
379 #[test]
380 fn signature_differs_on_media() {
381 let n = nid("Button");
382 let m1 = MediaContext { cols: 80, ..Default::default() };
383 let m2 = MediaContext { cols: 100, ..Default::default() };
384 assert_ne!(node_signature(&n, None, &[], &m1), node_signature(&n, None, &[], &m2));
385
386 let mt = MediaContext { truecolor: true, ..Default::default() };
388 let mf = MediaContext { truecolor: false, ..Default::default() };
389 assert_ne!(node_signature(&n, None, &[], &mt), node_signature(&n, None, &[], &mf));
390
391 let mc = MediaContext { no_color: true, ..Default::default() };
393 let mn = MediaContext { no_color: false, ..Default::default() };
394 assert_ne!(node_signature(&n, None, &[], &mc), node_signature(&n, None, &[], &mn));
395 }
396
397 #[test]
398 fn signature_transitively_captures_parent() {
399 let m = default_media();
402 let n = nid("Text");
403 let s1 = node_signature(&n, Some(111), &[], &m);
404 let s2 = node_signature(&n, Some(222), &[], &m);
405 assert_ne!(s1, s2);
406 }
407
408 #[test]
409 fn signature_differs_on_siblings() {
410 let m = default_media();
414 let n = nid("Item");
415 let no_sibs = node_signature(&n, None, &[], &m);
416 let with_sib = node_signature(&n, None, std::slice::from_ref(&nid("Item")), &m);
417 assert_ne!(no_sibs, with_sib);
418
419 let with_other = node_signature(&n, None, std::slice::from_ref(&nid("Other")), &m);
421 assert_ne!(with_sib, with_other);
422 }
423
424 fn cs(c: RC) -> ComputedStyle {
427 ComputedStyle::new(crate::style::CssStyle::new().color(c))
428 }
429
430 #[test]
431 fn cache_get_miss_on_empty() {
432 let mut cache = ComputeCache::new(8);
433 assert!(cache.get(1, 0).is_none());
434 assert!(cache.is_empty());
435 assert_eq!(cache.len(), 0);
436 }
437
438 #[test]
439 fn cache_insert_then_get_hit() {
440 let mut cache = ComputeCache::new(8);
441 let val = cs(RC::Red);
442 cache.insert(1, val.clone(), 0);
443 let got = cache.get(1, 0).expect("hit");
444 assert_eq!(got, val);
445 assert_eq!(cache.len(), 1);
446 }
447
448 #[test]
449 fn lru_get_promotes_entry_so_it_survives_eviction() {
450 let mut cache = ComputeCache::new(2);
451 cache.insert(10, cs(RC::Red), 0); cache.insert(20, cs(RC::Blue), 0); let _ = cache.get(10, 0);
455 cache.insert(30, cs(RC::Green), 0); assert!(cache.get(10, 0).is_some(), "A survived because it was promoted");
458 assert!(cache.get(20, 0).is_none(), "B evicted as least-recently-used");
459 assert!(cache.get(30, 0).is_some(), "C present");
460 assert_eq!(cache.len(), 2);
461 }
462
463 #[test]
464 fn lru_insert_update_promotes() {
465 let mut cache = ComputeCache::new(2);
466 cache.insert(10, cs(RC::Red), 0); cache.insert(20, cs(RC::Blue), 0); cache.insert(10, cs(RC::Yellow), 0);
470 cache.insert(30, cs(RC::Green), 0); let got = cache.get(10, 0).expect("A present (promoted by update)");
473 assert_eq!(got.style.color, Some(crate::color::Color::literal(RC::Yellow)));
474 assert!(cache.get(20, 0).is_none(), "B evicted as least-recently-used");
475 assert!(cache.get(30, 0).is_some(), "C present");
476 assert_eq!(cache.len(), 2);
477 }
478
479 #[test]
480 fn lru_miss_does_not_reorder() {
481 let mut cache = ComputeCache::new(2);
482 cache.insert(10, cs(RC::Red), 0); cache.insert(20, cs(RC::Blue), 0); assert!(cache.get(999, 0).is_none());
486 cache.insert(30, cs(RC::Green), 0); assert!(cache.get(10, 0).is_none(), "A evicted (oldest, never promoted)");
489 assert!(cache.get(20, 0).is_some(), "B present");
490 assert!(cache.get(30, 0).is_some(), "C present");
491 assert_eq!(cache.len(), 2);
492 }
493
494 #[test]
495 fn lru_capacity_zero_never_stores() {
496 let mut cache = ComputeCache::new(0);
497 cache.insert(1, cs(RC::Red), 0);
498 assert!(cache.is_empty());
499 assert_eq!(cache.len(), 0);
500 assert!(cache.get(1, 0).is_none());
501 }
502
503 #[test]
504 fn lru_generation_clear_resets_order() {
505 let mut cache = ComputeCache::new(2);
507 cache.insert(10, cs(RC::Red), 0); cache.insert(20, cs(RC::Blue), 0); cache.insert(30, cs(RC::Green), 1); assert!(cache.get(10, 1).is_none(), "A cleared by generation bump");
513 cache.insert(40, cs(RC::Yellow), 1); assert_eq!(cache.len(), 2);
517 let _ = cache.get(30, 1);
519 cache.insert(50, cs(RC::Magenta), 1); assert!(cache.get(30, 1).is_some(), "C survived (promoted)");
521 assert!(cache.get(40, 1).is_none(), "D evicted as LRU of the fresh order");
522 assert!(cache.get(50, 1).is_some(), "E present");
523 }
524
525 #[test]
526 fn cache_generation_mismatch_clears_on_get() {
527 let mut cache = ComputeCache::new(8);
528 cache.insert(1, cs(RC::Red), 0);
529 assert_eq!(cache.len(), 1);
530 let got = cache.get(1, 1);
532 assert!(got.is_none());
533 assert!(cache.is_empty(), "cache cleared by gen mismatch");
534 }
535
536 #[test]
537 fn cache_generation_mismatch_clears_on_insert() {
538 let mut cache = ComputeCache::new(8);
539 cache.insert(1, cs(RC::Red), 0);
540 assert_eq!(cache.len(), 1);
541 cache.insert(2, cs(RC::Blue), 1);
543 assert_eq!(cache.len(), 1, "old entry cleared, only the new one remains");
544 assert!(cache.get(1, 1).is_none());
546 assert!(cache.get(2, 1).is_some());
548 }
549
550 #[test]
551 fn cache_capacity_zero_never_stores() {
552 let mut cache = ComputeCache::new(0);
553 cache.insert(1, cs(RC::Red), 0);
554 assert_eq!(cache.len(), 0);
555 assert!(cache.is_empty());
556 assert!(cache.get(1, 0).is_none());
557 }
558
559 #[test]
560 fn cache_clear_drops_entries() {
561 let mut cache = ComputeCache::new(8);
562 cache.insert(1, cs(RC::Red), 0);
563 cache.insert(2, cs(RC::Blue), 0);
564 cache.clear();
565 assert!(cache.is_empty());
566 assert_eq!(cache.len(), 0);
567 assert!(cache.get(1, 0).is_none());
568 }
569
570 #[test]
571 fn cache_stable_across_same_gen_lookups() {
572 let mut cache = ComputeCache::new(4);
574 for i in 0u64..4 {
575 cache.insert(i, cs(RC::Red), 0);
576 }
577 for i in 0u64..4 {
578 assert!(cache.get(i, 0).is_some());
579 }
580 assert_eq!(cache.len(), 4);
581 }
582}