Skip to main content

freya_components/
gif_viewer.rs

1use std::{
2    any::Any,
3    borrow::Cow,
4    collections::{
5        HashMap,
6        hash_map::DefaultHasher,
7    },
8    fs,
9    hash::{
10        Hash,
11        Hasher,
12    },
13    path::PathBuf,
14    rc::Rc,
15    time::Duration,
16};
17
18use anyhow::Context;
19use async_io::Timer;
20use blocking::unblock;
21use bytes::Bytes;
22use freya_core::{
23    elements::image::{
24        AspectRatio,
25        ImageData,
26        SamplingMode,
27    },
28    integration::*,
29    prelude::*,
30};
31use freya_engine::prelude::{
32    AlphaType,
33    ClipOp,
34    Color,
35    ColorType,
36    CubicResampler,
37    Data,
38    FilterMode,
39    ISize,
40    ImageInfo,
41    MipmapMode,
42    Paint,
43    Rect,
44    SamplingOptions,
45    SkImage,
46    SkRect,
47    raster_from_data,
48    raster_n32_premul,
49};
50use gif::DisposalMethod;
51#[cfg(feature = "remote-asset")]
52use reqwest::Url;
53use torin::prelude::Size2D;
54
55#[cfg(feature = "remote-asset")]
56use crate::http::Http;
57use crate::{
58    cache::*,
59    loader::CircularLoader,
60};
61
62/// ### URI
63///
64/// Good to load remote GIFs.
65///
66/// > Needs the `remote-asset` feature enabled.
67///
68/// ```rust
69/// # use freya::prelude::*;
70/// let source: GifSource =
71///     "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExeXh5YWhscmo0YmF3OG1oMmpnMzBnbXFjcDR5Y2xoODE2ZnRpc2FhZiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/HTZVeK0esRjyw/giphy.gif"
72///         .into();
73/// ```
74///
75/// ### Path
76///
77/// Good for dynamic loading.
78///
79/// ```rust
80/// # use freya::prelude::*;
81/// # use std::path::PathBuf;
82/// let source: GifSource = PathBuf::from("./examples/frog_typing.gif").into();
83/// ```
84/// ### Raw bytes
85///
86/// Good for embedded GIFs.
87///
88/// ```rust
89/// # use freya::prelude::*;
90/// let source: GifSource = (
91///     "frog-typing",
92///     include_bytes!("../../../examples/frog_typing.gif"),
93/// )
94///     .into();
95/// ```
96#[derive(PartialEq, Clone)]
97pub enum GifSource {
98    /// Remote GIF loaded from a URI.
99    ///
100    /// Requires the `remote-asset` feature.
101    #[cfg(feature = "remote-asset")]
102    Uri(Url),
103
104    Path(PathBuf),
105
106    Bytes(u64, Bytes),
107}
108
109impl From<(&'static str, Bytes)> for GifSource {
110    fn from((id, bytes): (&'static str, Bytes)) -> Self {
111        let mut hasher = DefaultHasher::default();
112        id.hash(&mut hasher);
113        Self::Bytes(hasher.finish(), bytes)
114    }
115}
116
117impl From<(&'static str, &'static [u8])> for GifSource {
118    fn from((id, bytes): (&'static str, &'static [u8])) -> Self {
119        let mut hasher = DefaultHasher::default();
120        id.hash(&mut hasher);
121        Self::Bytes(hasher.finish(), Bytes::from_static(bytes))
122    }
123}
124
125impl<const N: usize> From<(&'static str, &'static [u8; N])> for GifSource {
126    fn from((id, bytes): (&'static str, &'static [u8; N])) -> Self {
127        let mut hasher = DefaultHasher::default();
128        id.hash(&mut hasher);
129        Self::Bytes(hasher.finish(), Bytes::from_static(bytes))
130    }
131}
132
133#[cfg(feature = "remote-asset")]
134impl From<Url> for GifSource {
135    fn from(uri: Url) -> Self {
136        Self::Uri(uri)
137    }
138}
139
140#[cfg(feature = "remote-asset")]
141impl From<&'static str> for GifSource {
142    fn from(src: &'static str) -> Self {
143        Self::Uri(Url::parse(src).expect("Invalid URL"))
144    }
145}
146
147impl From<PathBuf> for GifSource {
148    fn from(path: PathBuf) -> Self {
149        Self::Path(path)
150    }
151}
152
153impl Hash for GifSource {
154    fn hash<H: Hasher>(&self, state: &mut H) {
155        match self {
156            #[cfg(feature = "remote-asset")]
157            Self::Uri(uri) => uri.hash(state),
158            Self::Path(path) => path.hash(state),
159            Self::Bytes(id, _) => id.hash(state),
160        }
161    }
162}
163
164impl GifSource {
165    pub async fn bytes(&self) -> anyhow::Result<Bytes> {
166        let source = self.clone();
167        #[cfg(feature = "remote-asset")]
168        let client = Http::get();
169        blocking::unblock(move || {
170            let bytes = match source {
171                #[cfg(feature = "remote-asset")]
172                Self::Uri(uri) => client.get(uri).send()?.error_for_status()?.bytes()?,
173                Self::Path(path) => fs::read(path).map(Bytes::from)?,
174                Self::Bytes(_, bytes) => bytes,
175            };
176            Ok(bytes)
177        })
178        .await
179    }
180}
181
182/// GIF viewer component.
183///
184/// # Example
185///
186/// ```rust
187/// # use freya::prelude::*;
188/// fn app() -> impl IntoElement {
189///     let source: GifSource =
190///         "https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExeXh5YWhscmo0YmF3OG1oMmpnMzBnbXFjcDR5Y2xoODE2ZnRpc2FhZiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/HTZVeK0esRjyw/giphy.gif"
191///             .into();
192///
193///     GifViewer::new(source)
194/// }
195///
196/// # use freya_testing::prelude::*;
197/// # use std::path::PathBuf;
198/// # launch_doc(|| {
199/// #   rect().center().expanded().child(GifViewer::new(("frog-typing", include_bytes!("../../../examples/frog_typing.gif"))))
200/// # }, "./images/gallery_gif_viewer.png").with_hook(|t| { t.poll(std::time::Duration::from_millis(1), std::time::Duration::from_millis(1500)); t.sync_and_update(); }).with_scale_factor(1.).render();
201/// ```
202///
203/// # Preview
204/// ![Gif Preview][gif_viewer]
205#[cfg_attr(feature = "docs",
206    doc = embed_doc_image::embed_image!("gif_viewer", "images/gallery_gif_viewer.png")
207)]
208#[derive(PartialEq)]
209pub struct GifViewer {
210    source: GifSource,
211    asset_age: AssetAge,
212
213    layout: LayoutData,
214    image_data: ImageData,
215    accessibility: AccessibilityData,
216
217    key: DiffKey,
218}
219
220impl GifViewer {
221    pub fn new(source: impl Into<GifSource>) -> Self {
222        GifViewer {
223            source: source.into(),
224            asset_age: AssetAge::default(),
225            layout: LayoutData::default(),
226            image_data: ImageData::default(),
227            accessibility: AccessibilityData::default(),
228            key: DiffKey::None,
229        }
230    }
231
232    /// Customize how long the GIF will remain cached after no longer being used.
233    ///
234    /// Defaults to [`AssetAge::default`] (1h).
235    pub fn asset_age(mut self, asset_age: impl Into<AssetAge>) -> Self {
236        self.asset_age = asset_age.into();
237        self
238    }
239}
240
241impl KeyExt for GifViewer {
242    fn write_key(&mut self) -> &mut DiffKey {
243        &mut self.key
244    }
245}
246
247impl LayoutExt for GifViewer {
248    fn get_layout(&mut self) -> &mut LayoutData {
249        &mut self.layout
250    }
251}
252
253impl ContainerSizeExt for GifViewer {}
254impl ContainerPositionExt for GifViewer {}
255
256impl ImageExt for GifViewer {
257    fn get_image_data(&mut self) -> &mut ImageData {
258        &mut self.image_data
259    }
260}
261
262impl AccessibilityExt for GifViewer {
263    fn get_accessibility_data(&mut self) -> &mut AccessibilityData {
264        &mut self.accessibility
265    }
266}
267
268enum Status {
269    Playing(usize),
270    Decoding,
271    Errored(String),
272}
273
274impl Component for GifViewer {
275    fn render(&self) -> impl IntoElement {
276        let asset_config = AssetConfiguration::new(&self.source, self.asset_age);
277        let asset_data = use_asset(&asset_config);
278        let mut status = use_state(|| Status::Decoding);
279        let mut cached_frames = use_state::<Option<Rc<CachedGifFrames>>>(|| None);
280        let mut asset_cacher = use_hook(AssetCacher::get);
281        let mut assets_tasks = use_state::<Vec<TaskHandle>>(Vec::new);
282
283        let mut stream_gif = async move |bytes: Bytes| -> anyhow::Result<()> {
284            // Decode and pre-composite all frames upfront
285            let frames_data = unblock(move || -> anyhow::Result<Vec<CachedFrame>> {
286                let mut decoder_options = gif::DecodeOptions::new();
287                decoder_options.set_color_output(gif::ColorOutput::RGBA);
288                let cursor = std::io::Cursor::new(&bytes);
289                let mut decoder = decoder_options.read_info(cursor)?;
290                let width = decoder.width() as i32;
291                let height = decoder.height() as i32;
292
293                // Create a surface for compositing frames
294                let mut surface =
295                    raster_n32_premul((width, height)).context("Failed to create GIF surface")?;
296
297                let mut frames: Vec<CachedFrame> = Vec::new();
298
299                while let Ok(Some(frame)) = decoder.read_next_frame() {
300                    // Handle disposal of previous frame
301                    if let Some(prev_frame) = frames.last()
302                        && prev_frame.dispose == DisposalMethod::Background
303                    {
304                        let canvas = surface.canvas();
305                        let clear_rect = Rect::from_xywh(
306                            prev_frame.left,
307                            prev_frame.top,
308                            prev_frame.width,
309                            prev_frame.height,
310                        );
311                        canvas.save();
312                        canvas.clip_rect(clear_rect, None, false);
313                        canvas.clear(Color::TRANSPARENT);
314                        canvas.restore();
315                    }
316
317                    // Decode frame image
318                    let row_bytes = (frame.width * 4) as usize;
319                    let data = unsafe { Data::new_bytes(&frame.buffer) };
320                    let isize = ISize::new(frame.width as i32, frame.height as i32);
321                    let frame_image = raster_from_data(
322                        &ImageInfo::new(isize, ColorType::RGBA8888, AlphaType::Unpremul, None),
323                        data,
324                        row_bytes,
325                    )
326                    .context("Failed to create GIF Frame.")?;
327
328                    // Composite frame onto surface
329                    surface.canvas().draw_image(
330                        &frame_image,
331                        (frame.left as f32, frame.top as f32),
332                        None,
333                    );
334
335                    // Take a snapshot of the fully composed frame
336                    let composed_image = surface.image_snapshot();
337
338                    frames.push(CachedFrame {
339                        image: composed_image,
340                        dispose: frame.dispose,
341                        left: frame.left as f32,
342                        top: frame.top as f32,
343                        width: frame.width as f32,
344                        height: frame.height as f32,
345                        delay: Duration::from_millis(frame.delay as u64 * 10),
346                    });
347                }
348
349                Ok(frames)
350            })
351            .await?;
352
353            let frames = Rc::new(CachedGifFrames {
354                frames: frames_data,
355            });
356            *cached_frames.write() = Some(frames.clone());
357
358            // Now loop through cached frames
359            loop {
360                for (i, frame) in frames.frames.iter().enumerate() {
361                    *status.write() = Status::Playing(i);
362                    Timer::after(frame.delay).await;
363                }
364            }
365        };
366
367        use_side_effect_with_deps(&self.source, {
368            let asset_config = asset_config.clone();
369            move |source| {
370                let source = source.clone();
371
372                // Cancel previous tasks
373                for asset_task in assets_tasks.write().drain(..) {
374                    asset_task.cancel();
375                }
376
377                match asset_cacher.read_asset(&asset_config) {
378                    Some(Asset::Pending) | Some(Asset::Error(_)) => {
379                        // Mark asset as loading
380                        asset_cacher.update_asset(asset_config.clone(), Asset::Loading);
381
382                        let asset_config = asset_config.clone();
383                        let asset_task = spawn(async move {
384                            match source.bytes().await {
385                                Ok(bytes) => {
386                                    // Cache the GIF bytes
387                                    asset_cacher
388                                        .update_asset(asset_config, Asset::Cached(Rc::new(bytes)));
389                                }
390                                Err(err) => {
391                                    asset_cacher
392                                        .update_asset(asset_config, Asset::Error(err.to_string()));
393                                }
394                            }
395                        });
396
397                        assets_tasks.write().push(asset_task);
398                    }
399                    _ => {}
400                }
401            }
402        });
403
404        use_side_effect(move || {
405            if let Some(Asset::Cached(asset)) = asset_cacher.subscribe_asset(&asset_config) {
406                if let Some(bytes) = asset.downcast_ref::<Bytes>().cloned() {
407                    let asset_task = spawn(async move {
408                        if let Err(err) = stream_gif(bytes).await {
409                            *status.write() = Status::Errored(err.to_string());
410                            #[cfg(debug_assertions)]
411                            tracing::error!(
412                                "Failed to render GIF by ID <{}>, error: {err:?}",
413                                asset_config.id
414                            );
415                        }
416                    });
417                    assets_tasks.write().push(asset_task);
418                } else {
419                    #[cfg(debug_assertions)]
420                    tracing::error!(
421                        "Failed to downcast asset of GIF by ID <{}>",
422                        asset_config.id
423                    )
424                }
425            }
426        });
427
428        match (asset_data, cached_frames.read().as_ref()) {
429            (Asset::Cached(_), Some(frames)) => match &*status.read() {
430                Status::Playing(frame_idx) => gif(frames.clone(), *frame_idx)
431                    .accessibility(self.accessibility.clone())
432                    .a11y_role(AccessibilityRole::Image)
433                    .layout(self.layout.clone())
434                    .image_data(self.image_data.clone())
435                    .into_element(),
436                Status::Decoding => rect()
437                    .layout(self.layout.clone())
438                    .center()
439                    .child(CircularLoader::new())
440                    .into_element(),
441                Status::Errored(err) => err.clone().into_element(),
442            },
443            (Asset::Cached(_), _) | (Asset::Pending | Asset::Loading, _) => rect()
444                .layout(self.layout.clone())
445                .center()
446                .child(CircularLoader::new())
447                .into(),
448            (Asset::Error(err), _) => err.into(),
449        }
450    }
451
452    fn render_key(&self) -> DiffKey {
453        self.key.clone().or(self.default_key())
454    }
455}
456
457pub struct Gif {
458    key: DiffKey,
459    element: GifElement,
460}
461
462impl Gif {
463    pub fn try_downcast(element: &dyn ElementExt) -> Option<GifElement> {
464        (element as &dyn Any).downcast_ref::<GifElement>().cloned()
465    }
466}
467
468impl From<Gif> for Element {
469    fn from(value: Gif) -> Self {
470        Element::Element {
471            key: value.key,
472            element: Rc::new(value.element),
473            elements: vec![],
474        }
475    }
476}
477
478fn gif(frames: Rc<CachedGifFrames>, frame_idx: usize) -> Gif {
479    Gif {
480        key: DiffKey::None,
481        element: GifElement {
482            frames,
483            frame_idx,
484            accessibility: AccessibilityData::default(),
485            layout: LayoutData::default(),
486            event_handlers: HashMap::default(),
487            image_data: ImageData::default(),
488        },
489    }
490}
491
492impl LayoutExt for Gif {
493    fn get_layout(&mut self) -> &mut LayoutData {
494        &mut self.element.layout
495    }
496}
497
498impl ContainerExt for Gif {}
499
500impl ImageExt for Gif {
501    fn get_image_data(&mut self) -> &mut ImageData {
502        &mut self.element.image_data
503    }
504}
505
506impl KeyExt for Gif {
507    fn write_key(&mut self) -> &mut DiffKey {
508        &mut self.key
509    }
510}
511
512impl EventHandlersExt for Gif {
513    fn get_event_handlers(&mut self) -> &mut FxHashMap<EventName, EventHandlerType> {
514        &mut self.element.event_handlers
515    }
516}
517
518impl AccessibilityExt for Gif {
519    fn get_accessibility_data(&mut self) -> &mut AccessibilityData {
520        &mut self.element.accessibility
521    }
522}
523impl MaybeExt for Gif {}
524
525#[derive(Clone)]
526pub struct GifElement {
527    accessibility: AccessibilityData,
528    layout: LayoutData,
529    event_handlers: FxHashMap<EventName, EventHandlerType>,
530    frames: Rc<CachedGifFrames>,
531    frame_idx: usize,
532    image_data: ImageData,
533}
534
535impl PartialEq for GifElement {
536    fn eq(&self, other: &Self) -> bool {
537        self.accessibility == other.accessibility
538            && self.layout == other.layout
539            && self.image_data == other.image_data
540            && Rc::ptr_eq(&self.frames, &other.frames)
541            && self.frame_idx == other.frame_idx
542    }
543}
544
545impl ElementExt for GifElement {
546    fn changed(&self, other: &Rc<dyn ElementExt>) -> bool {
547        let Some(image) = (other.as_ref() as &dyn Any).downcast_ref::<GifElement>() else {
548            return false;
549        };
550        self != image
551    }
552
553    fn diff(&self, other: &Rc<dyn ElementExt>) -> DiffModifies {
554        let Some(image) = (other.as_ref() as &dyn Any).downcast_ref::<GifElement>() else {
555            return DiffModifies::all();
556        };
557
558        let mut diff = DiffModifies::empty();
559
560        if self.accessibility != image.accessibility {
561            diff.insert(DiffModifies::ACCESSIBILITY);
562        }
563
564        if self.layout != image.layout {
565            diff.insert(DiffModifies::LAYOUT);
566        }
567
568        if self.frame_idx != image.frame_idx || !Rc::ptr_eq(&self.frames, &image.frames) {
569            diff.insert(DiffModifies::LAYOUT);
570            diff.insert(DiffModifies::STYLE);
571        }
572
573        diff
574    }
575
576    fn layout(&'_ self) -> Cow<'_, LayoutData> {
577        Cow::Borrowed(&self.layout)
578    }
579
580    fn effect(&'_ self) -> Option<Cow<'_, EffectData>> {
581        None
582    }
583
584    fn style(&'_ self) -> Cow<'_, StyleState> {
585        Cow::Owned(StyleState::default())
586    }
587
588    fn text_style(&'_ self) -> Cow<'_, TextStyleData> {
589        Cow::Owned(TextStyleData::default())
590    }
591
592    fn accessibility(&'_ self) -> Cow<'_, AccessibilityData> {
593        Cow::Borrowed(&self.accessibility)
594    }
595
596    fn should_measure_inner_children(&self) -> bool {
597        false
598    }
599
600    fn should_hook_measurement(&self) -> bool {
601        true
602    }
603
604    fn measure(&self, context: LayoutContext) -> Option<(Size2D, Rc<dyn Any>)> {
605        let frame = &self.frames.frames[self.frame_idx];
606        let image = &frame.image;
607
608        let image_width = image.width() as f32;
609        let image_height = image.height() as f32;
610
611        let width_ratio = context.area_size.width / image.width() as f32;
612        let height_ratio = context.area_size.height / image.height() as f32;
613
614        let size = match self.image_data.aspect_ratio {
615            AspectRatio::Max => {
616                let ratio = width_ratio.max(height_ratio);
617
618                Size2D::new(image_width * ratio, image_height * ratio)
619            }
620            AspectRatio::Min => {
621                let ratio = width_ratio.min(height_ratio);
622
623                Size2D::new(image_width * ratio, image_height * ratio)
624            }
625            AspectRatio::Fit => Size2D::new(image_width, image_height),
626            AspectRatio::None => *context.area_size,
627        };
628
629        Some((size, Rc::new(())))
630    }
631
632    fn clip(&self, context: ClipContext) {
633        let area = context.visible_area;
634        context.canvas.clip_rect(
635            SkRect::new(area.min_x(), area.min_y(), area.max_x(), area.max_y()),
636            ClipOp::Intersect,
637            true,
638        );
639    }
640
641    fn render(&self, context: RenderContext) {
642        let mut paint = Paint::default();
643        paint.set_anti_alias(true);
644
645        let sampling = match self.image_data.sampling_mode {
646            SamplingMode::Nearest => SamplingOptions::new(FilterMode::Nearest, MipmapMode::None),
647            SamplingMode::Bilinear => SamplingOptions::new(FilterMode::Linear, MipmapMode::None),
648            SamplingMode::Trilinear => SamplingOptions::new(FilterMode::Linear, MipmapMode::Linear),
649            SamplingMode::Mitchell => SamplingOptions::from(CubicResampler::mitchell()),
650            SamplingMode::CatmullRom => SamplingOptions::from(CubicResampler::catmull_rom()),
651        };
652
653        let area = context.layout_node.visible_area();
654
655        let rect = SkRect::new(area.min_x(), area.min_y(), area.max_x(), area.max_y());
656
657        let current_frame = &self.frames.frames[self.frame_idx];
658
659        // Simply render the pre-composed frame image directly
660        context.canvas.draw_image_rect_with_sampling_options(
661            &current_frame.image,
662            None,
663            rect,
664            sampling,
665            &paint,
666        );
667    }
668}
669
670struct CachedFrame {
671    image: SkImage,
672    dispose: DisposalMethod,
673    left: f32,
674    top: f32,
675    width: f32,
676    height: f32,
677    delay: Duration,
678}
679
680struct CachedGifFrames {
681    frames: Vec<CachedFrame>,
682}