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    AlphaType,
21    ColorType,
22    Data,
23    ISize,
24    ImageInfo,
25    SkData,
26    SkImage,
27    raster_from_data,
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
174            if let Some(target) = decode_size
175                && let Some(image) = Self::downsample(&bytes, target)?
176            {
177                return Ok((image, bytes));
178            }
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            Ok((image, bytes))
184        })
185        .await
186    }
187
188    fn downsample(bytes: &[u8], target: DecodeSize) -> anyhow::Result<Option<SkImage>> {
189        use std::io::Cursor;
190
191        use image::ImageReader;
192
193        let reader = || {
194            ImageReader::new(Cursor::new(bytes))
195                .with_guessed_format()
196                .context("Failed to guess image format.")
197        };
198
199        let (natural_width, natural_height) = reader()?
200            .into_dimensions()
201            .context("Failed to read image dimensions.")?;
202
203        if natural_width <= target.width && natural_height <= target.height {
204            return Ok(None);
205        }
206
207        let rgba = reader()?
208            .decode()
209            .context("Failed to decode Image.")?
210            .thumbnail(target.width, target.height)
211            .to_rgba8();
212        let (width, height) = rgba.dimensions();
213        let info = ImageInfo::new(
214            ISize::new(width as i32, height as i32),
215            ColorType::RGBA8888,
216            AlphaType::Unpremul,
217            None,
218        );
219        raster_from_data(&info, Data::new_copy(&rgba), (width * 4) as usize)
220            .map(Some)
221            .context("Failed to wrap downsampled image as raster.")
222    }
223}
224
225/// How an [`ImageViewer`] picks its decode dimensions.
226#[derive(Default, Clone, Debug, PartialEq, Copy)]
227pub enum DecodeMode {
228    /// Use the layout's pixel dimensions when both are [`Size::Pixels`].
229    #[default]
230    FromLayout,
231    /// Decode at a specific maximum size, preserving aspect ratio.
232    Custom(Size2D),
233}
234
235impl DecodeMode {
236    fn resolve(&self, layout: &LayoutData) -> Option<DecodeSize> {
237        let size = match self {
238            Self::FromLayout => match (&layout.width, &layout.height) {
239                (Size::Pixels(w), Size::Pixels(h)) => Size2D::new(w.get(), h.get()),
240                _ => return None,
241            },
242            Self::Custom(size) => *size,
243        };
244        Some(DecodeSize::new(
245            size.width.round().max(1.) as u32,
246            size.height.round().max(1.) as u32,
247        ))
248    }
249}
250
251/// Image viewer component.
252///
253/// Handles async loading, caching, and error states for images.
254/// See [`ImageSource`] for all supported image sources.
255///
256/// # Example
257///
258/// ```rust
259/// # use freya::prelude::*;
260/// fn app() -> impl IntoElement {
261///     let source: ImageSource = (
262///         "rust-logo",
263///         include_bytes!("../../../examples/rust_logo.png"),
264///     )
265///         .into();
266///
267///     ImageViewer::new(source)
268/// }
269/// # use freya::prelude::*;
270/// # use freya_testing::prelude::*;
271/// # use std::path::PathBuf;
272/// # launch_doc(|| {
273/// #   rect().center().expanded().child(ImageViewer::new(("rust-logo", include_bytes!("../../../examples/rust_logo.png"))))
274/// # }, "./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();
275/// ```
276///
277/// # Preview
278/// ![ImageViewer Preview][image_viewer]
279#[cfg_attr(feature = "docs",
280    doc = embed_doc_image::embed_image!("image_viewer", "images/gallery_image_viewer.png")
281)]
282#[derive(PartialEq)]
283pub struct ImageViewer {
284    source: ImageSource,
285    asset_age: AssetAge,
286
287    layout: LayoutData,
288    image_data: ImageData,
289    accessibility: AccessibilityData,
290    effect: EffectData,
291    corner_radius: Option<CornerRadius>,
292    decode_mode: DecodeMode,
293
294    children: Vec<Element>,
295    loading_placeholder: Option<Element>,
296    error_renderer: Option<Callback<String, Element>>,
297
298    key: DiffKey,
299}
300
301impl ImageViewer {
302    pub fn new(source: impl Into<ImageSource>) -> Self {
303        ImageViewer {
304            source: source.into(),
305            asset_age: AssetAge::default(),
306            layout: LayoutData::default(),
307            image_data: ImageData::default(),
308            accessibility: AccessibilityData::default(),
309            effect: EffectData::default(),
310            corner_radius: None,
311            decode_mode: DecodeMode::default(),
312            children: Vec::new(),
313            loading_placeholder: None,
314            error_renderer: None,
315            key: DiffKey::None,
316        }
317    }
318}
319
320impl KeyExt for ImageViewer {
321    fn write_key(&mut self) -> &mut DiffKey {
322        &mut self.key
323    }
324}
325
326impl LayoutExt for ImageViewer {
327    fn get_layout(&mut self) -> &mut LayoutData {
328        &mut self.layout
329    }
330}
331
332impl ContainerSizeExt for ImageViewer {}
333impl ContainerWithContentExt for ImageViewer {}
334
335impl ImageExt for ImageViewer {
336    fn get_image_data(&mut self) -> &mut ImageData {
337        &mut self.image_data
338    }
339}
340
341impl AccessibilityExt for ImageViewer {
342    fn get_accessibility_data(&mut self) -> &mut AccessibilityData {
343        &mut self.accessibility
344    }
345}
346
347impl ChildrenExt for ImageViewer {
348    fn get_children(&mut self) -> &mut Vec<Element> {
349        &mut self.children
350    }
351}
352
353impl EffectExt for ImageViewer {
354    fn get_effect(&mut self) -> &mut EffectData {
355        &mut self.effect
356    }
357}
358
359impl ImageViewer {
360    pub fn corner_radius(mut self, corner_radius: impl Into<CornerRadius>) -> Self {
361        self.corner_radius = Some(corner_radius.into());
362        self
363    }
364
365    /// Custom element rendered while loading.
366    pub fn loading_placeholder(mut self, placeholder: impl Into<Element>) -> Self {
367        self.loading_placeholder = Some(placeholder.into());
368        self
369    }
370
371    /// Pick how the image is decoded. See [`DecodeMode`].
372    pub fn decode_mode(mut self, decode_mode: DecodeMode) -> Self {
373        self.decode_mode = decode_mode;
374        self
375    }
376
377    /// Customize how long the image will remain cached after no longer being used.
378    ///
379    /// Defaults to [`AssetAge::default`] (1h).
380    pub fn asset_age(mut self, asset_age: impl Into<AssetAge>) -> Self {
381        self.asset_age = asset_age.into();
382        self
383    }
384
385    /// Custom element rendered when the image fails to load.
386    pub fn error_renderer(mut self, renderer: impl Into<Callback<String, Element>>) -> Self {
387        self.error_renderer = Some(renderer.into());
388        self
389    }
390}
391
392impl Component for ImageViewer {
393    fn render(&self) -> impl IntoElement {
394        let target = self.decode_mode.resolve(&self.layout);
395        let asset_config = AssetConfiguration::new((&self.source, target), self.asset_age.clone());
396        let asset = use_asset(&asset_config);
397        let mut asset_cacher = use_hook(AssetCacher::get);
398
399        use_side_effect_with_deps(
400            &(self.source.clone(), asset_config, target),
401            move |(source, asset_config, target): &(
402                ImageSource,
403                AssetConfiguration,
404                Option<DecodeSize>,
405            )| {
406                if matches!(
407                    asset_cacher.read_asset(asset_config),
408                    Some(Asset::Pending) | Some(Asset::Error(_))
409                ) {
410                    asset_cacher.update_asset(asset_config.clone(), Asset::Loading);
411
412                    let source = source.clone();
413                    let asset_config = asset_config.clone();
414                    let target = *target;
415                    spawn_forever(async move {
416                        match source.bytes(target).await {
417                            Ok((image, bytes)) => {
418                                let image_holder = ImageHolder {
419                                    bytes,
420                                    image: Rc::new(RefCell::new(image)),
421                                };
422                                asset_cacher.update_asset(
423                                    asset_config,
424                                    Asset::Cached(Rc::new(image_holder)),
425                                );
426                            }
427                            Err(err) => {
428                                asset_cacher
429                                    .update_asset(asset_config, Asset::Error(err.to_string()));
430                            }
431                        }
432                    });
433                }
434            },
435        );
436
437        match asset {
438            Asset::Cached(asset) => {
439                let asset = asset.downcast_ref::<ImageHolder>().unwrap().clone();
440                image(asset)
441                    .accessibility(self.accessibility.clone())
442                    .a11y_role(AccessibilityRole::Image)
443                    .layout(self.layout.clone())
444                    .image_data(self.image_data.clone())
445                    .effect(self.effect.clone())
446                    .children(self.children.clone())
447                    .map(self.corner_radius, |img, corner_radius| {
448                        img.corner_radius(corner_radius)
449                    })
450                    .into_element()
451            }
452            Asset::Pending | Asset::Loading => rect()
453                .layout(self.layout.clone())
454                .center()
455                .child(
456                    self.loading_placeholder
457                        .clone()
458                        .unwrap_or_else(|| CircularLoader::new().into_element()),
459                )
460                .into(),
461            Asset::Error(err) => match &self.error_renderer {
462                Some(renderer) => renderer.call(err),
463                None => err.into(),
464            },
465        }
466    }
467
468    fn render_key(&self) -> DiffKey {
469        self.key.clone().or(self.default_key())
470    }
471}