use std::collections::hash_map::DefaultHasher;
use std::collections::{HashMap, VecDeque};
use std::hash::{Hash, Hasher};
use crate::cascade::ComputedStyle;
use crate::media::MediaContext;
use crate::node::{Position, State};
use crate::selector::NodeIdentity;
pub struct ComputeCache {
entries: HashMap<u64, ComputedStyle>,
order: VecDeque<u64>,
capacity: usize,
generation: u64,
}
impl ComputeCache {
pub fn new(capacity: usize) -> Self {
Self {
entries: HashMap::new(),
order: VecDeque::new(),
capacity,
generation: 0,
}
}
fn check_generation(&mut self, current_gen: u64) {
if self.generation != current_gen {
self.clear();
self.generation = current_gen;
}
}
pub fn get(&mut self, sig: u64, current_gen: u64) -> Option<ComputedStyle> {
self.check_generation(current_gen);
if self.entries.contains_key(&sig) {
if let Some(pos) = self.order.iter().position(|&k| k == sig) {
self.order.remove(pos);
}
self.order.push_back(sig);
}
self.entries.get(&sig).cloned()
}
pub fn insert(&mut self, sig: u64, value: ComputedStyle, current_gen: u64) {
self.check_generation(current_gen);
if self.capacity == 0 {
return;
}
if let Some(existing) = self.entries.get_mut(&sig) {
*existing = value;
if let Some(pos) = self.order.iter().position(|&k| k == sig) {
self.order.remove(pos);
}
self.order.push_back(sig);
return;
}
while self.entries.len() >= self.capacity {
if let Some(evicted) = self.order.pop_front() {
self.entries.remove(&evicted);
} else {
break;
}
}
self.entries.insert(sig, value);
self.order.push_back(sig);
}
pub fn clear(&mut self) {
self.entries.clear();
self.order.clear();
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
impl Default for ComputeCache {
fn default() -> Self {
Self::new(0)
}
}
pub(crate) fn node_signature(
node_id: &NodeIdentity,
parent_sig: Option<u64>,
siblings: &[NodeIdentity],
media: &MediaContext,
) -> u64 {
let mut h = DefaultHasher::new();
0xC5_C4_06_14u64.hash(&mut h);
parent_sig.hash(&mut h);
node_id.type_name.hash(&mut h);
node_id.id.hash(&mut h);
let mut sorted: Vec<&str> = node_id.classes.iter().map(String::as_str).collect();
sorted.sort_unstable();
sorted.len().hash(&mut h);
for c in sorted {
c.hash(&mut h);
}
hash_state(&mut h, node_id.state);
hash_position(&mut h, &node_id.position);
siblings.len().hash(&mut h);
for sib in siblings {
sib.type_name.hash(&mut h);
sib.id.hash(&mut h);
let mut sib_classes: Vec<&str> = sib.classes.iter().map(String::as_str).collect();
sib_classes.sort_unstable();
sib_classes.len().hash(&mut h);
for c in sib_classes {
c.hash(&mut h);
}
hash_state(&mut h, sib.state);
hash_position(&mut h, &sib.position);
}
media.cols.hash(&mut h);
media.rows.hash(&mut h);
media.truecolor.hash(&mut h);
media.no_color.hash(&mut h);
h.finish()
}
fn hash_state(h: &mut DefaultHasher, state: State) {
state.focus.hash(h);
state.hover.hash(h);
state.disabled.hash(h);
state.checked.hash(h);
state.active.hash(h);
}
fn hash_position(h: &mut DefaultHasher, pos: &Position) {
pos.index.hash(h);
pos.sibling_count.hash(h);
pos.parent_type.hash(h);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cascade::ComputedStyle;
use crate::node::Position;
use crate::selector::NodeIdentity;
use ratatui::style::Color as RC;
fn nid(type_name: &str) -> NodeIdentity {
NodeIdentity {
type_name: type_name.to_string(),
id: None,
classes: Vec::new(),
state: State::empty(),
position: Position::default(),
}
}
fn nid_with_classes(type_name: &str, classes: &[&str]) -> NodeIdentity {
NodeIdentity {
type_name: type_name.to_string(),
id: None,
classes: classes.iter().map(|s| s.to_string()).collect(),
state: State::empty(),
position: Position::default(),
}
}
fn default_media() -> MediaContext {
MediaContext::default()
}
#[test]
fn signature_identical_inputs_match() {
let a = nid("Button");
let b = nid("Button");
let m = default_media();
assert_eq!(
node_signature(&a, None, &[], &m),
node_signature(&b, None, &[], &m)
);
}
#[test]
fn signature_differs_on_type() {
let m = default_media();
let a = nid("Button");
let b = nid("Text");
assert_ne!(node_signature(&a, None, &[], &m), node_signature(&b, None, &[], &m));
}
#[test]
fn signature_differs_on_id() {
let m = default_media();
let mut a = nid("Button");
a.id = Some("save".to_string());
let b = nid("Button");
assert_ne!(node_signature(&a, None, &[], &m), node_signature(&b, None, &[], &m));
}
#[test]
fn signature_classes_are_order_independent() {
let m = default_media();
let a = nid_with_classes("Button", &["primary", "large"]);
let b = nid_with_classes("Button", &["large", "primary"]);
assert_eq!(node_signature(&a, None, &[], &m), node_signature(&b, None, &[], &m));
}
#[test]
fn signature_differs_on_classes() {
let m = default_media();
let a = nid_with_classes("Button", &["primary"]);
let b = nid_with_classes("Button", &["secondary"]);
assert_ne!(node_signature(&a, None, &[], &m), node_signature(&b, None, &[], &m));
}
#[test]
fn signature_differs_on_state() {
let m = default_media();
let mut a = nid("Button");
a.state = State::focus();
let b = nid("Button");
assert_ne!(node_signature(&a, None, &[], &m), node_signature(&b, None, &[], &m));
}
#[test]
fn signature_differs_on_position() {
let m = default_media();
let mut a = nid("Item");
a.position = Position::new(0, 3);
let mut b = nid("Item");
b.position = Position::new(1, 3);
assert_ne!(node_signature(&a, None, &[], &m), node_signature(&b, None, &[], &m));
}
#[test]
fn signature_differs_on_parent_sig() {
let m = default_media();
let n = nid("Text");
let s_none = node_signature(&n, None, &[], &m);
let s_some = node_signature(&n, Some(42), &[], &m);
assert_ne!(s_none, s_some);
}
#[test]
fn signature_differs_on_media() {
let n = nid("Button");
let m1 = MediaContext { cols: 80, ..Default::default() };
let m2 = MediaContext { cols: 100, ..Default::default() };
assert_ne!(node_signature(&n, None, &[], &m1), node_signature(&n, None, &[], &m2));
let mt = MediaContext { truecolor: true, ..Default::default() };
let mf = MediaContext { truecolor: false, ..Default::default() };
assert_ne!(node_signature(&n, None, &[], &mt), node_signature(&n, None, &[], &mf));
let mc = MediaContext { no_color: true, ..Default::default() };
let mn = MediaContext { no_color: false, ..Default::default() };
assert_ne!(node_signature(&n, None, &[], &mc), node_signature(&n, None, &[], &mn));
}
#[test]
fn signature_transitively_captures_parent() {
let m = default_media();
let n = nid("Text");
let s1 = node_signature(&n, Some(111), &[], &m);
let s2 = node_signature(&n, Some(222), &[], &m);
assert_ne!(s1, s2);
}
#[test]
fn signature_differs_on_siblings() {
let m = default_media();
let n = nid("Item");
let no_sibs = node_signature(&n, None, &[], &m);
let with_sib = node_signature(&n, None, std::slice::from_ref(&nid("Item")), &m);
assert_ne!(no_sibs, with_sib);
let with_other = node_signature(&n, None, std::slice::from_ref(&nid("Other")), &m);
assert_ne!(with_sib, with_other);
}
fn cs(c: RC) -> ComputedStyle {
ComputedStyle::new(crate::style::CssStyle::new().color(c))
}
#[test]
fn cache_get_miss_on_empty() {
let mut cache = ComputeCache::new(8);
assert!(cache.get(1, 0).is_none());
assert!(cache.is_empty());
assert_eq!(cache.len(), 0);
}
#[test]
fn cache_insert_then_get_hit() {
let mut cache = ComputeCache::new(8);
let val = cs(RC::Red);
cache.insert(1, val.clone(), 0);
let got = cache.get(1, 0).expect("hit");
assert_eq!(got, val);
assert_eq!(cache.len(), 1);
}
#[test]
fn lru_get_promotes_entry_so_it_survives_eviction() {
let mut cache = ComputeCache::new(2);
cache.insert(10, cs(RC::Red), 0); cache.insert(20, cs(RC::Blue), 0); let _ = cache.get(10, 0);
cache.insert(30, cs(RC::Green), 0); assert!(cache.get(10, 0).is_some(), "A survived because it was promoted");
assert!(cache.get(20, 0).is_none(), "B evicted as least-recently-used");
assert!(cache.get(30, 0).is_some(), "C present");
assert_eq!(cache.len(), 2);
}
#[test]
fn lru_insert_update_promotes() {
let mut cache = ComputeCache::new(2);
cache.insert(10, cs(RC::Red), 0); cache.insert(20, cs(RC::Blue), 0); cache.insert(10, cs(RC::Yellow), 0);
cache.insert(30, cs(RC::Green), 0); let got = cache.get(10, 0).expect("A present (promoted by update)");
assert_eq!(got.style.color, Some(crate::color::Color::literal(RC::Yellow)));
assert!(cache.get(20, 0).is_none(), "B evicted as least-recently-used");
assert!(cache.get(30, 0).is_some(), "C present");
assert_eq!(cache.len(), 2);
}
#[test]
fn lru_miss_does_not_reorder() {
let mut cache = ComputeCache::new(2);
cache.insert(10, cs(RC::Red), 0); cache.insert(20, cs(RC::Blue), 0); assert!(cache.get(999, 0).is_none());
cache.insert(30, cs(RC::Green), 0); assert!(cache.get(10, 0).is_none(), "A evicted (oldest, never promoted)");
assert!(cache.get(20, 0).is_some(), "B present");
assert!(cache.get(30, 0).is_some(), "C present");
assert_eq!(cache.len(), 2);
}
#[test]
fn lru_capacity_zero_never_stores() {
let mut cache = ComputeCache::new(0);
cache.insert(1, cs(RC::Red), 0);
assert!(cache.is_empty());
assert_eq!(cache.len(), 0);
assert!(cache.get(1, 0).is_none());
}
#[test]
fn lru_generation_clear_resets_order() {
let mut cache = ComputeCache::new(2);
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");
cache.insert(40, cs(RC::Yellow), 1); assert_eq!(cache.len(), 2);
let _ = cache.get(30, 1);
cache.insert(50, cs(RC::Magenta), 1); assert!(cache.get(30, 1).is_some(), "C survived (promoted)");
assert!(cache.get(40, 1).is_none(), "D evicted as LRU of the fresh order");
assert!(cache.get(50, 1).is_some(), "E present");
}
#[test]
fn cache_generation_mismatch_clears_on_get() {
let mut cache = ComputeCache::new(8);
cache.insert(1, cs(RC::Red), 0);
assert_eq!(cache.len(), 1);
let got = cache.get(1, 1);
assert!(got.is_none());
assert!(cache.is_empty(), "cache cleared by gen mismatch");
}
#[test]
fn cache_generation_mismatch_clears_on_insert() {
let mut cache = ComputeCache::new(8);
cache.insert(1, cs(RC::Red), 0);
assert_eq!(cache.len(), 1);
cache.insert(2, cs(RC::Blue), 1);
assert_eq!(cache.len(), 1, "old entry cleared, only the new one remains");
assert!(cache.get(1, 1).is_none());
assert!(cache.get(2, 1).is_some());
}
#[test]
fn cache_capacity_zero_never_stores() {
let mut cache = ComputeCache::new(0);
cache.insert(1, cs(RC::Red), 0);
assert_eq!(cache.len(), 0);
assert!(cache.is_empty());
assert!(cache.get(1, 0).is_none());
}
#[test]
fn cache_clear_drops_entries() {
let mut cache = ComputeCache::new(8);
cache.insert(1, cs(RC::Red), 0);
cache.insert(2, cs(RC::Blue), 0);
cache.clear();
assert!(cache.is_empty());
assert_eq!(cache.len(), 0);
assert!(cache.get(1, 0).is_none());
}
#[test]
fn cache_stable_across_same_gen_lookups() {
let mut cache = ComputeCache::new(4);
for i in 0u64..4 {
cache.insert(i, cs(RC::Red), 0);
}
for i in 0u64..4 {
assert!(cache.get(i, 0).is_some());
}
assert_eq!(cache.len(), 4);
}
}