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