notedeck

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

commit 5474613f1376fdf7103bfe751f35fa72066fccad
parent 761763b4f9f270b5909163d2b0748d9bec6789c3
Author: kernelkind <kernelkind@gmail.com>
Date:   Sat, 22 Nov 2025 21:02:27 -0700

feat(media-rendering): add helpers for various media rendering configs

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

Diffstat:
Acrates/notedeck/src/media/latest.rs | 229+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/notedeck/src/media/mod.rs | 4++++
2 files changed, 233 insertions(+), 0 deletions(-)

diff --git a/crates/notedeck/src/media/latest.rs b/crates/notedeck/src/media/latest.rs @@ -0,0 +1,229 @@ +use std::time::SystemTime; + +use egui::TextureHandle; + +use crate::jobs::MediaJobSender; +use crate::{ + media::{ + gif::{process_gif_frame, AnimatedImgTexCache}, + static_imgs::StaticImgTexCache, + AnimationMode, BlurCache, + }, + Error, GifStateMap, ImageType, MediaCacheType, ObfuscationType, TextureState, +}; + +pub enum MediaRenderState<'a> { + ActualImage(&'a TextureHandle), + Transitioning { + image: &'a TextureHandle, + obfuscation: ObfuscatedTexture<'a>, + }, + Error(&'a crate::Error), + Shimmering(ObfuscatedTexture<'a>), + Obfuscated(ObfuscatedTexture<'a>), +} + +pub enum ObfuscatedTexture<'a> { + Blur(&'a TextureHandle), + Default, +} + +pub struct NoLoadingLatestTex<'a> { + static_cache: &'a StaticImgTexCache, + animated_cache: &'a AnimatedImgTexCache, + gif_state: &'a mut GifStateMap, +} + +impl<'a> NoLoadingLatestTex<'a> { + pub fn new( + static_cache: &'a StaticImgTexCache, + animated_cache: &'a AnimatedImgTexCache, + gif_state: &'a mut GifStateMap, + ) -> Self { + Self { + static_cache, + animated_cache, + gif_state, + } + } + + pub fn latest( + &mut self, + jobs: &MediaJobSender, + ctx: &egui::Context, + url: &str, + cache_type: MediaCacheType, + imgtype: ImageType, + animation_mode: AnimationMode, + ) -> Option<&'a TextureHandle> { + let LatestImageTex::Loaded(tex) = + self.latest_state(jobs, ctx, url, cache_type, imgtype, animation_mode) + else { + return None; + }; + + Some(tex) + } + + pub fn latest_state( + &mut self, + jobs: &MediaJobSender, + ctx: &egui::Context, + url: &str, + cache_type: MediaCacheType, + imgtype: ImageType, + animation_mode: AnimationMode, + ) -> LatestImageTex<'a> { + match cache_type { + MediaCacheType::Image => { + match self.static_cache.get_or_request(jobs, ctx, url, imgtype) { + TextureState::Pending => LatestImageTex::Pending, + TextureState::Error(error) => LatestImageTex::Error(error), + TextureState::Loaded(t) => LatestImageTex::Loaded(t), + } + } + MediaCacheType::Gif => { + match self.animated_cache.get_or_request(jobs, ctx, url, imgtype) { + TextureState::Pending => LatestImageTex::Pending, + TextureState::Error(error) => LatestImageTex::Error(error), + TextureState::Loaded(animation) => { + let next_state = + process_gif_frame(animation, self.gif_state.get(url), animation_mode); + + if let Some(new_state) = next_state.maybe_new_state { + self.gif_state.insert(url.to_owned(), new_state); + } + + if let Some(repaint) = next_state.repaint_at { + tracing::trace!("requesting repaint for {url} after {repaint:?}"); + if let Ok(dur) = repaint.duration_since(SystemTime::now()) { + ctx.request_repaint_after(dur); + } + } + + LatestImageTex::Loaded(next_state.texture) + } + } + } + } + } +} + +pub enum LatestImageTex<'a> { + Pending, + Error(&'a Error), + Loaded(&'a TextureHandle), +} + +pub struct UntrustedMediaLatestTex<'a> { + blur_cache: &'a BlurCache, +} + +/// Media is untrusted and should only show a blur of the underlying media +impl<'a> UntrustedMediaLatestTex<'a> { + pub fn new(blur_cache: &'a BlurCache) -> Self { + Self { blur_cache } + } + + pub fn latest( + &self, + jobs: &MediaJobSender, + ui: &egui::Ui, + url: &str, + obfuscation_type: &'a ObfuscationType, + size: egui::Vec2, + ) -> MediaRenderState<'a> { + MediaRenderState::Obfuscated(self.latest_internal(jobs, ui, url, obfuscation_type, size)) + } + + fn latest_internal( + &self, + jobs: &MediaJobSender, + ui: &egui::Ui, + url: &str, + obfuscation_type: &'a ObfuscationType, + size: egui::Vec2, + ) -> ObfuscatedTexture<'a> { + let ObfuscationType::Blurhash(meta) = obfuscation_type else { + return ObfuscatedTexture::Default; + }; + + let state = self.blur_cache.get_or_request(jobs, ui, url, meta, size); + + match &state.tex_state { + TextureState::Pending | TextureState::Error(_) => ObfuscatedTexture::Default, + TextureState::Loaded(t) => ObfuscatedTexture::Blur(t), + } + } +} + +/// Media is trusted and should be loaded ASAP +pub struct TrustedMediaLatestTex<'a> { + img_no_loading: NoLoadingLatestTex<'a>, + blur_cache: &'a BlurCache, +} + +impl<'a> TrustedMediaLatestTex<'a> { + pub fn new(img_no_loading: NoLoadingLatestTex<'a>, blur_cache: &'a BlurCache) -> Self { + Self { + img_no_loading, + blur_cache, + } + } + + #[allow(clippy::too_many_arguments)] + pub fn latest( + &mut self, + jobs: &MediaJobSender, + ui: &egui::Ui, + url: &str, + cache_type: MediaCacheType, + imgtype: ImageType, + animation_mode: AnimationMode, + obfuscation_type: &'a ObfuscationType, + size: egui::Vec2, + ) -> MediaRenderState<'a> { + let actual_latest_tex = self.img_no_loading.latest_state( + jobs, + ui.ctx(), + url, + cache_type, + imgtype, + animation_mode, + ); + + match actual_latest_tex { + LatestImageTex::Pending => (), + LatestImageTex::Error(error) => return MediaRenderState::Error(error), + LatestImageTex::Loaded(texture_handle) => { + let Some(blur) = self.blur_cache.get(url) else { + return MediaRenderState::ActualImage(texture_handle); + }; + + if blur.finished_transitioning { + return MediaRenderState::ActualImage(texture_handle); + }; + + let obfuscation = match &blur.tex_state { + TextureState::Pending | TextureState::Error(_) => ObfuscatedTexture::Default, + TextureState::Loaded(t) => ObfuscatedTexture::Blur(t), + }; + + return MediaRenderState::Transitioning { + image: texture_handle, + obfuscation, + }; + } + }; + + MediaRenderState::Shimmering( + UntrustedMediaLatestTex::new(self.blur_cache).latest_internal( + jobs, + ui, + url, + obfuscation_type, + size, + ), + ) + } +} diff --git a/crates/notedeck/src/media/mod.rs b/crates/notedeck/src/media/mod.rs @@ -3,6 +3,7 @@ pub mod blur; pub mod gif; pub mod images; pub mod imeta; +pub mod latest; pub mod network; pub mod renderable; pub mod static_imgs; @@ -14,6 +15,9 @@ pub use blur::{ }; use egui::{ColorImage, TextureHandle}; pub use images::ImageType; +pub use latest::{ + MediaRenderState, NoLoadingLatestTex, TrustedMediaLatestTex, UntrustedMediaLatestTex, +}; pub use renderable::RenderableMedia; #[derive(Copy, Clone, Debug)]