use crate::core::{Rect, Size};
use crate::image::{resize, ImageData};
use crate::signal::Signal1;
use crate::widget::{BaseWidget, Widget, WidgetKind};
use std::cell::{Cell, RefCell};
use std::collections::HashMap;
struct Entry {
data: ImageData,
stamp: u64,
}
pub struct ThumbnailCache {
base: BaseWidget,
max_entries: Cell<usize>,
target_size: Cell<(u32, u32)>,
entries: RefCell<HashMap<(u64, u32, u32), Entry>>,
order: RefCell<Vec<(u64, u32, u32)>>,
next_stamp: Cell<u64>,
pub thumbnail_ready: Signal1<u64>,
}
impl ThumbnailCache {
pub fn new(geometry: Rect) -> Self {
Self {
base: BaseWidget::new(WidgetKind::Panel, geometry, "ThumbnailCache"),
max_entries: Cell::new(256),
target_size: Cell::new((96, 96)),
entries: RefCell::new(HashMap::new()),
order: RefCell::new(Vec::new()),
next_stamp: Cell::new(0),
thumbnail_ready: Signal1::new(),
}
}
pub fn len(&self) -> usize {
self.entries.borrow().len()
}
pub fn is_empty(&self) -> bool {
self.entries.borrow().is_empty()
}
pub fn max_entries(&self) -> usize {
self.max_entries.get()
}
pub fn set_max_entries(&self, max_entries: usize) {
self.max_entries.set(max_entries.max(1));
self.evict_to_capacity();
}
pub fn target_size(&self) -> (u32, u32) {
self.target_size.get()
}
pub fn set_target_size(&self, size: (u32, u32)) {
self.target_size.set((size.0.max(1), size.1.max(1)));
}
pub fn thumbnail_for(
&self,
key: u64,
source: &ImageData,
source_size: (u32, u32),
) -> ImageData {
self.thumbnail_for_sized(key, source, source_size, self.target_size.get())
}
pub fn thumbnail_for_sized(
&self,
key: u64,
source: &ImageData,
source_size: (u32, u32),
target: (u32, u32),
) -> ImageData {
let target = (target.0.max(1), target.1.max(1));
let cache_key = (key, target.0, target.1);
let stamp = self.next_stamp.get().wrapping_add(1);
self.next_stamp.set(stamp);
if let Some(entry) = self.entries.borrow_mut().get_mut(&cache_key) {
entry.stamp = stamp;
return entry.data.clone();
}
let data = fit_resample(source, source_size, target);
self.entries.borrow_mut().insert(cache_key, Entry { data: data.clone(), stamp });
self.order.borrow_mut().push(cache_key);
self.evict_to_capacity();
self.thumbnail_ready.emit(key);
data
}
pub fn invalidate(&self, key: u64) {
self.entries.borrow_mut().retain(|(cached_key, _, _), _| *cached_key != key);
self.order.borrow_mut().retain(|(cached_key, _, _)| *cached_key != key);
}
pub fn clear(&self) {
self.entries.borrow_mut().clear();
self.order.borrow_mut().clear();
}
fn evict_to_capacity(&self) {
loop {
let len = self.entries.borrow().len();
if len <= self.max_entries.get() {
break;
}
let oldest = {
let entries = self.entries.borrow();
self.order
.borrow()
.iter()
.copied()
.filter(|key| entries.contains_key(key))
.min_by_key(|key| entries.get(key).map(|entry| entry.stamp).unwrap_or(0))
};
let Some(oldest) = oldest else {
break;
};
self.entries.borrow_mut().remove(&oldest);
self.order.borrow_mut().retain(|key| *key != oldest);
}
}
}
fn fit_resample(source: &ImageData, source_size: (u32, u32), target: (u32, u32)) -> ImageData {
let (source_w, source_h) = (source_size.0.max(1), source_size.1.max(1));
let scale = (target.0 as f32 / source_w as f32).min(target.1 as f32 / source_h as f32);
let dst_w = (source_w as f32 * scale).round().max(1.0) as u32;
let dst_h = (source_h as f32 * scale).round().max(1.0) as u32;
if dst_w == source_w && dst_h == source_h {
return source.clone();
}
resize(source.clone(), source_w, source_h, dst_w, dst_h).unwrap_or_else(|_| source.clone())
}
impl Widget for ThumbnailCache {
fn base(&self) -> &BaseWidget {
&self.base
}
fn set_state_theme_hook(&mut self) {
crate::style::reapply_active_theme_state(self);
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn size_hint(&self) -> Size {
let (width, height) = self.target_size.get();
Size::new(width, height)
}
}
impl crate::event::EventHandler for ThumbnailCache {
fn handle_event(&mut self, _event: &crate::event::Event) {
}
}
#[cfg(test)]
mod tests {
use super::*;
fn rgba(width: u32, height: u32, value: u8) -> ImageData {
ImageData::Rgba8(vec![value; (width * height * 4) as usize])
}
#[test]
fn a_miss_generates_once_and_a_hit_reuses_it() {
let cache = ThumbnailCache::new(Rect::new(0, 0, 100, 100));
let source = rgba(64, 32, 120);
let ready = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
let sink = std::sync::Arc::clone(&ready);
cache.thumbnail_ready.connect(move |_| {
sink.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
});
let first = cache.thumbnail_for(7, &source, (64, 32));
assert_eq!(ready.load(std::sync::atomic::Ordering::Relaxed), 1);
assert_eq!(cache.len(), 1);
let second = cache.thumbnail_for(7, &source, (64, 32));
assert_eq!(second, first, "the cached bitmap is returned");
assert_eq!(
ready.load(std::sync::atomic::Ordering::Relaxed),
1,
"a cache hit does not regenerate"
);
match first {
ImageData::Rgba8(bytes) => {
assert_eq!(bytes.len(), 96 * 48 * 4);
}
_ => panic!("the fixture is RGBA8"),
}
}
#[test]
fn invalidate_and_lru_eviction_drop_the_right_entries() {
let cache = ThumbnailCache::new(Rect::new(0, 0, 100, 100));
cache.set_max_entries(2);
let source = rgba(8, 8, 10);
let _ = cache.thumbnail_for(1, &source, (8, 8));
let _ = cache.thumbnail_for(2, &source, (8, 8));
assert_eq!(cache.len(), 2);
let _ = cache.thumbnail_for(1, &source, (8, 8));
let _ = cache.thumbnail_for(3, &source, (8, 8));
assert_eq!(cache.len(), 2);
assert_eq!(cache.thumbnail_for(1, &source, (8, 8)), rgba(96, 96, 10));
cache.invalidate(1);
assert_eq!(cache.len(), 1);
cache.clear();
assert!(cache.is_empty());
}
}