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