use crate::geometry::{Rect, Size};
use crate::tree::WidgetId;
use std::collections::{HashMap, HashSet};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
struct LayoutKey {
id: WidgetId,
width_bits: u32,
height_bits: u32,
content_hash: u64,
}
impl LayoutKey {
fn new(id: WidgetId, avail: Size, content_hash: u64) -> Self {
Self {
id,
width_bits: avail.width.to_bits(),
height_bits: avail.height.to_bits(),
content_hash,
}
}
}
#[derive(Debug, Default)]
pub struct LayoutCache {
entries: HashMap<LayoutKey, Rect>,
dirty: HashSet<WidgetId>,
hits: u64,
misses: u64,
}
impl LayoutCache {
pub fn new() -> Self {
Self::default()
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn hits(&self) -> u64 {
self.hits
}
pub fn misses(&self) -> u64 {
self.misses
}
pub fn hit_rate(&self) -> f32 {
let total = self.hits + self.misses;
if total == 0 {
0.0
} else {
self.hits as f32 / total as f32
}
}
pub fn reset_stats(&mut self) {
self.hits = 0;
self.misses = 0;
}
pub fn get(&mut self, id: WidgetId, avail: Size, content_hash: u64) -> Option<Rect> {
if self.dirty.contains(&id) {
self.misses += 1;
return None;
}
let key = LayoutKey::new(id, avail, content_hash);
match self.entries.get(&key) {
Some(rect) => {
self.hits += 1;
Some(*rect)
}
None => {
self.misses += 1;
None
}
}
}
pub fn put(&mut self, id: WidgetId, avail: Size, content_hash: u64, rect: Rect) {
self.dirty.remove(&id);
self.entries
.insert(LayoutKey::new(id, avail, content_hash), rect);
}
pub fn invalidate(&mut self, id: WidgetId) {
self.dirty.insert(id);
self.entries.retain(|k, _| k.id != id);
}
pub fn invalidate_many(&mut self, ids: impl IntoIterator<Item = WidgetId>) {
for id in ids {
self.invalidate(id);
}
}
pub fn is_dirty(&self, id: WidgetId) -> bool {
self.dirty.contains(&id)
}
pub fn clear(&mut self) {
self.entries.clear();
self.dirty.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sz(w: f32, h: f32) -> Size {
Size::new(w, h)
}
#[test]
fn hit_after_put() {
let mut c = LayoutCache::new();
let id = WidgetId(1);
let rect = Rect::new(0.0, 0.0, 10.0, 20.0);
assert!(c.get(id, sz(100.0, 100.0), 7).is_none());
c.put(id, sz(100.0, 100.0), 7, rect);
assert_eq!(c.get(id, sz(100.0, 100.0), 7), Some(rect));
assert_eq!(c.hits(), 1);
assert_eq!(c.misses(), 1);
}
#[test]
fn miss_on_different_size_or_content() {
let mut c = LayoutCache::new();
let id = WidgetId(1);
c.put(id, sz(100.0, 100.0), 7, Rect::ZERO);
assert!(c.get(id, sz(120.0, 100.0), 7).is_none());
assert!(c.get(id, sz(100.0, 100.0), 8).is_none());
assert_eq!(c.get(id, sz(100.0, 100.0), 7), Some(Rect::ZERO));
}
#[test]
fn dirty_flag_forces_miss_until_refreshed() {
let mut c = LayoutCache::new();
let id = WidgetId(2);
c.put(id, sz(50.0, 50.0), 1, Rect::new(0.0, 0.0, 5.0, 5.0));
assert!(c.get(id, sz(50.0, 50.0), 1).is_some());
c.invalidate(id);
assert!(c.is_dirty(id));
assert!(c.get(id, sz(50.0, 50.0), 1).is_none());
c.put(id, sz(50.0, 50.0), 1, Rect::new(0.0, 0.0, 5.0, 5.0));
assert!(!c.is_dirty(id));
assert!(c.get(id, sz(50.0, 50.0), 1).is_some());
}
#[test]
fn invalidate_drops_stale_entries() {
let mut c = LayoutCache::new();
let id = WidgetId(3);
c.put(id, sz(10.0, 10.0), 0, Rect::ZERO);
c.put(id, sz(20.0, 20.0), 0, Rect::ZERO);
assert_eq!(c.len(), 2);
c.invalidate(id);
assert_eq!(c.len(), 0);
assert!(c.is_dirty(id));
}
#[test]
fn hit_rate_and_reset() {
let mut c = LayoutCache::new();
let id = WidgetId(4);
c.put(id, sz(1.0, 1.0), 0, Rect::ZERO);
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);
c.reset_stats();
assert_eq!(c.hits(), 0);
assert_eq!(c.misses(), 0);
assert_eq!(c.hit_rate(), 0.0);
}
#[test]
fn invalidate_many_and_clear() {
let mut c = LayoutCache::new();
c.put(WidgetId(1), sz(1.0, 1.0), 0, Rect::ZERO);
c.put(WidgetId(2), sz(1.0, 1.0), 0, Rect::ZERO);
c.invalidate_many([WidgetId(1), WidgetId(2)]);
assert!(c.is_dirty(WidgetId(1)) && c.is_dirty(WidgetId(2)));
c.clear();
assert!(c.is_empty());
assert!(!c.is_dirty(WidgetId(1)));
}
}