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::gif::ensure_latest_texture_from_cache;
|
||||||
use crate::media::images::ImageType;
|
use crate::media::images::ImageType;
|
||||||
|
use crate::media::AnimationMode;
|
||||||
use crate::urls::{UrlCache, UrlMimes};
|
use crate::urls::{UrlCache, UrlMimes};
|
||||||
use crate::ImageMetadata;
|
use crate::ImageMetadata;
|
||||||
use crate::ObfuscationType;
|
use crate::ObfuscationType;
|
||||||
@@ -464,6 +465,7 @@ impl Images {
|
|||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
url: &str,
|
url: &str,
|
||||||
img_type: ImageType,
|
img_type: ImageType,
|
||||||
|
animation_mode: AnimationMode,
|
||||||
) -> Option<TextureHandle> {
|
) -> Option<TextureHandle> {
|
||||||
let cache_type = crate::urls::supported_mime_hosted_at_url(&mut self.urls, url)?;
|
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,
|
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 {
|
pub fn get_cache(&self, cache_type: MediaCacheType) -> &MediaCache {
|
||||||
|
|||||||
@@ -3,14 +3,18 @@ use std::{
|
|||||||
time::{Instant, SystemTime},
|
time::{Instant, SystemTime},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::media::AnimationMode;
|
||||||
|
use crate::Animation;
|
||||||
use crate::{GifState, GifStateMap, TextureState, TexturedImage, TexturesCache};
|
use crate::{GifState, GifStateMap, TextureState, TexturedImage, TexturesCache};
|
||||||
use egui::TextureHandle;
|
use egui::TextureHandle;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
pub fn ensure_latest_texture_from_cache(
|
pub fn ensure_latest_texture_from_cache(
|
||||||
ui: &egui::Ui,
|
ui: &egui::Ui,
|
||||||
url: &str,
|
url: &str,
|
||||||
gifs: &mut GifStateMap,
|
gifs: &mut GifStateMap,
|
||||||
textures: &mut TexturesCache,
|
textures: &mut TexturesCache,
|
||||||
|
animation_mode: AnimationMode,
|
||||||
) -> Option<TextureHandle> {
|
) -> Option<TextureHandle> {
|
||||||
let tstate = textures.cache.get_mut(url)?;
|
let tstate = textures.cache.get_mut(url)?;
|
||||||
|
|
||||||
@@ -18,7 +22,102 @@ pub fn ensure_latest_texture_from_cache(
|
|||||||
return None;
|
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(
|
pub fn ensure_latest_texture(
|
||||||
@@ -26,6 +125,7 @@ pub fn ensure_latest_texture(
|
|||||||
url: &str,
|
url: &str,
|
||||||
gifs: &mut GifStateMap,
|
gifs: &mut GifStateMap,
|
||||||
img: &mut TexturedImage,
|
img: &mut TexturedImage,
|
||||||
|
animation_mode: AnimationMode,
|
||||||
) -> TextureHandle {
|
) -> TextureHandle {
|
||||||
match img {
|
match img {
|
||||||
TexturedImage::Static(handle) => handle.clone(),
|
TexturedImage::Static(handle) => handle.clone(),
|
||||||
@@ -45,80 +145,21 @@ pub fn ensure_latest_texture(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let now = Instant::now();
|
let next_state = process_gif_frame(animation, gifs.get(url), animation_mode);
|
||||||
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;
|
|
||||||
|
|
||||||
if should_advance {
|
if let Some(new_state) = next_state.maybe_new_state {
|
||||||
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 {
|
|
||||||
gifs.insert(url.to_owned(), 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
|
// TODO(jb55): make a continuous gif rendering setting
|
||||||
// 24fps for gif is fine
|
// 24fps for gif is fine
|
||||||
/*
|
|
||||||
tracing::trace!("requesting repaint for {url} after {req:?}");
|
tracing::trace!("requesting repaint for {url} after {req:?}");
|
||||||
ui.ctx()
|
ui.ctx()
|
||||||
.request_repaint_after(std::time::Duration::from_millis(41));
|
.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 images::ImageType;
|
||||||
pub use renderable::RenderableMedia;
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ use egui::{
|
|||||||
use enostr::{FilledKeypair, FullKeypair, NoteId, Pubkey, RelayPool};
|
use enostr::{FilledKeypair, FullKeypair, NoteId, Pubkey, RelayPool};
|
||||||
use nostrdb::{Ndb, Transaction};
|
use nostrdb::{Ndb, Transaction};
|
||||||
use notedeck::media::gif::ensure_latest_texture;
|
use notedeck::media::gif::ensure_latest_texture;
|
||||||
|
use notedeck::media::AnimationMode;
|
||||||
use notedeck::{get_render_state, JobsCache, PixelDimensions, RenderState};
|
use notedeck::{get_render_state, JobsCache, PixelDimensions, RenderState};
|
||||||
|
|
||||||
use notedeck_ui::{
|
use notedeck_ui::{
|
||||||
@@ -37,6 +38,7 @@ pub struct PostView<'a, 'd> {
|
|||||||
inner_rect: egui::Rect,
|
inner_rect: egui::Rect,
|
||||||
note_options: NoteOptions,
|
note_options: NoteOptions,
|
||||||
jobs: &'a mut JobsCache,
|
jobs: &'a mut JobsCache,
|
||||||
|
animation_mode: AnimationMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -110,6 +112,11 @@ impl<'a, 'd> PostView<'a, 'd> {
|
|||||||
note_options: NoteOptions,
|
note_options: NoteOptions,
|
||||||
jobs: &'a mut JobsCache,
|
jobs: &'a mut JobsCache,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
let animation_mode = if note_options.contains(NoteOptions::NoAnimations) {
|
||||||
|
AnimationMode::NoAnimation
|
||||||
|
} else {
|
||||||
|
AnimationMode::Continuous { fps: None }
|
||||||
|
};
|
||||||
PostView {
|
PostView {
|
||||||
note_context,
|
note_context,
|
||||||
draft,
|
draft,
|
||||||
@@ -117,6 +124,7 @@ impl<'a, 'd> PostView<'a, 'd> {
|
|||||||
post_type,
|
post_type,
|
||||||
inner_rect,
|
inner_rect,
|
||||||
note_options,
|
note_options,
|
||||||
|
animation_mode,
|
||||||
jobs,
|
jobs,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -129,6 +137,11 @@ impl<'a, 'd> PostView<'a, 'd> {
|
|||||||
PostView::id().with("scroll")
|
PostView::id().with("scroll")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn animation_mode(mut self, animation_mode: AnimationMode) -> Self {
|
||||||
|
self.animation_mode = animation_mode;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
fn editbox(&mut self, txn: &nostrdb::Transaction, ui: &mut egui::Ui) -> egui::Response {
|
fn editbox(&mut self, txn: &nostrdb::Transaction, ui: &mut egui::Ui) -> egui::Response {
|
||||||
ui.spacing_mut().item_spacing.x = 12.0;
|
ui.spacing_mut().item_spacing.x = 12.0;
|
||||||
|
|
||||||
@@ -492,6 +505,7 @@ impl<'a, 'd> PostView<'a, 'd> {
|
|||||||
height,
|
height,
|
||||||
cur_state,
|
cur_state,
|
||||||
url,
|
url,
|
||||||
|
self.animation_mode,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
to_remove.reverse();
|
to_remove.reverse();
|
||||||
@@ -582,6 +596,7 @@ fn render_post_view_media(
|
|||||||
height: u32,
|
height: u32,
|
||||||
render_state: RenderState,
|
render_state: RenderState,
|
||||||
url: &str,
|
url: &str,
|
||||||
|
animation_mode: AnimationMode,
|
||||||
) {
|
) {
|
||||||
match render_state.texture_state {
|
match render_state.texture_state {
|
||||||
notedeck::TextureState::Pending => {
|
notedeck::TextureState::Pending => {
|
||||||
@@ -605,7 +620,7 @@ fn render_post_view_media(
|
|||||||
.to_vec();
|
.to_vec();
|
||||||
|
|
||||||
let texture_handle =
|
let texture_handle =
|
||||||
ensure_latest_texture(ui, url, render_state.gifs, renderable_media);
|
ensure_latest_texture(ui, url, render_state.gifs, renderable_media, animation_mode);
|
||||||
let img_resp = ui.add(
|
let img_resp = ui.add(
|
||||||
egui::Image::new(&texture_handle)
|
egui::Image::new(&texture_handle)
|
||||||
.max_size(size)
|
.max_size(size)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use bitflags::bitflags;
|
use bitflags::bitflags;
|
||||||
use egui::{emath::TSTransform, pos2, Color32, Rangef, Rect};
|
use egui::{emath::TSTransform, pos2, Color32, Rangef, Rect};
|
||||||
use notedeck::media::{MediaInfo, ViewMediaInfo};
|
use notedeck::media::{AnimationMode, MediaInfo, ViewMediaInfo};
|
||||||
use notedeck::{ImageType, Images};
|
use notedeck::{ImageType, Images};
|
||||||
|
|
||||||
bitflags! {
|
bitflags! {
|
||||||
@@ -176,7 +176,12 @@ impl<'a> MediaViewer<'a> {
|
|||||||
/// we have image layouts
|
/// we have image layouts
|
||||||
fn first_image_rect(ui: &mut egui::Ui, media: &MediaInfo, images: &mut Images) -> Rect {
|
fn first_image_rect(ui: &mut egui::Ui, media: &MediaInfo, images: &mut Images) -> Rect {
|
||||||
// fetch image texture
|
// 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");
|
tracing::error!("could not get latest texture in first_image_rect");
|
||||||
return Rect::ZERO;
|
return Rect::ZERO;
|
||||||
};
|
};
|
||||||
@@ -206,7 +211,14 @@ impl<'a> MediaViewer<'a> {
|
|||||||
let url = &info.url;
|
let url = &info.url;
|
||||||
|
|
||||||
// fetch image texture
|
// 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;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use notedeck::{
|
|||||||
use crate::NoteOptions;
|
use crate::NoteOptions;
|
||||||
use notedeck::media::gif::ensure_latest_texture;
|
use notedeck::media::gif::ensure_latest_texture;
|
||||||
use notedeck::media::images::{fetch_no_pfp_promise, ImageType};
|
use notedeck::media::images::{fetch_no_pfp_promise, ImageType};
|
||||||
|
use notedeck::media::AnimationMode;
|
||||||
use notedeck::media::{MediaInfo, ViewMediaInfo};
|
use notedeck::media::{MediaInfo, ViewMediaInfo};
|
||||||
|
|
||||||
use crate::{app_images, AnimationHelper, PulseAlpha};
|
use crate::{app_images, AnimationHelper, PulseAlpha};
|
||||||
@@ -82,6 +83,18 @@ pub fn image_carousel(
|
|||||||
blur_type,
|
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(
|
let media_response = render_media(
|
||||||
ui,
|
ui,
|
||||||
&mut img_cache.gif_states,
|
&mut img_cache.gif_states,
|
||||||
@@ -90,6 +103,7 @@ pub fn image_carousel(
|
|||||||
size,
|
size,
|
||||||
i18n,
|
i18n,
|
||||||
note_options.contains(NoteOptions::Wide),
|
note_options.contains(NoteOptions::Wide),
|
||||||
|
animation_mode,
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Some(action) = media_response.inner {
|
if let Some(action) = media_response.inner {
|
||||||
@@ -324,10 +338,12 @@ fn render_media(
|
|||||||
size: egui::Vec2,
|
size: egui::Vec2,
|
||||||
i18n: &mut Localization,
|
i18n: &mut Localization,
|
||||||
is_scaled: bool,
|
is_scaled: bool,
|
||||||
|
animation_mode: AnimationMode,
|
||||||
) -> egui::InnerResponse<Option<MediaUIAction>> {
|
) -> egui::InnerResponse<Option<MediaUIAction>> {
|
||||||
match render_state {
|
match render_state {
|
||||||
MediaRenderState::ActualImage(image) => {
|
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() {
|
if resp.clicked() {
|
||||||
egui::InnerResponse::new(Some(MediaUIAction::Clicked), resp)
|
egui::InnerResponse::new(Some(MediaUIAction::Clicked), resp)
|
||||||
} else {
|
} else {
|
||||||
@@ -559,6 +575,7 @@ pub(crate) fn find_renderable_media<'a>(
|
|||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn render_success_media(
|
fn render_success_media(
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
url: &str,
|
url: &str,
|
||||||
@@ -567,8 +584,9 @@ fn render_success_media(
|
|||||||
size: Vec2,
|
size: Vec2,
|
||||||
i18n: &mut Localization,
|
i18n: &mut Localization,
|
||||||
is_scaled: bool,
|
is_scaled: bool,
|
||||||
|
animation_mode: AnimationMode,
|
||||||
) -> Response {
|
) -> 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);
|
let scaled = ScaledTexture::new(&texture, size, is_scaled);
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ bitflags! {
|
|||||||
|
|
||||||
/// Note has an unread reply indicator
|
/// Note has an unread reply indicator
|
||||||
const UnreadIndicator = 1 << 16;
|
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::get_render_state;
|
||||||
use notedeck::media::gif::ensure_latest_texture;
|
use notedeck::media::gif::ensure_latest_texture;
|
||||||
use notedeck::media::images::{fetch_no_pfp_promise, ImageType};
|
use notedeck::media::images::{fetch_no_pfp_promise, ImageType};
|
||||||
|
use notedeck::media::AnimationMode;
|
||||||
use notedeck::MediaAction;
|
use notedeck::MediaAction;
|
||||||
use notedeck::{show_one_error_message, supported_mime_hosted_at_url, Images};
|
use notedeck::{show_one_error_message, supported_mime_hosted_at_url, Images};
|
||||||
|
|
||||||
@@ -12,12 +13,21 @@ pub struct ProfilePic<'cache, 'url> {
|
|||||||
size: f32,
|
size: f32,
|
||||||
sense: Sense,
|
sense: Sense,
|
||||||
border: Option<Stroke>,
|
border: Option<Stroke>,
|
||||||
|
animation_mode: AnimationMode,
|
||||||
pub action: Option<MediaAction>,
|
pub action: Option<MediaAction>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl egui::Widget for &mut ProfilePic<'_, '_> {
|
impl egui::Widget for &mut ProfilePic<'_, '_> {
|
||||||
fn ui(self, ui: &mut egui::Ui) -> egui::Response {
|
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;
|
self.action = inner.inner;
|
||||||
|
|
||||||
@@ -35,6 +45,7 @@ impl<'cache, 'url> ProfilePic<'cache, 'url> {
|
|||||||
sense,
|
sense,
|
||||||
url,
|
url,
|
||||||
size,
|
size,
|
||||||
|
animation_mode: AnimationMode::Reactive,
|
||||||
border: None,
|
border: None,
|
||||||
action: None,
|
action: None,
|
||||||
}
|
}
|
||||||
@@ -45,6 +56,11 @@ impl<'cache, 'url> ProfilePic<'cache, 'url> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn animation_mode(mut self, mode: AnimationMode) -> Self {
|
||||||
|
self.animation_mode = mode;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn border_stroke(ui: &egui::Ui) -> Stroke {
|
pub fn border_stroke(ui: &egui::Ui) -> Stroke {
|
||||||
Stroke::new(4.0, ui.visuals().panel_fill)
|
Stroke::new(4.0, ui.visuals().panel_fill)
|
||||||
}
|
}
|
||||||
@@ -109,6 +125,7 @@ fn render_pfp(
|
|||||||
ui_size: f32,
|
ui_size: f32,
|
||||||
border: Option<Stroke>,
|
border: Option<Stroke>,
|
||||||
sense: Sense,
|
sense: Sense,
|
||||||
|
animation_mode: AnimationMode,
|
||||||
) -> InnerResponse<Option<MediaAction>> {
|
) -> InnerResponse<Option<MediaAction>> {
|
||||||
// We will want to downsample these so it's not blurry on hi res displays
|
// We will want to downsample these so it's not blurry on hi res displays
|
||||||
let img_size = 128u32;
|
let img_size = 128u32;
|
||||||
@@ -141,7 +158,8 @@ fn render_pfp(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
notedeck::TextureState::Loaded(textured_image) => {
|
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))
|
egui::InnerResponse::new(None, pfp_image(ui, &texture_handle, ui_size, border, sense))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user