use std::collections::HashMap;
use crate::compile::CompiledStyleSheet;
use crate::stylesheet::ComputedStyle;
#[derive(Hash, Eq, PartialEq, Clone)]
struct StyleCacheKey {
widget_type: String,
classes: Vec<String>,
id: Option<String>,
generation: u64,
}
impl StyleCacheKey {
fn new(widget_type: &str, classes: &[&str], id: Option<&str>, generation: u64) -> Self {
let mut sorted_classes: Vec<String> = classes.iter().map(|s| s.to_string()).collect();
sorted_classes.sort();
Self {
widget_type: widget_type.to_owned(),
classes: sorted_classes,
id: id.map(ToOwned::to_owned),
generation,
}
}
}
pub struct StyleCache {
cache: HashMap<StyleCacheKey, ComputedStyle>,
current_generation: u64,
}
impl StyleCache {
pub fn new() -> Self {
Self {
cache: HashMap::new(),
current_generation: 0,
}
}
pub fn get_or_compute(
&mut self,
compiled: &CompiledStyleSheet,
widget_type: &str,
classes: &[&str],
id: Option<&str>,
) -> &ComputedStyle {
if compiled.generation != self.current_generation {
self.cache.clear();
self.current_generation = compiled.generation;
}
let key = StyleCacheKey::new(widget_type, classes, id, compiled.generation);
self.cache
.entry(key)
.or_insert_with(|| compiled.compute_style(widget_type, classes, id))
}
}
impl Default for StyleCache {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::compile::CompiledStyleSheet;
use crate::stylesheet::StyleSheet;
fn make_compiled(css: &str, generation: u64) -> CompiledStyleSheet {
let sheet = StyleSheet::parse(css).stylesheet;
CompiledStyleSheet::compile(&sheet, generation)
}
#[test]
fn test_style_cache_miss_computes() {
let compiled = make_compiled("button { color: #ff0000; }", 1);
let mut cache = StyleCache::new();
let style = cache.get_or_compute(&compiled, "button", &[], None);
assert!(
style.color.is_some(),
"cache miss should compute and return the correct style"
);
}
#[test]
fn test_style_cache_hit_returns_identical() {
let compiled = make_compiled("button { color: #ff0000; padding: 8px; }", 1);
let mut cache = StyleCache::new();
let first = cache.get_or_compute(&compiled, "button", &[], None).clone();
let second = cache.get_or_compute(&compiled, "button", &[], None).clone();
assert_eq!(
first, second,
"second call (cache hit) must return identical style"
);
}
#[test]
fn test_style_cache_invalidates_on_generation_change() {
let compiled_v1 = make_compiled("button { color: #ff0000; }", 1);
let mut cache = StyleCache::new();
let style_v1 = cache
.get_or_compute(&compiled_v1, "button", &[], None)
.clone();
assert!(style_v1.color.is_some(), "v1 should have color");
let compiled_v2 = make_compiled("button { padding: 4px; }", 2);
let style_v2 = cache
.get_or_compute(&compiled_v2, "button", &[], None)
.clone();
assert!(
style_v2.color.is_none(),
"after generation change the old cached value must not be returned"
);
assert!(style_v2.padding.is_some(), "v2 should have padding");
}
#[test]
fn test_style_cache_class_order_irrelevant() {
let compiled = make_compiled(".primary { color: #7aa2f7; }", 1);
let mut cache = StyleCache::new();
let a = cache
.get_or_compute(&compiled, "button", &["disabled", "primary"], None)
.clone();
let b = cache
.get_or_compute(&compiled, "button", &["primary", "disabled"], None)
.clone();
assert_eq!(a, b, "class order must not affect cache lookup");
}
}