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#[derive(PartialEq, Clone)]
92pub enum GifSource {
93 #[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#[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 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 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 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 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 surface.canvas().draw_image(
310 &frame_image,
311 (frame.left as f32, frame.top as f32),
312 None,
313 );
314
315 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 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 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 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 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 context.canvas.draw_image_rect_with_sampling_options(
647 ¤t_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}