feat: retain bounded preview caches across selections and views
This commit is contained in:
+78
-7
@@ -18,6 +18,8 @@ use ratatui_image::{
|
||||
|
||||
use crate::youtube::ThumbnailJob;
|
||||
|
||||
const CACHE_SIZE: usize = 8;
|
||||
|
||||
struct ThumbnailImage {
|
||||
id: String,
|
||||
protocol: ThreadProtocol,
|
||||
@@ -31,8 +33,8 @@ pub struct Thumbnail {
|
||||
fetch: Option<ThumbnailJob>,
|
||||
decoder: Option<Receiver<Result<ThumbnailImage, String>>>,
|
||||
image: Option<ThumbnailImage>,
|
||||
// ponytail: one completed thumbnail plus an in-flight replacement, not a video cache.
|
||||
cached: Option<ThumbnailImage>,
|
||||
// ponytail: eight-entry linear lookup; use a keyed LRU if the buffer grows.
|
||||
cached: std::collections::VecDeque<ThumbnailImage>,
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
@@ -44,7 +46,7 @@ impl Thumbnail {
|
||||
fetch: None,
|
||||
decoder: None,
|
||||
image: None,
|
||||
cached: None,
|
||||
cached: std::collections::VecDeque::with_capacity(CACHE_SIZE),
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
@@ -62,10 +64,16 @@ impl Thumbnail {
|
||||
if self.selected.as_deref() == Some(id) {
|
||||
return;
|
||||
}
|
||||
// Remove a hit before storing the previous image so eviction cannot discard the target.
|
||||
let cached = self
|
||||
.cached
|
||||
.iter()
|
||||
.position(|image| image.id == id)
|
||||
.and_then(|index| self.cached.remove(index));
|
||||
self.cancel();
|
||||
self.selected = Some(id.to_owned());
|
||||
if self.cached.as_ref().is_some_and(|image| image.id == id) {
|
||||
self.image = self.cached.take();
|
||||
if let Some(image) = cached {
|
||||
self.image = Some(image);
|
||||
return;
|
||||
}
|
||||
match ThumbnailJob::start(id) {
|
||||
@@ -83,10 +91,18 @@ impl Thumbnail {
|
||||
if let Some(image) = self.image.take()
|
||||
&& image.complete
|
||||
{
|
||||
self.cached = Some(image);
|
||||
if self.cached.len() == CACHE_SIZE {
|
||||
self.cached.pop_front();
|
||||
}
|
||||
self.cached.push_back(image);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_cache(&mut self) {
|
||||
self.cancel();
|
||||
self.cached.clear();
|
||||
}
|
||||
|
||||
pub fn is_loading(&self) -> bool {
|
||||
self.selected.is_some()
|
||||
&& self.error.is_none()
|
||||
@@ -180,7 +196,6 @@ impl Thumbnail {
|
||||
// area again before rendering, so rapid resizes cannot draw an old size.
|
||||
if image.protocol.update_resized_protocol(response) {
|
||||
image.complete = true;
|
||||
self.cached = None;
|
||||
}
|
||||
}
|
||||
Ok(Err(error)) => {
|
||||
@@ -281,6 +296,62 @@ mod tests {
|
||||
assert!(thumbnail.error.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thumbnail_hits_survive_a_full_buffer_and_promote_recent_images() {
|
||||
let mut thumbnail = Thumbnail::new();
|
||||
let completed = |id: String| {
|
||||
let (requests, _receiver) = mpsc::channel();
|
||||
let (_sender, replies) = mpsc::channel();
|
||||
ThumbnailImage {
|
||||
id,
|
||||
protocol: ThreadProtocol::new(
|
||||
requests,
|
||||
Some(
|
||||
Picker::halfblocks()
|
||||
.new_resize_protocol(image::DynamicImage::new_rgb8(1, 1)),
|
||||
),
|
||||
),
|
||||
replies,
|
||||
complete: true,
|
||||
}
|
||||
};
|
||||
for index in 0..CACHE_SIZE {
|
||||
thumbnail.image = Some(completed(format!("video{index:06}")));
|
||||
thumbnail.cancel();
|
||||
}
|
||||
thumbnail.image = Some(completed("video000008".into()));
|
||||
thumbnail.selected = Some("video000008".into());
|
||||
// This is the oldest cached image: storing the outgoing image first would evict it.
|
||||
thumbnail.select("video000000");
|
||||
assert_eq!(thumbnail.image.as_ref().unwrap().id, "video000000");
|
||||
assert!(thumbnail.fetch.is_none());
|
||||
thumbnail.select("video000001");
|
||||
assert_eq!(thumbnail.image.as_ref().unwrap().id, "video000001");
|
||||
assert!(thumbnail.fetch.is_none());
|
||||
thumbnail.cancel();
|
||||
assert!(
|
||||
!thumbnail
|
||||
.cached
|
||||
.iter()
|
||||
.any(|image| image.id == "video000002")
|
||||
);
|
||||
assert!(
|
||||
thumbnail
|
||||
.cached
|
||||
.iter()
|
||||
.any(|image| image.id == "video000000")
|
||||
);
|
||||
assert!(
|
||||
thumbnail
|
||||
.cached
|
||||
.iter()
|
||||
.any(|image| image.id == "video000008")
|
||||
);
|
||||
thumbnail.clear_cache();
|
||||
assert!(thumbnail.cached.is_empty());
|
||||
assert!(thumbnail.image.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn small_thumbnail_fills_popup_proportionally_and_resizes() {
|
||||
use ratatui::{Terminal, backend::TestBackend, style::Color};
|
||||
|
||||
Reference in New Issue
Block a user