Skip to main content

freya_components/
image_viewer.rs

1use std::{
2    collections::hash_map::DefaultHasher,
3    fs,
4    hash::{
5        Hash,
6        Hasher,
7    },
8    path::PathBuf,
9    rc::Rc,
10};
11
12use anyhow::Context;
13use bytes::Bytes;
14use freya_core::{
15    elements::image::*,
16    prelude::*,
17};
18use freya_engine::prelude::{
19    Paint,
20    SkData,
21    SkImage,
22    SkRect,
23    raster_n32_premul,
24};
25use torin::prelude::{
26    Size,
27    Size2D,
28};
29#[cfg(feature = "remote-asset")]
30use ureq::http::Uri;
31
32use crate::{
33    cache::*,
34    loader::CircularLoader,
35};
36
37/// Supported image sources for [`ImageViewer`].
38///
39/// ### URI
40///
41/// Good to load remote images.
42///
43/// > Requires the `remote-asset` feature to be enabled.
44///
45/// ```rust
46/// # use freya::prelude::*;
47/// let source: ImageSource =
48///     "https://upload.wikimedia.org/wikipedia/commons/8/8a/Gecarcinus_quadratus_%28Nosara%29.jpg"
49///         .into();
50/// ```
51///
52/// ### Path
53///
54/// Good for dynamic loading.
55///
56/// ```rust
57/// # use freya::prelude::*;
58/// # use std::path::PathBuf;
59/// let source: ImageSource = PathBuf::from("./examples/rust_logo.png").into();
60/// ```
61/// ### Raw bytes
62///
63/// Good for embedded images.
64///
65/// ```rust
66/// # use freya::prelude::*;
67/// let source: ImageSource = (
68///     "rust-logo",
69///     include_bytes!("../../../examples/rust_logo.png"),
70/// )
71///     .into();
72/// ```
73///
74/// ### Dynamic bytes
75///
76/// Good for rendering custom allocated images.
77///
78/// ```rust
79/// # use freya::prelude::*;
80/// # use bytes::Bytes;
81/// fn app() -> impl IntoElement {
82///     let image_data = use_state(|| (0, Bytes::from(vec![/* ... */])));
83///     let source: ImageSource = image_data.read().clone().into();
84///     ImageViewer::new(source)
85/// }
86/// ```
87#[derive(PartialEq, Clone)]
88pub enum ImageSource {
89    /// Remote image loaded from a URI.
90    ///
91    /// Requires the `remote-asset` feature.
92    #[cfg(feature = "remote-asset")]
93    Uri(Uri),
94
95    Path(PathBuf),
96
97    Bytes(u64, Bytes),
98}
99
100impl<H: Hash> From<(H, Bytes)> for ImageSource {
101    fn from((id, bytes): (H, Bytes)) -> Self {
102        let mut hasher = DefaultHasher::default();
103        id.hash(&mut hasher);
104        Self::Bytes(hasher.finish(), bytes)
105    }
106}
107
108impl<H: Hash> From<(H, &'static [u8])> for ImageSource {
109    fn from((id, bytes): (H, &'static [u8])) -> Self {
110        (id, Bytes::from_static(bytes)).into()
111    }
112}
113
114impl<const N: usize, H: Hash> From<(H, &'static [u8; N])> for ImageSource {
115    fn from((id, bytes): (H, &'static [u8; N])) -> Self {
116        (id, Bytes::from_static(bytes)).into()
117    }
118}
119
120#[cfg_attr(feature = "docs", doc(cfg(feature = "remote-asset")))]
121#[cfg(feature = "remote-asset")]
122impl From<Uri> for ImageSource {
123    fn from(uri: Uri) -> Self {
124        Self::Uri(uri)
125    }
126}
127
128#[cfg_attr(feature = "docs", doc(cfg(feature = "remote-asset")))]
129#[cfg(feature = "remote-asset")]
130impl From<&'static str> for ImageSource {
131    fn from(src: &'static str) -> Self {
132        Self::Uri(Uri::from_static(src))
133    }
134}
135
136impl From<PathBuf> for ImageSource {
137    fn from(path: PathBuf) -> Self {
138        Self::Path(path)
139    }
140}
141
142impl Hash for ImageSource {
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
153pub type DecodeSize = euclid::Size2D<u32, ()>;
154
155impl ImageSource {
156    /// Fetch the source's encoded bytes and decode them into a Skia image.
157    pub async fn load(
158        &self,
159        decode_size: Option<DecodeSize>,
160        sampling_mode: SamplingMode,
161    ) -> anyhow::Result<(SkImage, Bytes)> {
162        let source = self.clone();
163        blocking::unblock(move || {
164            let bytes = match source {
165                #[cfg(feature = "remote-asset")]
166                Self::Uri(uri) => ureq::get(uri)
167                    .call()?
168                    .body_mut()
169                    .read_to_vec()
170                    .map(Bytes::from)?,
171                Self::Path(path) => fs::read(path).map(Bytes::from)?,
172                Self::Bytes(_, bytes) => bytes,
173            };
174            let encoded = SkImage::from_encoded(unsafe { SkData::new_bytes(&bytes) })
175                .context("Failed to decode Image.")?;
176            let image = match decode_size
177                .and_then(|target| Self::downsample(&encoded, target, &sampling_mode))
178            {
179                Some(scaled) => scaled,
180                None => encoded.make_raster_image(None, None).unwrap_or(encoded),
181            };
182            Ok((image, bytes))
183        })
184        .await
185    }
186
187    fn downsample(
188        encoded: &SkImage,
189        target: DecodeSize,
190        sampling_mode: &SamplingMode,
191    ) -> Option<SkImage> {
192        let natural_width = encoded.width() as f32;
193        let natural_height = encoded.height() as f32;
194        let target_width = target.width as f32;
195        let target_height = target.height as f32;
196        if natural_width <= target_width && natural_height <= target_height {
197            return None;
198        }
199        let ratio = (target_width / natural_width).min(target_height / natural_height);
200        let width = (natural_width * ratio).round().max(1.);
201        let height = (natural_height * ratio).round().max(1.);
202
203        let mut surface = raster_n32_premul((width as i32, height as i32))?;
204        let destination = SkRect::from_xywh(0., 0., width, height);
205        let sampling = sampling_mode.sampling_options();
206        let mut paint = Paint::default();
207        paint.set_anti_alias(true);
208        surface.canvas().draw_image_rect_with_sampling_options(
209            encoded,
210            None,
211            destination,
212            sampling,
213            &paint,
214        );
215        Some(surface.image_snapshot())
216    }
217}
218
219/// How an [`ImageViewer`] picks its decode dimensions.
220#[derive(Default, Clone, Debug, PartialEq, Copy)]
221pub enum DecodeMode {
222    /// Default. Decodes to the pixel-sized layout scaled by the window scale factor,
223    /// falling back to the natural size for any other sizing (fill, percentages, auto).
224    #[default]
225    FromLayout,
226    /// Decode at the image's natural size.
227    Source,
228    /// Decode to fit within the given size, preserving aspect ratio and never upscaling.
229    Custom(Size2D),
230}
231
232impl DecodeMode {
233    fn resolve(&self, layout: &LayoutData, scale_factor: f64) -> Option<DecodeSize> {
234        let scale = scale_factor as f32;
235        let size = match self {
236            Self::Source => return None,
237            Self::FromLayout => match (&layout.width, &layout.height) {
238                (Size::Pixels(width), Size::Pixels(height)) => {
239                    Size2D::new(width.get() * scale, height.get() * scale)
240                }
241                _ => return None,
242            },
243            Self::Custom(size) => *size,
244        };
245        Some(DecodeSize::new(
246            size.width.round().max(1.) as u32,
247            size.height.round().max(1.) as u32,
248        ))
249    }
250}
251
252/// Image viewer component.
253///
254/// Handles async loading, caching, and error states for images.
255/// See [`ImageSource`] for all supported image sources.
256///
257/// # Example
258///
259/// ```rust
260/// # use freya::prelude::*;
261/// fn app() -> impl IntoElement {
262///     let source: ImageSource = (
263///         "rust-logo",
264///         include_bytes!("../../../examples/rust_logo.png"),
265///     )
266///         .into();
267///
268///     ImageViewer::new(source)
269/// }
270/// # use freya::prelude::*;
271/// # use freya_testing::prelude::*;
272/// # use std::path::PathBuf;
273/// # launch_doc(|| {
274/// #   rect().center().expanded().child(ImageViewer::new(("rust-logo", include_bytes!("../../../examples/rust_logo.png"))))
275/// # }, "./images/gallery_image_viewer.png").with_hook(|t| { t.poll(std::time::Duration::from_millis(1), std::time::Duration::from_millis(300)); t.sync_and_update(); }).with_scale_factor(1.).render();
276/// ```
277///
278/// # Preview
279/// ![ImageViewer Preview][image_viewer]
280#[cfg_attr(feature = "docs",
281    doc = embed_doc_image::embed_image!("image_viewer", "images/gallery_image_viewer.png")
282)]
283#[derive(PartialEq)]
284pub struct ImageViewer {
285    source: ImageSource,
286    asset_age: AssetAge,
287
288    layout: LayoutData,
289    image_data: ImageData,
290    accessibility: AccessibilityData,
291    effect: EffectData,
292    corner_radius: Option<CornerRadius>,
293    decode_mode: DecodeMode,
294
295    children: Vec<Element>,
296    loading_placeholder: Option<Element>,
297    error_renderer: Option<Callback<String, Element>>,
298
299    key: DiffKey,
300}
301
302impl ImageViewer {
303    pub fn new(source: impl Into<ImageSource>) -> Self {
304        ImageViewer {
305            source: source.into(),
306            asset_age: AssetAge::default(),
307            layout: LayoutData::default(),
308            image_data: ImageData::default(),
309            accessibility: AccessibilityData::default(),
310            effect: EffectData::default(),
311            corner_radius: None,
312            decode_mode: DecodeMode::default(),
313            children: Vec::new(),
314            loading_placeholder: None,
315            error_renderer: None,
316            key: DiffKey::None,
317        }
318    }
319}
320
321impl KeyExt for ImageViewer {
322    fn write_key(&mut self) -> &mut DiffKey {
323        &mut self.key
324    }
325}
326
327impl LayoutExt for ImageViewer {
328    fn get_layout(&mut self) -> &mut LayoutData {
329        &mut self.layout
330    }
331}
332
333impl ContainerSizeExt for ImageViewer {}
334impl ContainerWithContentExt for ImageViewer {}
335
336impl ImageExt for ImageViewer {
337    fn get_image_data(&mut self) -> &mut ImageData {
338        &mut self.image_data
339    }
340}
341
342impl AccessibilityExt for ImageViewer {
343    fn get_accessibility_data(&mut self) -> &mut AccessibilityData {
344        &mut self.accessibility
345    }
346}
347
348impl ChildrenExt for ImageViewer {
349    fn get_children(&mut self) -> &mut Vec<Element> {
350        &mut self.children
351    }
352}
353
354impl EffectExt for ImageViewer {
355    fn get_effect(&mut self) -> &mut EffectData {
356        &mut self.effect
357    }
358}
359
360impl ImageViewer {
361    pub fn corner_radius(mut self, corner_radius: impl Into<CornerRadius>) -> Self {
362        self.corner_radius = Some(corner_radius.into());
363        self
364    }
365
366    /// Custom element rendered while loading.
367    pub fn loading_placeholder(mut self, placeholder: impl Into<Element>) -> Self {
368        self.loading_placeholder = Some(placeholder.into());
369        self
370    }
371
372    /// Pick how the image is decoded. See [`DecodeMode`].
373    pub fn decode_mode(mut self, decode_mode: DecodeMode) -> Self {
374        self.decode_mode = decode_mode;
375        self
376    }
377
378    /// Customize how long the image will remain cached after no longer being used.
379    ///
380    /// Defaults to [`AssetAge::default`] (1h).
381    pub fn asset_age(mut self, asset_age: impl Into<AssetAge>) -> Self {
382        self.asset_age = asset_age.into();
383        self
384    }
385
386    /// Custom element rendered when the image fails to load.
387    pub fn error_renderer(mut self, renderer: impl Into<Callback<String, Element>>) -> Self {
388        self.error_renderer = Some(renderer.into());
389        self
390    }
391}
392
393impl Component for ImageViewer {
394    fn render(&self) -> impl IntoElement {
395        let target = self
396            .decode_mode
397            .resolve(&self.layout, *Platform::get().scale_factor.read());
398        let sampling_mode = self.image_data.sampling_mode.clone();
399        let asset_config =
400            AssetConfiguration::new((&self.source, target, &sampling_mode), self.asset_age);
401        let asset = use_asset(&asset_config);
402        let mut asset_cacher = use_hook(AssetCacher::get);
403
404        use_side_effect_with_deps(
405            &(self.source.clone(), asset_config, target, sampling_mode),
406            move |(source, asset_config, target, sampling_mode)| {
407                if matches!(
408                    asset_cacher.read_asset(asset_config),
409                    Some(Asset::Pending) | Some(Asset::Error(_))
410                ) {
411                    asset_cacher.update_asset(asset_config.clone(), Asset::Loading);
412
413                    let source = source.clone();
414                    let asset_config = asset_config.clone();
415                    let target = *target;
416                    let sampling_mode = sampling_mode.clone();
417                    spawn_forever(async move {
418                        match source.load(target, sampling_mode).await {
419                            Ok((image, bytes)) => {
420                                asset_cacher.update_asset(
421                                    asset_config,
422                                    Asset::Cached(Rc::new(ImageHandle::new(image, bytes))),
423                                );
424                            }
425                            Err(err) => {
426                                asset_cacher
427                                    .update_asset(asset_config, Asset::Error(err.to_string()));
428                            }
429                        }
430                    });
431                }
432            },
433        );
434
435        match asset {
436            Asset::Cached(asset) => {
437                let asset = asset.downcast_ref::<ImageHandle>().unwrap().clone();
438                image(asset)
439                    .accessibility(self.accessibility.clone())
440                    .a11y_role(AccessibilityRole::Image)
441                    .layout(self.layout.clone())
442                    .image_data(self.image_data.clone())
443                    .effect(self.effect.clone())
444                    .children(self.children.clone())
445                    .map(self.corner_radius, |img, corner_radius| {
446                        img.corner_radius(corner_radius)
447                    })
448                    .into_element()
449            }
450            Asset::Pending | Asset::Loading => rect()
451                .layout(self.layout.clone())
452                .center()
453                .child(
454                    self.loading_placeholder
455                        .clone()
456                        .unwrap_or_else(|| CircularLoader::new().into_element()),
457                )
458                .into(),
459            Asset::Error(err) => match &self.error_renderer {
460                Some(renderer) => renderer.call(err),
461                None => err.into(),
462            },
463        }
464    }
465
466    fn render_key(&self) -> DiffKey {
467        self.key.clone().or(self.default_key())
468    }
469}