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