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,6 +1,6 @@
|
||||
use bitflags::bitflags;
|
||||
use egui::{emath::TSTransform, pos2, Color32, Rangef, Rect};
|
||||
use notedeck::media::{MediaInfo, ViewMediaInfo};
|
||||
use notedeck::media::{AnimationMode, MediaInfo, ViewMediaInfo};
|
||||
use notedeck::{ImageType, Images};
|
||||
|
||||
bitflags! {
|
||||
@@ -176,7 +176,12 @@ impl<'a> MediaViewer<'a> {
|
||||
/// we have image layouts
|
||||
fn first_image_rect(ui: &mut egui::Ui, media: &MediaInfo, images: &mut Images) -> Rect {
|
||||
// fetch image texture
|
||||
let Some(texture) = images.latest_texture(ui, &media.url, ImageType::Content(None)) else {
|
||||
let Some(texture) = images.latest_texture(
|
||||
ui,
|
||||
&media.url,
|
||||
ImageType::Content(None),
|
||||
AnimationMode::NoAnimation,
|
||||
) else {
|
||||
tracing::error!("could not get latest texture in first_image_rect");
|
||||
return Rect::ZERO;
|
||||
};
|
||||
@@ -206,7 +211,14 @@ impl<'a> MediaViewer<'a> {
|
||||
let url = &info.url;
|
||||
|
||||
// fetch image texture
|
||||
let Some(texture) = images.latest_texture(ui, url, ImageType::Content(None)) else {
|
||||
|
||||
// we want to continually redraw things in the gallery
|
||||
let Some(texture) = images.latest_texture(
|
||||
ui,
|
||||
url,
|
||||
ImageType::Content(None),
|
||||
AnimationMode::Continuous { fps: None }, // media viewer has continuous rendering
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ use notedeck::{
|
||||
use crate::NoteOptions;
|
||||
use notedeck::media::gif::ensure_latest_texture;
|
||||
use notedeck::media::images::{fetch_no_pfp_promise, ImageType};
|
||||
use notedeck::media::AnimationMode;
|
||||
use notedeck::media::{MediaInfo, ViewMediaInfo};
|
||||
|
||||
use crate::{app_images, AnimationHelper, PulseAlpha};
|
||||
@@ -82,6 +83,18 @@ pub fn image_carousel(
|
||||
blur_type,
|
||||
);
|
||||
|
||||
let animation_mode = if note_options.contains(NoteOptions::NoAnimations)
|
||||
{
|
||||
AnimationMode::NoAnimation
|
||||
} else {
|
||||
// if animations aren't disabled, we cap it at 24fps for gifs in carousels
|
||||
let fps = match media_type {
|
||||
MediaCacheType::Gif => Some(24.0),
|
||||
MediaCacheType::Image => None,
|
||||
};
|
||||
AnimationMode::Continuous { fps }
|
||||
};
|
||||
|
||||
let media_response = render_media(
|
||||
ui,
|
||||
&mut img_cache.gif_states,
|
||||
@@ -90,6 +103,7 @@ pub fn image_carousel(
|
||||
size,
|
||||
i18n,
|
||||
note_options.contains(NoteOptions::Wide),
|
||||
animation_mode,
|
||||
);
|
||||
|
||||
if let Some(action) = media_response.inner {
|
||||
@@ -324,10 +338,12 @@ fn render_media(
|
||||
size: egui::Vec2,
|
||||
i18n: &mut Localization,
|
||||
is_scaled: bool,
|
||||
animation_mode: AnimationMode,
|
||||
) -> egui::InnerResponse<Option<MediaUIAction>> {
|
||||
match render_state {
|
||||
MediaRenderState::ActualImage(image) => {
|
||||
let resp = render_success_media(ui, url, image, gifs, size, i18n, is_scaled);
|
||||
let resp =
|
||||
render_success_media(ui, url, image, gifs, size, i18n, is_scaled, animation_mode);
|
||||
if resp.clicked() {
|
||||
egui::InnerResponse::new(Some(MediaUIAction::Clicked), resp)
|
||||
} else {
|
||||
@@ -559,6 +575,7 @@ pub(crate) fn find_renderable_media<'a>(
|
||||
}
|
||||
*/
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_success_media(
|
||||
ui: &mut egui::Ui,
|
||||
url: &str,
|
||||
@@ -567,8 +584,9 @@ fn render_success_media(
|
||||
size: Vec2,
|
||||
i18n: &mut Localization,
|
||||
is_scaled: bool,
|
||||
animation_mode: AnimationMode,
|
||||
) -> Response {
|
||||
let texture = ensure_latest_texture(ui, url, gifs, tex);
|
||||
let texture = ensure_latest_texture(ui, url, gifs, tex, animation_mode);
|
||||
|
||||
let scaled = ScaledTexture::new(&texture, size, is_scaled);
|
||||
|
||||
|
||||
@@ -35,6 +35,9 @@ bitflags! {
|
||||
|
||||
/// Note has an unread reply indicator
|
||||
const UnreadIndicator = 1 << 16;
|
||||
|
||||
/// no animation override (accessibility)
|
||||
const NoAnimations = 1 << 17;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user