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 url::Url;
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(Url),
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
122impl From<Bytes> for ImageSource {
123    fn from(bytes: Bytes) -> Self {
124        let mut hasher = DefaultHasher::default();
125        bytes.hash(&mut hasher);
126        Self::Bytes(hasher.finish(), bytes)
127    }
128}
129
130impl From<&'static [u8]> for ImageSource {
131    fn from(bytes: &'static [u8]) -> Self {
132        Bytes::from_static(bytes).into()
133    }
134}
135
136impl<const N: usize> From<&'static [u8; N]> for ImageSource {
137    fn from(bytes: &'static [u8; N]) -> Self {
138        Bytes::from_static(bytes).into()
139    }
140}
141
142#[cfg_attr(feature = "docs", doc(cfg(feature = "remote-asset")))]
143#[cfg(feature = "remote-asset")]
144impl From<Url> for ImageSource {
145    fn from(uri: Url) -> Self {
146        Self::Uri(uri)
147    }
148}
149
150#[cfg_attr(feature = "docs", doc(cfg(feature = "remote-asset")))]
151#[cfg(feature = "remote-asset")]
152impl From<&'static str> for ImageSource {
153    fn from(src: &'static str) -> Self {
154        Self::Uri(Url::parse(src).expect("Invalid URL"))
155    }
156}
157
158impl From<PathBuf> for ImageSource {
159    fn from(path: PathBuf) -> Self {
160        Self::Path(path)
161    }
162}
163
164impl Hash for ImageSource {
165    fn hash<H: Hasher>(&self, state: &mut H) {
166        match self {
167            #[cfg(feature = "remote-asset")]
168            Self::Uri(uri) => uri.hash(state),
169            Self::Path(path) => path.hash(state),
170            Self::Bytes(id, _) => id.hash(state),
171        }
172    }
173}
174
175pub type DecodeSize = euclid::Size2D<u32, ()>;
176
177/// Limit the amount of images that are loaded in parallel.
178static DECODE_LIMIT: LazyLock<Semaphore> = LazyLock::new(|| Semaphore::new(4));
179
180impl ImageSource {
181    pub(crate) fn is_remote(&self) -> bool {
182        #[cfg(feature = "remote-asset")]
183        if let Self::Uri(_) = self {
184            return true;
185        }
186        false
187    }
188
189    /// Read the source's raw encoded bytes.
190    pub(crate) fn fetch(self) -> anyhow::Result<Bytes> {
191        Ok(match self {
192            #[cfg(feature = "remote-asset")]
193            Self::Uri(uri) => anyhow::bail!("{uri} must be loaded in parallel."),
194            Self::Path(path) => fs::read(path).map(Bytes::from)?,
195            Self::Bytes(_, bytes) => bytes,
196        })
197    }
198
199    /// Async [`Self::fetch`].
200    pub(crate) async fn bytes(self) -> anyhow::Result<Bytes> {
201        match self {
202            #[cfg(feature = "remote-asset")]
203            Self::Uri(uri) => crate::http::fetch(uri).await,
204            source => thread(move || source.fetch()).await,
205        }
206    }
207
208    /// Fetch the source's encoded bytes and decode them into a Skia image.
209    pub async fn load(
210        &self,
211        decode_size: Option<DecodeSize>,
212        sampling_mode: SamplingMode,
213    ) -> anyhow::Result<(SkImage, Bytes)> {
214        let _decode_permit = DECODE_LIMIT.acquire().await;
215        let bytes = self.clone().bytes().await?;
216        thread(move || {
217            let image = SkImage::from_encoded(unsafe { SkData::new_bytes(&bytes) })
218                .context("Failed to decode Image.")?;
219            let image = image.make_raster_image(None, None).unwrap_or(image);
220            let image = decode_size
221                .and_then(|target| Self::downsample(&image, target, &sampling_mode))
222                .unwrap_or(image);
223            Ok((image, bytes))
224        })
225        .await
226    }
227
228    fn downsample(
229        encoded: &SkImage,
230        target: DecodeSize,
231        sampling_mode: &SamplingMode,
232    ) -> Option<SkImage> {
233        let natural_width = encoded.width() as f32;
234        let natural_height = encoded.height() as f32;
235        let target_width = target.width as f32;
236        let target_height = target.height as f32;
237        if natural_width <= target_width && natural_height <= target_height {
238            return None;
239        }
240        let ratio = (target_width / natural_width).min(target_height / natural_height);
241        let width = (natural_width * ratio).round().max(1.);
242        let height = (natural_height * ratio).round().max(1.);
243
244        let mut surface = raster_n32_premul((width as i32, height as i32))?;
245        let destination = SkRect::from_xywh(0., 0., width, height);
246        let sampling = sampling_mode.sampling_options();
247        let mut paint = Paint::default();
248        paint.set_anti_alias(true);
249        surface.canvas().draw_image_rect_with_sampling_options(
250            encoded,
251            None,
252            destination,
253            sampling,
254            &paint,
255        );
256        Some(surface.image_snapshot())
257    }
258}
259
260/// How an [`ImageViewer`] picks its decode dimensions.
261#[derive(Default, Clone, Debug, PartialEq, Copy)]
262pub enum DecodeMode {
263    /// Default. Decodes to the pixel-sized layout scaled by the window scale factor,
264    /// falling back to the natural size for any other sizing (fill, percentages, auto).
265    #[default]
266    FromLayout,
267    /// Decode at the image's natural size.
268    Source,
269    /// Decode to fit within the given size, preserving aspect ratio and never upscaling.
270    Custom(Size2D),
271}
272
273impl DecodeMode {
274    fn resolve(&self, layout: &LayoutData, scale_factor: f64) -> Option<DecodeSize> {
275        let scale = scale_factor as f32;
276        let size = match self {
277            Self::Source => return None,
278            Self::FromLayout => match (&layout.width, &layout.height) {
279                (Size::Pixels(width), Size::Pixels(height)) => {
280                    Size2D::new(width.get() * scale, height.get() * scale)
281                }
282                _ => return None,
283            },
284            Self::Custom(size) => *size,
285        };
286        Some(DecodeSize::new(
287            size.width.round().max(1.) as u32,
288            size.height.round().max(1.) as u32,
289        ))
290    }
291}
292
293/// Image viewer component.
294///
295/// Handles async loading, caching, and error states for images.
296/// See [`ImageSource`] for all supported image sources.
297///
298/// # Example
299///
300/// ```rust
301/// # use freya::prelude::*;
302/// fn app() -> impl IntoElement {
303///     let source: ImageSource = (
304///         "rust-logo",
305///         include_bytes!("../../../examples/rust_logo.png"),
306///     )
307///         .into();
308///
309///     ImageViewer::new(source)
310/// }
311/// ```
312///
313/// See the [interactive components demo](https://freyaui.dev/demo).
314#[derive(PartialEq)]
315pub struct ImageViewer {
316    source: ImageSource,
317    asset_age: AssetAge,
318
319    layout: LayoutData,
320    image_data: ImageData,
321    accessibility: AccessibilityData,
322    effect: EffectData,
323    style: StyleState,
324    decode_mode: DecodeMode,
325
326    children: Vec<Element>,
327    loading_placeholder: Option<Element>,
328    error_renderer: Option<Callback<String, Element>>,
329
330    key: DiffKey,
331}
332
333impl ImageViewer {
334    pub fn new(source: impl Into<ImageSource>) -> Self {
335        ImageViewer {
336            source: source.into(),
337            asset_age: AssetAge::default(),
338            layout: LayoutData::default(),
339            image_data: ImageData::default(),
340            accessibility: AccessibilityData::default(),
341            effect: EffectData::default(),
342            style: StyleState::default(),
343            decode_mode: DecodeMode::default(),
344            children: Vec::new(),
345            loading_placeholder: None,
346            error_renderer: None,
347            key: DiffKey::None,
348        }
349    }
350}
351
352impl KeyExt for ImageViewer {
353    fn key_mut(&mut self) -> &mut DiffKey {
354        &mut self.key
355    }
356}
357
358impl LayoutExt for ImageViewer {
359    fn layout_mut(&mut self) -> &mut LayoutData {
360        &mut self.layout
361    }
362}
363
364impl ContainerSizeExt for ImageViewer {}
365impl ContainerWithContentExt for ImageViewer {}
366impl ContainerPositionExt for ImageViewer {}
367
368impl ImageExt for ImageViewer {
369    fn image_data_mut(&mut self) -> &mut ImageData {
370        &mut self.image_data
371    }
372}
373
374impl AccessibilityExt for ImageViewer {
375    fn accessibility_data_mut(&mut self) -> &mut AccessibilityData {
376        &mut self.accessibility
377    }
378}
379
380impl ChildrenExt for ImageViewer {
381    fn children_mut(&mut self) -> &mut Vec<Element> {
382        &mut self.children
383    }
384}
385
386impl EffectExt for ImageViewer {
387    fn effect_mut(&mut self) -> &mut EffectData {
388        &mut self.effect
389    }
390}
391
392impl DecorationExt for ImageViewer {}
393
394impl StyleExt for ImageViewer {
395    fn style_mut(&mut self) -> &mut StyleState {
396        &mut self.style
397    }
398}
399
400impl ImageViewer {
401    /// Custom element rendered while loading.
402    pub fn loading_placeholder(mut self, placeholder: impl Into<Element>) -> Self {
403        self.loading_placeholder = Some(placeholder.into());
404        self
405    }
406
407    /// Pick how the image is decoded. See [`DecodeMode`].
408    pub fn decode_mode(mut self, decode_mode: DecodeMode) -> Self {
409        self.decode_mode = decode_mode;
410        self
411    }
412
413    /// Customize how long the image will remain cached after no longer being used.
414    ///
415    /// Defaults to [`AssetAge::default`] (1h).
416    pub fn asset_age(mut self, asset_age: impl Into<AssetAge>) -> Self {
417        self.asset_age = asset_age.into();
418        self
419    }
420
421    /// Custom element rendered when the image fails to load.
422    pub fn error_renderer(mut self, renderer: impl Into<Callback<String, Element>>) -> Self {
423        self.error_renderer = Some(renderer.into());
424        self
425    }
426}
427
428impl Component for ImageViewer {
429    fn render(&self) -> impl IntoElement {
430        let target = self.decode_mode.resolve(
431            &self.layout,
432            *Platform::get().current_window().scale_factor.read(),
433        );
434        let sampling_mode = self.image_data.sampling_mode.clone();
435        let asset_config =
436            AssetConfiguration::new((&self.source, target, &sampling_mode), self.asset_age);
437        use_asset(&asset_config);
438        let mut asset_cacher = use_hook(AssetCacher::get);
439
440        use_side_effect_with_deps(
441            &(
442                self.source.clone(),
443                asset_config.clone(),
444                target,
445                sampling_mode,
446            ),
447            move |(source, asset_config, target, sampling_mode)| {
448                if matches!(
449                    asset_cacher.read_asset(asset_config),
450                    Some(Asset::Pending) | Some(Asset::Error(_))
451                ) {
452                    asset_cacher.update_asset(asset_config.clone(), Asset::Loading);
453
454                    let source = source.clone();
455                    let asset_config = asset_config.clone();
456                    let target = *target;
457                    let sampling_mode = sampling_mode.clone();
458                    spawn_in_window(async move {
459                        match source.load(target, sampling_mode).await {
460                            Ok((image, bytes)) => {
461                                asset_cacher.update_asset(
462                                    asset_config,
463                                    Asset::Cached(Rc::new(ImageHandle::new(image, bytes))),
464                                );
465                            }
466                            Err(err) => {
467                                asset_cacher
468                                    .update_asset(asset_config, Asset::Error(err.to_string()));
469                            }
470                        }
471                    });
472                }
473            },
474        );
475
476        let asset = asset_cacher
477            .read_asset(&asset_config)
478            .expect("Asset should exist by now");
479
480        match asset {
481            Asset::Cached(asset) => {
482                let asset = asset.downcast_ref::<ImageHandle>().unwrap().clone();
483                image(asset)
484                    .accessibility(self.accessibility.clone())
485                    .a11y_role(AccessibilityRole::Image)
486                    .layout(self.layout.clone())
487                    .image_data(self.image_data.clone())
488                    .effect(self.effect.clone())
489                    .children(self.children.clone())
490                    .style(self.style.clone())
491                    .into_element()
492            }
493            Asset::Pending | Asset::Loading => rect()
494                .layout(self.layout.clone())
495                .center()
496                .child(
497                    self.loading_placeholder
498                        .clone()
499                        .unwrap_or_else(|| CircularLoader::new().into_element()),
500                )
501                .into(),
502            Asset::Error(err) => match &self.error_renderer {
503                Some(renderer) => renderer.call(err),
504                None => err.into(),
505            },
506        }
507    }
508
509    fn render_key(&self) -> DiffKey {
510        self.key.clone().or(self.default_key())
511    }
512}