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(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 {}
254
255impl ImageExt for GifViewer {
256    fn get_image_data(&mut self) -> &mut ImageData {
257        &mut self.image_data
258    }
259}
260
261impl AccessibilityExt for GifViewer {
262    fn get_accessibility_data(&mut self) -> &mut AccessibilityData {
263        &mut self.accessibility
264    }
265}
266
267enum Status {
268    Playing(usize),
269    Decoding,
270    Errored(String),
271}
272
273impl Component for GifViewer {
274    fn render(&self) -> impl IntoElement {
275        let asset_config = AssetConfiguration::new(&self.source, self.asset_age.clone());
276        let asset_data = use_asset(&asset_config);
277        let mut status = use_state(|| Status::Decoding);
278        let mut cached_frames = use_state::<Option<Rc<CachedGifFrames>>>(|| None);
279        let mut asset_cacher = use_hook(AssetCacher::get);
280        let mut assets_tasks = use_state::<Vec<TaskHandle>>(Vec::new);
281
282        let mut stream_gif = async move |bytes: Bytes| -> anyhow::Result<()> {
283            // Decode and pre-composite all frames upfront
284            let frames_data = unblock(move || -> anyhow::Result<Vec<CachedFrame>> {
285                let mut decoder_options = gif::DecodeOptions::new();
286                decoder_options.set_color_output(gif::ColorOutput::RGBA);
287                let cursor = std::io::Cursor::new(&bytes);
288                let mut decoder = decoder_options.read_info(cursor)?;
289                let width = decoder.width() as i32;
290                let height = decoder.height() as i32;
291
292                // Create a surface for compositing frames
293                let mut surface =
294                    raster_n32_premul((width, height)).context("Failed to create GIF surface")?;
295
296                let mut frames: Vec<CachedFrame> = Vec::new();
297
298                while let Ok(Some(frame)) = decoder.read_next_frame() {
299                    // Handle disposal of previous frame
300                    if let Some(prev_frame) = frames.last()
301                        && prev_frame.dispose == DisposalMethod::Background
302                    {
303                        let canvas = surface.canvas();
304                        let clear_rect = Rect::from_xywh(
305                            prev_frame.left,
306                            prev_frame.top,
307                            prev_frame.width,
308                            prev_frame.height,
309                        );
310                        canvas.save();
311                        canvas.clip_rect(clear_rect, None, false);
312                        canvas.clear(Color::TRANSPARENT);
313                        canvas.restore();
314                    }
315
316                    // Decode frame image
317                    let row_bytes = (frame.width * 4) as usize;
318                    let data = unsafe { Data::new_bytes(&frame.buffer) };
319                    let isize = ISize::new(frame.width as i32, frame.height as i32);
320                    let frame_image = raster_from_data(
321                        &ImageInfo::new(isize, ColorType::RGBA8888, AlphaType::Unpremul, None),
322                        data,
323                        row_bytes,
324                    )
325                    .context("Failed to create GIF Frame.")?;
326
327                    // Composite frame onto surface
328                    surface.canvas().draw_image(
329                        &frame_image,
330                        (frame.left as f32, frame.top as f32),
331                        None,
332                    );
333
334                    // Take a snapshot of the fully composed frame
335                    let composed_image = surface.image_snapshot();
336
337                    frames.push(CachedFrame {
338                        image: composed_image,
339                        dispose: frame.dispose,
340                        left: frame.left as f32,
341                        top: frame.top as f32,
342                        width: frame.width as f32,
343                        height: frame.height as f32,
344                        delay: Duration::from_millis(frame.delay as u64 * 10),
345                    });
346                }
347
348                Ok(frames)
349            })
350            .await?;
351
352            let frames = Rc::new(CachedGifFrames {
353                frames: frames_data,
354            });
355            *cached_frames.write() = Some(frames.clone());
356
357            // Now loop through cached frames
358            loop {
359                for (i, frame) in frames.frames.iter().enumerate() {
360                    *status.write() = Status::Playing(i);
361                    Timer::after(frame.delay).await;
362                }
363            }
364        };
365
366        use_side_effect_with_deps(&self.source, {
367            let asset_config = asset_config.clone();
368            move |source| {
369                let source = source.clone();
370
371                // Cancel previous tasks
372                for asset_task in assets_tasks.write().drain(..) {
373                    asset_task.cancel();
374                }
375
376                match asset_cacher.read_asset(&asset_config) {
377                    Some(Asset::Pending) | Some(Asset::Error(_)) => {
378                        // Mark asset as loading
379                        asset_cacher.update_asset(asset_config.clone(), Asset::Loading);
380
381                        let asset_config = asset_config.clone();
382                        let asset_task = spawn(async move {
383                            match source.bytes().await {
384                                Ok(bytes) => {
385                                    // Cache the GIF bytes
386                                    asset_cacher
387                                        .update_asset(asset_config, Asset::Cached(Rc::new(bytes)));
388                                }
389                                Err(err) => {
390                                    asset_cacher
391                                        .update_asset(asset_config, Asset::Error(err.to_string()));
392                                }
393                            }
394                        });
395
396                        assets_tasks.write().push(asset_task);
397                    }
398                    _ => {}
399                }
400            }
401        });
402
403        use_side_effect(move || {
404            if let Some(Asset::Cached(asset)) = asset_cacher.subscribe_asset(&asset_config) {
405                if let Some(bytes) = asset.downcast_ref::<Bytes>().cloned() {
406                    let asset_task = spawn(async move {
407                        if let Err(err) = stream_gif(bytes).await {
408                            *status.write() = Status::Errored(err.to_string());
409                            #[cfg(debug_assertions)]
410                            tracing::error!(
411                                "Failed to render GIF by ID <{}>, error: {err:?}",
412                                asset_config.id
413                            );
414                        }
415                    });
416                    assets_tasks.write().push(asset_task);
417                } else {
418                    #[cfg(debug_assertions)]
419                    tracing::error!(
420                        "Failed to downcast asset of GIF by ID <{}>",
421                        asset_config.id
422                    )
423                }
424            }
425        });
426
427        match (asset_data, cached_frames.read().as_ref()) {
428            (Asset::Cached(_), Some(frames)) => match &*status.read() {
429                Status::Playing(frame_idx) => gif(frames.clone(), *frame_idx)
430                    .accessibility(self.accessibility.clone())
431                    .a11y_role(AccessibilityRole::Image)
432                    .layout(self.layout.clone())
433                    .image_data(self.image_data.clone())
434                    .into_element(),
435                Status::Decoding => rect()
436                    .layout(self.layout.clone())
437                    .center()
438                    .child(CircularLoader::new())
439                    .into_element(),
440                Status::Errored(err) => err.clone().into_element(),
441            },
442            (Asset::Cached(_), _) | (Asset::Pending | Asset::Loading, _) => rect()
443                .layout(self.layout.clone())
444                .center()
445                .child(CircularLoader::new())
446                .into(),
447            (Asset::Error(err), _) => err.into(),
448        }
449    }
450
451    fn render_key(&self) -> DiffKey {
452        self.key.clone().or(self.default_key())
453    }
454}
455
456pub struct Gif {
457    key: DiffKey,
458    element: GifElement,
459}
460
461impl Gif {
462    pub fn try_downcast(element: &dyn ElementExt) -> Option<GifElement> {
463        (element as &dyn Any).downcast_ref::<GifElement>().cloned()
464    }
465}
466
467impl From<Gif> for Element {
468    fn from(value: Gif) -> Self {
469        Element::Element {
470            key: value.key,
471            element: Rc::new(value.element),
472            elements: vec![],
473        }
474    }
475}
476
477fn gif(frames: Rc<CachedGifFrames>, frame_idx: usize) -> Gif {
478    Gif {
479        key: DiffKey::None,
480        element: GifElement {
481            frames,
482            frame_idx,
483            accessibility: AccessibilityData::default(),
484            layout: LayoutData::default(),
485            event_handlers: HashMap::default(),
486            image_data: ImageData::default(),
487        },
488    }
489}
490
491impl LayoutExt for Gif {
492    fn get_layout(&mut self) -> &mut LayoutData {
493        &mut self.element.layout
494    }
495}
496
497impl ContainerExt for Gif {}
498
499impl ImageExt for Gif {
500    fn get_image_data(&mut self) -> &mut ImageData {
501        &mut self.element.image_data
502    }
503}
504
505impl KeyExt for Gif {
506    fn write_key(&mut self) -> &mut DiffKey {
507        &mut self.key
508    }
509}
510
511impl EventHandlersExt for Gif {
512    fn get_event_handlers(&mut self) -> &mut FxHashMap<EventName, EventHandlerType> {
513        &mut self.element.event_handlers
514    }
515}
516
517impl AccessibilityExt for Gif {
518    fn get_accessibility_data(&mut self) -> &mut AccessibilityData {
519        &mut self.element.accessibility
520    }
521}
522impl MaybeExt for Gif {}
523
524#[derive(Clone)]
525pub struct GifElement {
526    accessibility: AccessibilityData,
527    layout: LayoutData,
528    event_handlers: FxHashMap<EventName, EventHandlerType>,
529    frames: Rc<CachedGifFrames>,
530    frame_idx: usize,
531    image_data: ImageData,
532}
533
534impl PartialEq for GifElement {
535    fn eq(&self, other: &Self) -> bool {
536        self.accessibility == other.accessibility
537            && self.layout == other.layout
538            && self.image_data == other.image_data
539            && Rc::ptr_eq(&self.frames, &other.frames)
540            && self.frame_idx == other.frame_idx
541    }
542}
543
544impl ElementExt for GifElement {
545    fn changed(&self, other: &Rc<dyn ElementExt>) -> bool {
546        let Some(image) = (other.as_ref() as &dyn Any).downcast_ref::<GifElement>() else {
547            return false;
548        };
549        self != image
550    }
551
552    fn diff(&self, other: &Rc<dyn ElementExt>) -> DiffModifies {
553        let Some(image) = (other.as_ref() as &dyn Any).downcast_ref::<GifElement>() else {
554            return DiffModifies::all();
555        };
556
557        let mut diff = DiffModifies::empty();
558
559        if self.accessibility != image.accessibility {
560            diff.insert(DiffModifies::ACCESSIBILITY);
561        }
562
563        if self.layout != image.layout {
564            diff.insert(DiffModifies::LAYOUT);
565        }
566
567        if self.frame_idx != image.frame_idx || !Rc::ptr_eq(&self.frames, &image.frames) {
568            diff.insert(DiffModifies::LAYOUT);
569            diff.insert(DiffModifies::STYLE);
570        }
571
572        diff
573    }
574
575    fn layout(&'_ self) -> Cow<'_, LayoutData> {
576        Cow::Borrowed(&self.layout)
577    }
578
579    fn effect(&'_ self) -> Option<Cow<'_, EffectData>> {
580        None
581    }
582
583    fn style(&'_ self) -> Cow<'_, StyleState> {
584        Cow::Owned(StyleState::default())
585    }
586
587    fn text_style(&'_ self) -> Cow<'_, TextStyleData> {
588        Cow::Owned(TextStyleData::default())
589    }
590
591    fn accessibility(&'_ self) -> Cow<'_, AccessibilityData> {
592        Cow::Borrowed(&self.accessibility)
593    }
594
595    fn should_measure_inner_children(&self) -> bool {
596        false
597    }
598
599    fn should_hook_measurement(&self) -> bool {
600        true
601    }
602
603    fn measure(&self, context: LayoutContext) -> Option<(Size2D, Rc<dyn Any>)> {
604        let frame = &self.frames.frames[self.frame_idx];
605        let image = &frame.image;
606
607        let image_width = image.width() as f32;
608        let image_height = image.height() as f32;
609
610        let width_ratio = context.area_size.width / image.width() as f32;
611        let height_ratio = context.area_size.height / image.height() as f32;
612
613        let size = match self.image_data.aspect_ratio {
614            AspectRatio::Max => {
615                let ratio = width_ratio.max(height_ratio);
616
617                Size2D::new(image_width * ratio, image_height * ratio)
618            }
619            AspectRatio::Min => {
620                let ratio = width_ratio.min(height_ratio);
621
622                Size2D::new(image_width * ratio, image_height * ratio)
623            }
624            AspectRatio::Fit => Size2D::new(image_width, image_height),
625            AspectRatio::None => *context.area_size,
626        };
627
628        Some((size, Rc::new(())))
629    }
630
631    fn clip(&self, context: ClipContext) {
632        let area = context.visible_area;
633        context.canvas.clip_rect(
634            SkRect::new(area.min_x(), area.min_y(), area.max_x(), area.max_y()),
635            ClipOp::Intersect,
636            true,
637        );
638    }
639
640    fn render(&self, context: RenderContext) {
641        let mut paint = Paint::default();
642        paint.set_anti_alias(true);
643
644        let sampling = match self.image_data.sampling_mode {
645            SamplingMode::Nearest => SamplingOptions::new(FilterMode::Nearest, MipmapMode::None),
646            SamplingMode::Bilinear => SamplingOptions::new(FilterMode::Linear, MipmapMode::None),
647            SamplingMode::Trilinear => SamplingOptions::new(FilterMode::Linear, MipmapMode::Linear),
648            SamplingMode::Mitchell => SamplingOptions::from(CubicResampler::mitchell()),
649            SamplingMode::CatmullRom => SamplingOptions::from(CubicResampler::catmull_rom()),
650        };
651
652        let area = context.layout_node.visible_area();
653
654        let rect = SkRect::new(area.min_x(), area.min_y(), area.max_x(), area.max_y());
655
656        let current_frame = &self.frames.frames[self.frame_idx];
657
658        // Simply render the pre-composed frame image directly
659        context.canvas.draw_image_rect_with_sampling_options(
660            &current_frame.image,
661            None,
662            rect,
663            sampling,
664            &paint,
665        );
666    }
667}
668
669struct CachedFrame {
670    image: SkImage,
671    dispose: DisposalMethod,
672    left: f32,
673    top: f32,
674    width: f32,
675    height: f32,
676    delay: Duration,
677}
678
679struct CachedGifFrames {
680    frames: Vec<CachedFrame>,
681}