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#[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#[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 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 break;
301 }
302 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 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 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 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}