Skip to main content

freya_components/
image_viewer.rs

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