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#[derive(PartialEq, Clone)]
95pub enum GifSource {
96 #[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#[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 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 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 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 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 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 surface.canvas().draw_image(
329 &frame_image,
330 (frame.left as f32, frame.top as f32),
331 None,
332 );
333
334 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 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 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 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 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 context.canvas.draw_image_rect_with_sampling_options(
660 ¤t_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}