1use crate::geometry::{Rect, Size};
19use crate::tree::WidgetId;
20use std::collections::{HashMap, HashSet};
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
24struct LayoutKey {
25 id: WidgetId,
26 width_bits: u32,
27 height_bits: u32,
28 content_hash: u64,
29}
30
31impl LayoutKey {
32 fn new(id: WidgetId, avail: Size, content_hash: u64) -> Self {
33 Self {
34 id,
35 width_bits: avail.width.to_bits(),
36 height_bits: avail.height.to_bits(),
37 content_hash,
38 }
39 }
40}
41
42#[derive(Debug, Default)]
44pub struct LayoutCache {
45 entries: HashMap<LayoutKey, Rect>,
46 dirty: HashSet<WidgetId>,
48 hits: u64,
49 misses: u64,
50}
51
52impl LayoutCache {
53 pub fn new() -> Self {
55 Self::default()
56 }
57
58 pub fn len(&self) -> usize {
60 self.entries.len()
61 }
62
63 pub fn is_empty(&self) -> bool {
65 self.entries.is_empty()
66 }
67
68 pub fn hits(&self) -> u64 {
70 self.hits
71 }
72
73 pub fn misses(&self) -> u64 {
75 self.misses
76 }
77
78 pub fn hit_rate(&self) -> f32 {
80 let total = self.hits + self.misses;
81 if total == 0 {
82 0.0
83 } else {
84 self.hits as f32 / total as f32
85 }
86 }
87
88 pub fn reset_stats(&mut self) {
90 self.hits = 0;
91 self.misses = 0;
92 }
93
94 pub fn get(&mut self, id: WidgetId, avail: Size, content_hash: u64) -> Option<Rect> {
99 if self.dirty.contains(&id) {
100 self.misses += 1;
101 return None;
102 }
103 let key = LayoutKey::new(id, avail, content_hash);
104 match self.entries.get(&key) {
105 Some(rect) => {
106 self.hits += 1;
107 Some(*rect)
108 }
109 None => {
110 self.misses += 1;
111 None
112 }
113 }
114 }
115
116 pub fn put(&mut self, id: WidgetId, avail: Size, content_hash: u64, rect: Rect) {
119 self.dirty.remove(&id);
120 self.entries
121 .insert(LayoutKey::new(id, avail, content_hash), rect);
122 }
123
124 pub fn invalidate(&mut self, id: WidgetId) {
127 self.dirty.insert(id);
128 self.entries.retain(|k, _| k.id != id);
129 }
130
131 pub fn invalidate_many(&mut self, ids: impl IntoIterator<Item = WidgetId>) {
133 for id in ids {
134 self.invalidate(id);
135 }
136 }
137
138 pub fn is_dirty(&self, id: WidgetId) -> bool {
140 self.dirty.contains(&id)
141 }
142
143 pub fn clear(&mut self) {
145 self.entries.clear();
146 self.dirty.clear();
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 fn sz(w: f32, h: f32) -> Size {
155 Size::new(w, h)
156 }
157
158 #[test]
159 fn hit_after_put() {
160 let mut c = LayoutCache::new();
161 let id = WidgetId(1);
162 let rect = Rect::new(0.0, 0.0, 10.0, 20.0);
163 assert!(c.get(id, sz(100.0, 100.0), 7).is_none());
165 c.put(id, sz(100.0, 100.0), 7, rect);
166 assert_eq!(c.get(id, sz(100.0, 100.0), 7), Some(rect));
168 assert_eq!(c.hits(), 1);
169 assert_eq!(c.misses(), 1);
170 }
171
172 #[test]
173 fn miss_on_different_size_or_content() {
174 let mut c = LayoutCache::new();
175 let id = WidgetId(1);
176 c.put(id, sz(100.0, 100.0), 7, Rect::ZERO);
177 assert!(c.get(id, sz(120.0, 100.0), 7).is_none());
179 assert!(c.get(id, sz(100.0, 100.0), 8).is_none());
181 assert_eq!(c.get(id, sz(100.0, 100.0), 7), Some(Rect::ZERO));
183 }
184
185 #[test]
186 fn dirty_flag_forces_miss_until_refreshed() {
187 let mut c = LayoutCache::new();
188 let id = WidgetId(2);
189 c.put(id, sz(50.0, 50.0), 1, Rect::new(0.0, 0.0, 5.0, 5.0));
190 assert!(c.get(id, sz(50.0, 50.0), 1).is_some());
191 c.invalidate(id);
193 assert!(c.is_dirty(id));
194 assert!(c.get(id, sz(50.0, 50.0), 1).is_none());
195 c.put(id, sz(50.0, 50.0), 1, Rect::new(0.0, 0.0, 5.0, 5.0));
197 assert!(!c.is_dirty(id));
198 assert!(c.get(id, sz(50.0, 50.0), 1).is_some());
199 }
200
201 #[test]
202 fn invalidate_drops_stale_entries() {
203 let mut c = LayoutCache::new();
204 let id = WidgetId(3);
205 c.put(id, sz(10.0, 10.0), 0, Rect::ZERO);
207 c.put(id, sz(20.0, 20.0), 0, Rect::ZERO);
208 assert_eq!(c.len(), 2);
209 c.invalidate(id);
210 assert_eq!(c.len(), 0);
212 assert!(c.is_dirty(id));
213 }
214
215 #[test]
216 fn hit_rate_and_reset() {
217 let mut c = LayoutCache::new();
218 let id = WidgetId(4);
219 c.put(id, sz(1.0, 1.0), 0, Rect::ZERO);
220 let _ = c.get(id, sz(1.0, 1.0), 0); let _ = c.get(id, sz(2.0, 2.0), 0); assert!((c.hit_rate() - 0.5).abs() < 1e-6);
223 c.reset_stats();
224 assert_eq!(c.hits(), 0);
225 assert_eq!(c.misses(), 0);
226 assert_eq!(c.hit_rate(), 0.0);
227 }
228
229 #[test]
230 fn invalidate_many_and_clear() {
231 let mut c = LayoutCache::new();
232 c.put(WidgetId(1), sz(1.0, 1.0), 0, Rect::ZERO);
233 c.put(WidgetId(2), sz(1.0, 1.0), 0, Rect::ZERO);
234 c.invalidate_many([WidgetId(1), WidgetId(2)]);
235 assert!(c.is_dirty(WidgetId(1)) && c.is_dirty(WidgetId(2)));
236 c.clear();
237 assert!(c.is_empty());
238 assert!(!c.is_dirty(WidgetId(1)));
239 }
240}