freya_components/
gif_viewer.rs

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