Skip to main content

oxiui_theme/
style_cache.rs

1//! Memoized style cache for resolved [`ComputedStyle`] values.
2//!
3//! [`StyleCache`] wraps a [`CompiledStyleSheet`] and caches the result of
4//! [`CompiledStyleSheet::compute_style`] per `(widget_type, classes, id,
5//! generation)` key.  When the stylesheet's generation changes the cache is
6//! automatically invalidated and rebuilt on the next access.
7
8use std::collections::HashMap;
9
10use crate::compile::CompiledStyleSheet;
11use crate::stylesheet::ComputedStyle;
12
13// ── Cache key ─────────────────────────────────────────────────────────────────
14
15/// The key used to look up a previously computed style.
16///
17/// Classes are sorted so that `["primary", "disabled"]` and
18/// `["disabled", "primary"]` produce the same key.
19#[derive(Hash, Eq, PartialEq, Clone)]
20struct StyleCacheKey {
21    widget_type: String,
22    classes: Vec<String>,
23    id: Option<String>,
24    generation: u64,
25}
26
27impl StyleCacheKey {
28    fn new(widget_type: &str, classes: &[&str], id: Option<&str>, generation: u64) -> Self {
29        let mut sorted_classes: Vec<String> = classes.iter().map(|s| s.to_string()).collect();
30        sorted_classes.sort();
31        Self {
32            widget_type: widget_type.to_owned(),
33            classes: sorted_classes,
34            id: id.map(ToOwned::to_owned),
35            generation,
36        }
37    }
38}
39
40// ── StyleCache ────────────────────────────────────────────────────────────────
41
42/// Memoizes [`ComputedStyle`] per `(widget_type, classes, id, stylesheet_generation)`.
43///
44/// When [`CompiledStyleSheet::generation`] advances, all cached entries from
45/// the previous generation are discarded before the new lookup.
46///
47/// # Example
48/// ```rust
49/// use oxiui_theme::{StyleCache, stylesheet::StyleSheet};
50/// use oxiui_theme::compile::CompiledStyleSheet;
51///
52/// let sheet = StyleSheet::parse("button { color: #ff0000; }").stylesheet;
53/// let compiled = CompiledStyleSheet::compile(&sheet, 1);
54/// let mut cache = StyleCache::new();
55/// let style = cache.get_or_compute(&compiled, "button", &[], None);
56/// assert!(style.color.is_some());
57/// ```
58pub struct StyleCache {
59    cache: HashMap<StyleCacheKey, ComputedStyle>,
60    current_generation: u64,
61}
62
63impl StyleCache {
64    /// Create an empty cache.
65    pub fn new() -> Self {
66        Self {
67            cache: HashMap::new(),
68            current_generation: 0,
69        }
70    }
71
72    /// Look up or compute a style.
73    ///
74    /// If `compiled.generation` differs from the last seen generation, the
75    /// entire cache is cleared before the lookup proceeds.  On a cache miss the
76    /// style is computed via [`CompiledStyleSheet::compute_style`], inserted,
77    /// and returned.
78    pub fn get_or_compute(
79        &mut self,
80        compiled: &CompiledStyleSheet,
81        widget_type: &str,
82        classes: &[&str],
83        id: Option<&str>,
84    ) -> &ComputedStyle {
85        // Invalidate on generation change.
86        if compiled.generation != self.current_generation {
87            self.cache.clear();
88            self.current_generation = compiled.generation;
89        }
90
91        let key = StyleCacheKey::new(widget_type, classes, id, compiled.generation);
92        self.cache
93            .entry(key)
94            .or_insert_with(|| compiled.compute_style(widget_type, classes, id))
95    }
96}
97
98impl Default for StyleCache {
99    fn default() -> Self {
100        Self::new()
101    }
102}
103
104// ── Tests ─────────────────────────────────────────────────────────────────────
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use crate::compile::CompiledStyleSheet;
110    use crate::stylesheet::StyleSheet;
111
112    fn make_compiled(css: &str, generation: u64) -> CompiledStyleSheet {
113        let sheet = StyleSheet::parse(css).stylesheet;
114        CompiledStyleSheet::compile(&sheet, generation)
115    }
116
117    #[test]
118    fn test_style_cache_miss_computes() {
119        let compiled = make_compiled("button { color: #ff0000; }", 1);
120        let mut cache = StyleCache::new();
121        let style = cache.get_or_compute(&compiled, "button", &[], None);
122        assert!(
123            style.color.is_some(),
124            "cache miss should compute and return the correct style"
125        );
126    }
127
128    #[test]
129    fn test_style_cache_hit_returns_identical() {
130        let compiled = make_compiled("button { color: #ff0000; padding: 8px; }", 1);
131        let mut cache = StyleCache::new();
132
133        let first = cache.get_or_compute(&compiled, "button", &[], None).clone();
134        let second = cache.get_or_compute(&compiled, "button", &[], None).clone();
135
136        assert_eq!(
137            first, second,
138            "second call (cache hit) must return identical style"
139        );
140    }
141
142    #[test]
143    fn test_style_cache_invalidates_on_generation_change() {
144        // Gen 1: button has red color.
145        let compiled_v1 = make_compiled("button { color: #ff0000; }", 1);
146        let mut cache = StyleCache::new();
147
148        let style_v1 = cache
149            .get_or_compute(&compiled_v1, "button", &[], None)
150            .clone();
151        assert!(style_v1.color.is_some(), "v1 should have color");
152
153        // Gen 2: button now has no color set at all.
154        let compiled_v2 = make_compiled("button { padding: 4px; }", 2);
155
156        // Before accessing with v2, the cache still holds v1's entry.
157        // After the call with v2's compiled sheet, cache is invalidated.
158        let style_v2 = cache
159            .get_or_compute(&compiled_v2, "button", &[], None)
160            .clone();
161        assert!(
162            style_v2.color.is_none(),
163            "after generation change the old cached value must not be returned"
164        );
165        assert!(style_v2.padding.is_some(), "v2 should have padding");
166    }
167
168    #[test]
169    fn test_style_cache_class_order_irrelevant() {
170        // Classes are sorted when building the key, so order must not matter.
171        let compiled = make_compiled(".primary { color: #7aa2f7; }", 1);
172        let mut cache = StyleCache::new();
173
174        let a = cache
175            .get_or_compute(&compiled, "button", &["disabled", "primary"], None)
176            .clone();
177        let b = cache
178            .get_or_compute(&compiled, "button", &["primary", "disabled"], None)
179            .clone();
180
181        assert_eq!(a, b, "class order must not affect cache lookup");
182    }
183}