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