freya_components/
gif_viewer.rs

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