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.clone(),
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.update_asset(
377                                        asset_config,
378                                        Asset::Cached(Rc::new(bytes.clone())),
379                                    );
380                                }
381                                Err(err) => {
382                                    asset_cacher
383                                        .update_asset(asset_config, Asset::Error(err.to_string()));
384                                }
385                            }
386                        });
387
388                        assets_tasks.write().push(asset_task);
389                    }
390                    _ => {}
391                }
392            }
393        });
394
395        use_side_effect(move || {
396            if let Some(Asset::Cached(asset)) = asset_cacher.subscribe_asset(&asset_config) {
397                if let Some(bytes) = asset.downcast_ref::<Bytes>().cloned() {
398                    let asset_task = spawn(async move {
399                        if let Err(err) = stream_gif(bytes).await {
400                            *status.write() = Status::Errored(err.to_string());
401                            #[cfg(debug_assertions)]
402                            tracing::error!(
403                                "Failed to render GIF by ID <{}>, error: {err:?}",
404                                asset_config.id
405                            );
406                        }
407                    });
408                    assets_tasks.write().push(asset_task);
409                } else {
410                    #[cfg(debug_assertions)]
411                    tracing::error!(
412                        "Failed to downcast asset of GIF by ID <{}>",
413                        asset_config.id
414                    )
415                }
416            }
417        });
418
419        match (asset_data, cached_frames.read().as_ref()) {
420            (Asset::Cached(_), Some(frames)) => match &*status.read() {
421                Status::Playing(frame_idx) => gif(frames.clone(), *frame_idx)
422                    .accessibility(self.accessibility.clone())
423                    .a11y_role(AccessibilityRole::Image)
424                    .a11y_focusable(true)
425                    .layout(self.layout.clone())
426                    .image_data(self.image_data.clone())
427                    .into_element(),
428                Status::Decoding => rect()
429                    .layout(self.layout.clone())
430                    .center()
431                    .child(CircularLoader::new())
432                    .into_element(),
433                Status::Errored(err) => err.clone().into_element(),
434            },
435            (Asset::Cached(_), _) | (Asset::Pending | Asset::Loading, _) => rect()
436                .layout(self.layout.clone())
437                .center()
438                .child(CircularLoader::new())
439                .into(),
440            (Asset::Error(err), _) => err.into(),
441        }
442    }
443
444    fn render_key(&self) -> DiffKey {
445        self.key.clone().or(self.default_key())
446    }
447}
448
449pub struct Gif {
450    key: DiffKey,
451    element: GifElement,
452}
453
454impl Gif {
455    pub fn try_downcast(element: &dyn ElementExt) -> Option<GifElement> {
456        (element as &dyn Any).downcast_ref::<GifElement>().cloned()
457    }
458}
459
460impl From<Gif> for Element {
461    fn from(value: Gif) -> Self {
462        Element::Element {
463            key: value.key,
464            element: Rc::new(value.element),
465            elements: vec![],
466        }
467    }
468}
469
470fn gif(frames: Rc<CachedGifFrames>, frame_idx: usize) -> Gif {
471    Gif {
472        key: DiffKey::None,
473        element: GifElement {
474            frames,
475            frame_idx,
476            accessibility: AccessibilityData::default(),
477            layout: LayoutData::default(),
478            event_handlers: HashMap::default(),
479            image_data: ImageData::default(),
480        },
481    }
482}
483
484impl LayoutExt for Gif {
485    fn get_layout(&mut self) -> &mut LayoutData {
486        &mut self.element.layout
487    }
488}
489
490impl ContainerExt for Gif {}
491
492impl ImageExt for Gif {
493    fn get_image_data(&mut self) -> &mut ImageData {
494        &mut self.element.image_data
495    }
496}
497
498impl KeyExt for Gif {
499    fn write_key(&mut self) -> &mut DiffKey {
500        &mut self.key
501    }
502}
503
504impl EventHandlersExt for Gif {
505    fn get_event_handlers(&mut self) -> &mut FxHashMap<EventName, EventHandlerType> {
506        &mut self.element.event_handlers
507    }
508}
509
510impl AccessibilityExt for Gif {
511    fn get_accessibility_data(&mut self) -> &mut AccessibilityData {
512        &mut self.element.accessibility
513    }
514}
515impl MaybeExt for Gif {}
516
517#[derive(Clone)]
518pub struct GifElement {
519    accessibility: AccessibilityData,
520    layout: LayoutData,
521    event_handlers: FxHashMap<EventName, EventHandlerType>,
522    frames: Rc<CachedGifFrames>,
523    frame_idx: usize,
524    image_data: ImageData,
525}
526
527impl PartialEq for GifElement {
528    fn eq(&self, other: &Self) -> bool {
529        self.accessibility == other.accessibility
530            && self.layout == other.layout
531            && self.image_data == other.image_data
532            && Rc::ptr_eq(&self.frames, &other.frames)
533            && self.frame_idx == other.frame_idx
534    }
535}
536
537impl ElementExt for GifElement {
538    fn changed(&self, other: &Rc<dyn ElementExt>) -> bool {
539        let Some(image) = (other.as_ref() as &dyn Any).downcast_ref::<GifElement>() else {
540            return false;
541        };
542        self != image
543    }
544
545    fn diff(&self, other: &Rc<dyn ElementExt>) -> DiffModifies {
546        let Some(image) = (other.as_ref() as &dyn Any).downcast_ref::<GifElement>() else {
547            return DiffModifies::all();
548        };
549
550        let mut diff = DiffModifies::empty();
551
552        if self.accessibility != image.accessibility {
553            diff.insert(DiffModifies::ACCESSIBILITY);
554        }
555
556        if self.layout != image.layout {
557            diff.insert(DiffModifies::LAYOUT);
558        }
559
560        if self.frame_idx != image.frame_idx || !Rc::ptr_eq(&self.frames, &image.frames) {
561            diff.insert(DiffModifies::LAYOUT);
562            diff.insert(DiffModifies::STYLE);
563        }
564
565        diff
566    }
567
568    fn layout(&'_ self) -> Cow<'_, LayoutData> {
569        Cow::Borrowed(&self.layout)
570    }
571
572    fn effect(&'_ self) -> Option<Cow<'_, EffectData>> {
573        None
574    }
575
576    fn style(&'_ self) -> Cow<'_, StyleState> {
577        Cow::Owned(StyleState::default())
578    }
579
580    fn text_style(&'_ self) -> Cow<'_, TextStyleData> {
581        Cow::Owned(TextStyleData::default())
582    }
583
584    fn accessibility(&'_ self) -> Cow<'_, AccessibilityData> {
585        Cow::Borrowed(&self.accessibility)
586    }
587
588    fn should_measure_inner_children(&self) -> bool {
589        false
590    }
591
592    fn should_hook_measurement(&self) -> bool {
593        true
594    }
595
596    fn measure(&self, context: LayoutContext) -> Option<(Size2D, Rc<dyn Any>)> {
597        let frame = &self.frames.frames[self.frame_idx];
598        let image = &frame.image;
599
600        let image_width = image.width() as f32;
601        let image_height = image.height() as f32;
602
603        let width_ratio = context.area_size.width / image.width() as f32;
604        let height_ratio = context.area_size.height / image.height() as f32;
605
606        let size = match self.image_data.aspect_ratio {
607            AspectRatio::Max => {
608                let ratio = width_ratio.max(height_ratio);
609
610                Size2D::new(image_width * ratio, image_height * ratio)
611            }
612            AspectRatio::Min => {
613                let ratio = width_ratio.min(height_ratio);
614
615                Size2D::new(image_width * ratio, image_height * ratio)
616            }
617            AspectRatio::Fit => Size2D::new(image_width, image_height),
618            AspectRatio::None => *context.area_size,
619        };
620
621        Some((size, Rc::new(())))
622    }
623
624    fn clip(&self, context: ClipContext) {
625        let area = context.visible_area;
626        context.canvas.clip_rect(
627            SkRect::new(area.min_x(), area.min_y(), area.max_x(), area.max_y()),
628            ClipOp::Intersect,
629            true,
630        );
631    }
632
633    fn render(&self, context: RenderContext) {
634        let mut paint = Paint::default();
635        paint.set_anti_alias(true);
636
637        let sampling = match self.image_data.sampling_mode {
638            SamplingMode::Nearest => SamplingOptions::new(FilterMode::Nearest, MipmapMode::None),
639            SamplingMode::Bilinear => SamplingOptions::new(FilterMode::Linear, MipmapMode::None),
640            SamplingMode::Trilinear => SamplingOptions::new(FilterMode::Linear, MipmapMode::Linear),
641            SamplingMode::Mitchell => SamplingOptions::from(CubicResampler::mitchell()),
642            SamplingMode::CatmullRom => SamplingOptions::from(CubicResampler::catmull_rom()),
643        };
644
645        let area = context.layout_node.visible_area();
646
647        let rect = SkRect::new(area.min_x(), area.min_y(), area.max_x(), area.max_y());
648
649        let current_frame = &self.frames.frames[self.frame_idx];
650
651        // Simply render the pre-composed frame image directly
652        context.canvas.draw_image_rect_with_sampling_options(
653            &current_frame.image,
654            None,
655            rect,
656            sampling,
657            &paint,
658        );
659    }
660}
661
662struct CachedFrame {
663    image: SkImage,
664    dispose: DisposalMethod,
665    left: f32,
666    top: f32,
667    width: f32,
668    height: f32,
669    delay: Duration,
670}
671
672struct CachedGifFrames {
673    frames: Vec<CachedFrame>,
674}