notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

commit 7f01f3623dadda9f342b56e2afc47be6afed1b33
parent faec75e1b6b042ed30eee8a17318ab115830301e
Author: kernelkind <kernelkind@gmail.com>
Date:   Tue, 29 Apr 2025 10:48:07 -0400

add `TexturesCache`

Signed-off-by: kernelkind <kernelkind@gmail.com>

Diffstat:
Mcrates/notedeck/src/imgcache.rs | 183++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mcrates/notedeck/src/lib.rs | 4++--
Mcrates/notedeck_ui/src/images.rs | 2+-
Mcrates/notedeck_ui/src/note/contents.rs | 8++++----
4 files changed, 188 insertions(+), 9 deletions(-)

diff --git a/crates/notedeck/src/imgcache.rs b/crates/notedeck/src/imgcache.rs @@ -13,18 +13,183 @@ use std::time::{Duration, Instant, SystemTime}; use hex::ToHex; use sha2::Digest; -use std::path; use std::path::PathBuf; +use std::path::{self}; use tracing::warn; pub type MediaCacheValue = Promise<Result<TexturedImage>>; pub type MediaCacheMap = HashMap<String, MediaCacheValue>; +#[derive(Default)] +pub struct TexturesCache { + cache: hashbrown::HashMap<String, TextureStateInternal>, +} + +impl TexturesCache { + pub fn handle_and_get_or_insert_loadable( + &mut self, + url: &str, + closure: impl FnOnce() -> Promise<Option<Result<TexturedImage>>>, + ) -> LoadableTextureState { + let internal = self.handle_and_get_state_internal(url, true, closure); + + internal.into() + } + + pub fn handle_and_get_or_insert( + &mut self, + url: &str, + + closure: impl FnOnce() -> Promise<Option<Result<TexturedImage>>>, + ) -> TextureState { + let internal = self.handle_and_get_state_internal(url, false, closure); + + internal.into() + } + + fn handle_and_get_state_internal( + &mut self, + url: &str, + use_loading: bool, + closure: impl FnOnce() -> Promise<Option<Result<TexturedImage>>>, + ) -> &mut TextureStateInternal { + let state = match self.cache.raw_entry_mut().from_key(url) { + hashbrown::hash_map::RawEntryMut::Occupied(entry) => { + let state = entry.into_mut(); + handle_occupied(state, use_loading); + + state + } + hashbrown::hash_map::RawEntryMut::Vacant(entry) => { + let res = closure(); + let (_, state) = entry.insert(url.to_owned(), TextureStateInternal::Pending(res)); + + state + } + }; + + state + } + + pub fn insert_pending(&mut self, url: &str, promise: Promise<Option<Result<TexturedImage>>>) { + self.cache + .insert(url.to_owned(), TextureStateInternal::Pending(promise)); + } + + pub fn move_to_loaded(&mut self, url: &str) { + let hashbrown::hash_map::RawEntryMut::Occupied(entry) = + self.cache.raw_entry_mut().from_key(url) + else { + return; + }; + + entry.replace_entry_with(|_, v| { + let TextureStateInternal::Loading(textured) = v else { + return None; + }; + + Some(TextureStateInternal::Loaded(textured)) + }); + } + + pub fn get_and_handle(&mut self, url: &str) -> Option<LoadableTextureState> { + self.cache.get_mut(url).map(|state| { + handle_occupied(state, true); + state.into() + }) + } +} + +fn handle_occupied(state: &mut TextureStateInternal, use_loading: bool) { + let TextureStateInternal::Pending(promise) = state else { + return; + }; + + let Some(res) = promise.ready_mut() else { + return; + }; + + let Some(res) = res.take() else { + tracing::error!("Failed to take the promise"); + *state = + TextureStateInternal::Error(crate::Error::Generic("Promise already taken".to_owned())); + return; + }; + + match res { + Ok(textured) => { + *state = if use_loading { + TextureStateInternal::Loading(textured) + } else { + TextureStateInternal::Loaded(textured) + } + } + Err(e) => *state = TextureStateInternal::Error(e), + } +} + +pub enum LoadableTextureState<'a> { + Pending, + Error(&'a crate::Error), + Loading { + actual_image_tex: &'a mut TexturedImage, + }, // the texture is in the loading state, for transitioning between the pending and loaded states + Loaded(&'a mut TexturedImage), +} + +pub enum TextureState<'a> { + Pending, + Error(&'a crate::Error), + Loaded(&'a mut TexturedImage), +} + +impl<'a> From<&'a mut TextureStateInternal> for TextureState<'a> { + fn from(value: &'a mut TextureStateInternal) -> Self { + match value { + TextureStateInternal::Pending(_) => TextureState::Pending, + TextureStateInternal::Error(error) => TextureState::Error(error), + TextureStateInternal::Loading(textured_image) => TextureState::Loaded(textured_image), + TextureStateInternal::Loaded(textured_image) => TextureState::Loaded(textured_image), + } + } +} + +pub enum TextureStateInternal { + Pending(Promise<Option<Result<TexturedImage>>>), + Error(crate::Error), + Loading(TexturedImage), // the image is in the loading state, for transitioning between blur and image + Loaded(TexturedImage), +} + +impl<'a> From<&'a mut TextureStateInternal> for LoadableTextureState<'a> { + fn from(value: &'a mut TextureStateInternal) -> Self { + match value { + TextureStateInternal::Pending(_) => LoadableTextureState::Pending, + TextureStateInternal::Error(error) => LoadableTextureState::Error(error), + TextureStateInternal::Loading(textured_image) => LoadableTextureState::Loading { + actual_image_tex: textured_image, + }, + TextureStateInternal::Loaded(textured_image) => { + LoadableTextureState::Loaded(textured_image) + } + } + } +} + pub enum TexturedImage { Static(TextureHandle), Animated(Animation), } +impl TexturedImage { + pub fn get_first_texture(&self) -> &TextureHandle { + match self { + TexturedImage::Static(texture_handle) => texture_handle, + TexturedImage::Animated(animation) => &animation.first_frame.texture, + } + } +} + pub struct Animation { pub first_frame: TextureFrame, pub other_frames: Vec<TextureFrame>, @@ -60,7 +225,7 @@ pub struct MediaCache { url_imgs: MediaCacheMap, } -#[derive(Clone)] +#[derive(Debug, Eq, PartialEq, Clone, Copy)] pub enum MediaCacheType { Image, Gif, @@ -215,6 +380,20 @@ impl Images { self.static_imgs.migrate_v0()?; self.gifs.migrate_v0() } + + pub fn get_cache(&self, cache_type: MediaCacheType) -> &MediaCache { + match cache_type { + MediaCacheType::Image => &self.static_imgs, + MediaCacheType::Gif => &self.gifs, + } + } + + pub fn get_cache_mut(&mut self, cache_type: MediaCacheType) -> &mut MediaCache { + match cache_type { + MediaCacheType::Image => &mut self.static_imgs, + MediaCacheType::Gif => &mut self.gifs, + } + } } pub type GifStateMap = HashMap<String, GifState>; diff --git a/crates/notedeck/src/lib.rs b/crates/notedeck/src/lib.rs @@ -41,8 +41,8 @@ pub use error::{Error, FilterError, ZapError}; pub use filter::{FilterState, FilterStates, UnifiedSubscription}; pub use fonts::NamedFontFamily; pub use imgcache::{ - Animation, GifState, GifStateMap, ImageFrame, Images, MediaCache, MediaCacheType, - MediaCacheValue, TextureFrame, TexturedImage, + Animation, GifState, GifStateMap, ImageFrame, Images, LoadableTextureState, MediaCache, + MediaCacheType, TextureFrame, TextureState, TexturedImage, TexturesCache, }; pub use job_pool::JobPool; pub use muted::{MuteFun, Muted}; diff --git a/crates/notedeck_ui/src/images.rs b/crates/notedeck_ui/src/images.rs @@ -468,7 +468,7 @@ fn render_media_cache( let m_cached_promise = cache.map().get(url); if m_cached_promise.is_none() { - let res = crate::images::fetch_img(cache, ui.ctx(), url, img_type, cache_type.clone()); + let res = crate::images::fetch_img(cache, ui.ctx(), url, img_type, cache_type); cache.map_mut().insert(url.to_owned(), res); } diff --git a/crates/notedeck_ui/src/note/contents.rs b/crates/notedeck_ui/src/note/contents.rs @@ -318,7 +318,7 @@ fn image_carousel( ui.ctx().memory(|mem| { mem.data .get_temp::<(String, MediaCacheType)>(carousel_id.with("current_image")) - .unwrap_or_else(|| (images[0].0.clone(), images[0].1.clone())) + .unwrap_or_else(|| (images[0].0.clone(), images[0].1)) }) }); @@ -333,7 +333,7 @@ fn image_carousel( img_cache, &image, ImageType::Content, - cache_type.clone(), + cache_type, |ui| { ui.allocate_space(egui::vec2(spinsz, spinsz)); }, @@ -360,7 +360,7 @@ fn image_carousel( mem.data.insert_temp(carousel_id.with("show_popup"), true); mem.data.insert_temp( carousel_id.with("current_image"), - (image.clone(), cache_type.clone()), + (image.clone(), cache_type), ); }); } @@ -438,7 +438,7 @@ fn image_carousel( img_cache, &image, ImageType::Content, - cache_type.clone(), + cache_type, |ui| { ui.allocate_space(egui::vec2(spinsz, spinsz)); },