oxiui_theme/
style_cache.rs1use std::collections::HashMap;
9
10use crate::compile::CompiledStyleSheet;
11use crate::stylesheet::ComputedStyle;
12
13#[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
40pub struct StyleCache {
59 cache: HashMap<StyleCacheKey, ComputedStyle>,
60 current_generation: u64,
61}
62
63impl StyleCache {
64 pub fn new() -> Self {
66 Self {
67 cache: HashMap::new(),
68 current_generation: 0,
69 }
70 }
71
72 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 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#[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 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 let compiled_v2 = make_compiled("button { padding: 4px; }", 2);
155
156 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 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}