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:
William Casarin
2025-08-04 13:38:27 -07:00
parent 54b86ee5a6
commit b94e715539
8 changed files with 206 additions and 73 deletions

View File

@@ -3,6 +3,7 @@ use egui::{vec2, InnerResponse, Sense, Stroke, TextureHandle};
use notedeck::get_render_state;
use notedeck::media::gif::ensure_latest_texture;
use notedeck::media::images::{fetch_no_pfp_promise, ImageType};
use notedeck::media::AnimationMode;
use notedeck::MediaAction;
use notedeck::{show_one_error_message, supported_mime_hosted_at_url, Images};
@@ -12,12 +13,21 @@ pub struct ProfilePic<'cache, 'url> {
size: f32,
sense: Sense,
border: Option<Stroke>,
animation_mode: AnimationMode,
pub action: Option<MediaAction>,
}
impl egui::Widget for &mut ProfilePic<'_, '_> {
fn ui(self, ui: &mut egui::Ui) -> egui::Response {
let inner = render_pfp(ui, self.cache, self.url, self.size, self.border, self.sense);
let inner = render_pfp(
ui,
self.cache,
self.url,
self.size,
self.border,
self.sense,
self.animation_mode,
);
self.action = inner.inner;
@@ -35,6 +45,7 @@ impl<'cache, 'url> ProfilePic<'cache, 'url> {
sense,
url,
size,
animation_mode: AnimationMode::Reactive,
border: None,
action: None,
}
@@ -45,6 +56,11 @@ impl<'cache, 'url> ProfilePic<'cache, 'url> {
self
}
pub fn animation_mode(mut self, mode: AnimationMode) -> Self {
self.animation_mode = mode;
self
}
pub fn border_stroke(ui: &egui::Ui) -> Stroke {
Stroke::new(4.0, ui.visuals().panel_fill)
}
@@ -109,6 +125,7 @@ fn render_pfp(
ui_size: f32,
border: Option<Stroke>,
sense: Sense,
animation_mode: AnimationMode,
) -> InnerResponse<Option<MediaAction>> {
// We will want to downsample these so it's not blurry on hi res displays
let img_size = 128u32;
@@ -141,7 +158,8 @@ fn render_pfp(
)
}
notedeck::TextureState::Loaded(textured_image) => {
let texture_handle = ensure_latest_texture(ui, url, cur_state.gifs, textured_image);
let texture_handle =
ensure_latest_texture(ui, url, cur_state.gifs, textured_image, animation_mode);
egui::InnerResponse::new(None, pfp_image(ui, &texture_handle, ui_size, border, sense))
}