ui: add AnimationMode to control GIF rendering behavior
Introduces an `AnimationMode` enum with `Reactive`, `Continuous`, and `NoAnimation` variants to allow fine-grained control over GIF playback across the UI. This supports performance optimizations and accessibility features, such as disabling animations when requested. - Plumbs AnimationMode through image rendering paths - Replaces hardcoded gif frame logic with reusable `process_gif_frame` - Supports customizable FPS in Continuous mode - Enables global animation opt-out via `NoteOptions::NoAnimations` - Applies mode-specific logic in profile pictures, posts, media carousels, and viewer Animation behavior by context ----------------------------- - Profile pictures: Reactive (render only on interaction/activity) - PostView: NoAnimation if disabled in NoteOptions, else Continuous (uncapped) - Media carousels: NoAnimation or Continuous (capped at 24fps) - Viewer/gallery: Always Continuous (full animation) In the future, we can customize these by power settings. Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
use crate::media::gif::ensure_latest_texture_from_cache;
|
||||
use crate::media::images::ImageType;
|
||||
use crate::media::AnimationMode;
|
||||
use crate::urls::{UrlCache, UrlMimes};
|
||||
use crate::ImageMetadata;
|
||||
use crate::ObfuscationType;
|
||||
@@ -464,6 +465,7 @@ impl Images {
|
||||
ui: &mut egui::Ui,
|
||||
url: &str,
|
||||
img_type: ImageType,
|
||||
animation_mode: AnimationMode,
|
||||
) -> Option<TextureHandle> {
|
||||
let cache_type = crate::urls::supported_mime_hosted_at_url(&mut self.urls, url)?;
|
||||
|
||||
@@ -485,7 +487,13 @@ impl Images {
|
||||
MediaCacheType::Gif => &mut self.gifs,
|
||||
};
|
||||
|
||||
ensure_latest_texture_from_cache(ui, url, &mut self.gif_states, &mut cache.textures_cache)
|
||||
ensure_latest_texture_from_cache(
|
||||
ui,
|
||||
url,
|
||||
&mut self.gif_states,
|
||||
&mut cache.textures_cache,
|
||||
animation_mode,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get_cache(&self, cache_type: MediaCacheType) -> &MediaCache {
|
||||
|
||||
@@ -3,14 +3,18 @@ use std::{
|
||||
time::{Instant, SystemTime},
|
||||
};
|
||||
|
||||
use crate::media::AnimationMode;
|
||||
use crate::Animation;
|
||||
use crate::{GifState, GifStateMap, TextureState, TexturedImage, TexturesCache};
|
||||
use egui::TextureHandle;
|
||||
use std::time::Duration;
|
||||
|
||||
pub fn ensure_latest_texture_from_cache(
|
||||
ui: &egui::Ui,
|
||||
url: &str,
|
||||
gifs: &mut GifStateMap,
|
||||
textures: &mut TexturesCache,
|
||||
animation_mode: AnimationMode,
|
||||
) -> Option<TextureHandle> {
|
||||
let tstate = textures.cache.get_mut(url)?;
|
||||
|
||||
@@ -18,7 +22,102 @@ pub fn ensure_latest_texture_from_cache(
|
||||
return None;
|
||||
};
|
||||
|
||||
Some(ensure_latest_texture(ui, url, gifs, img))
|
||||
Some(ensure_latest_texture(ui, url, gifs, img, animation_mode))
|
||||
}
|
||||
|
||||
struct ProcessedGifFrame {
|
||||
texture: TextureHandle,
|
||||
maybe_new_state: Option<GifState>,
|
||||
repaint_at: Option<SystemTime>,
|
||||
}
|
||||
|
||||
/// Process a gif state frame, and optionally present a new
|
||||
/// state and when to repaint it
|
||||
fn process_gif_frame(
|
||||
animation: &Animation,
|
||||
frame_state: Option<&GifState>,
|
||||
animation_mode: AnimationMode,
|
||||
) -> ProcessedGifFrame {
|
||||
let now = Instant::now();
|
||||
|
||||
match frame_state {
|
||||
Some(prev_state) => {
|
||||
let should_advance = animation_mode.can_animate()
|
||||
&& (now - prev_state.last_frame_rendered >= prev_state.last_frame_duration);
|
||||
|
||||
if should_advance {
|
||||
let maybe_new_index = if animation.receiver.is_some()
|
||||
|| prev_state.last_frame_index < animation.num_frames() - 1
|
||||
{
|
||||
prev_state.last_frame_index + 1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
match animation.get_frame(maybe_new_index) {
|
||||
Some(frame) => {
|
||||
let next_frame_time = match animation_mode {
|
||||
AnimationMode::Continuous { fps } => match fps {
|
||||
Some(fps) => {
|
||||
let max_delay_ms = Duration::from_millis((1000.0 / fps) as u64);
|
||||
SystemTime::now().checked_add(frame.delay.max(max_delay_ms))
|
||||
}
|
||||
None => SystemTime::now().checked_add(frame.delay),
|
||||
},
|
||||
|
||||
AnimationMode::NoAnimation | AnimationMode::Reactive => None,
|
||||
};
|
||||
|
||||
ProcessedGifFrame {
|
||||
texture: frame.texture.clone(),
|
||||
maybe_new_state: Some(GifState {
|
||||
last_frame_rendered: now,
|
||||
last_frame_duration: frame.delay,
|
||||
next_frame_time,
|
||||
last_frame_index: maybe_new_index,
|
||||
}),
|
||||
repaint_at: next_frame_time,
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let (texture, maybe_new_state) =
|
||||
match animation.get_frame(prev_state.last_frame_index) {
|
||||
Some(frame) => (frame.texture.clone(), None),
|
||||
None => (animation.first_frame.texture.clone(), None),
|
||||
};
|
||||
|
||||
ProcessedGifFrame {
|
||||
texture,
|
||||
maybe_new_state,
|
||||
repaint_at: prev_state.next_frame_time,
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let (texture, maybe_new_state) =
|
||||
match animation.get_frame(prev_state.last_frame_index) {
|
||||
Some(frame) => (frame.texture.clone(), None),
|
||||
None => (animation.first_frame.texture.clone(), None),
|
||||
};
|
||||
|
||||
ProcessedGifFrame {
|
||||
texture,
|
||||
maybe_new_state,
|
||||
repaint_at: prev_state.next_frame_time,
|
||||
}
|
||||
}
|
||||
}
|
||||
None => ProcessedGifFrame {
|
||||
texture: animation.first_frame.texture.clone(),
|
||||
maybe_new_state: Some(GifState {
|
||||
last_frame_rendered: now,
|
||||
last_frame_duration: animation.first_frame.delay,
|
||||
next_frame_time: None,
|
||||
last_frame_index: 0,
|
||||
}),
|
||||
repaint_at: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ensure_latest_texture(
|
||||
@@ -26,6 +125,7 @@ pub fn ensure_latest_texture(
|
||||
url: &str,
|
||||
gifs: &mut GifStateMap,
|
||||
img: &mut TexturedImage,
|
||||
animation_mode: AnimationMode,
|
||||
) -> TextureHandle {
|
||||
match img {
|
||||
TexturedImage::Static(handle) => handle.clone(),
|
||||
@@ -45,80 +145,21 @@ pub fn ensure_latest_texture(
|
||||
}
|
||||
}
|
||||
|
||||
let now = Instant::now();
|
||||
let (texture, maybe_new_state, request_next_repaint) = match gifs.get(url) {
|
||||
Some(prev_state) => {
|
||||
let should_advance =
|
||||
now - prev_state.last_frame_rendered >= prev_state.last_frame_duration;
|
||||
let next_state = process_gif_frame(animation, gifs.get(url), animation_mode);
|
||||
|
||||
if should_advance {
|
||||
let maybe_new_index = if animation.receiver.is_some()
|
||||
|| prev_state.last_frame_index < animation.num_frames() - 1
|
||||
{
|
||||
prev_state.last_frame_index + 1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
match animation.get_frame(maybe_new_index) {
|
||||
Some(frame) => {
|
||||
let next_frame_time = SystemTime::now().checked_add(frame.delay);
|
||||
(
|
||||
&frame.texture,
|
||||
Some(GifState {
|
||||
last_frame_rendered: now,
|
||||
last_frame_duration: frame.delay,
|
||||
next_frame_time,
|
||||
last_frame_index: maybe_new_index,
|
||||
}),
|
||||
next_frame_time,
|
||||
)
|
||||
}
|
||||
None => {
|
||||
let (tex, state) =
|
||||
match animation.get_frame(prev_state.last_frame_index) {
|
||||
Some(frame) => (&frame.texture, None),
|
||||
None => (&animation.first_frame.texture, None),
|
||||
};
|
||||
|
||||
(tex, state, prev_state.next_frame_time)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let (tex, state) = match animation.get_frame(prev_state.last_frame_index) {
|
||||
Some(frame) => (&frame.texture, None),
|
||||
None => (&animation.first_frame.texture, None),
|
||||
};
|
||||
(tex, state, prev_state.next_frame_time)
|
||||
}
|
||||
}
|
||||
None => (
|
||||
&animation.first_frame.texture,
|
||||
Some(GifState {
|
||||
last_frame_rendered: now,
|
||||
last_frame_duration: animation.first_frame.delay,
|
||||
next_frame_time: None,
|
||||
last_frame_index: 0,
|
||||
}),
|
||||
None,
|
||||
),
|
||||
};
|
||||
|
||||
if let Some(new_state) = maybe_new_state {
|
||||
if let Some(new_state) = next_state.maybe_new_state {
|
||||
gifs.insert(url.to_owned(), new_state);
|
||||
}
|
||||
|
||||
if let Some(req) = request_next_repaint {
|
||||
if let Some(req) = next_state.repaint_at {
|
||||
// TODO(jb55): make a continuous gif rendering setting
|
||||
// 24fps for gif is fine
|
||||
/*
|
||||
tracing::trace!("requesting repaint for {url} after {req:?}");
|
||||
ui.ctx()
|
||||
.request_repaint_after(std::time::Duration::from_millis(41));
|
||||
*/
|
||||
}
|
||||
|
||||
texture.clone()
|
||||
next_state.texture
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,3 +12,21 @@ pub use blur::{
|
||||
};
|
||||
pub use images::ImageType;
|
||||
pub use renderable::RenderableMedia;
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum AnimationMode {
|
||||
/// Only render when scrolling, network activity, etc
|
||||
Reactive,
|
||||
|
||||
/// Continuous with an optional target fps
|
||||
Continuous { fps: Option<f32> },
|
||||
|
||||
/// Disable animation
|
||||
NoAnimation,
|
||||
}
|
||||
|
||||
impl AnimationMode {
|
||||
pub fn can_animate(&self) -> bool {
|
||||
!matches!(self, Self::NoAnimation)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user