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    SkData,
21    SkImage,
22};
23#[cfg(feature = "remote-asset")]
24use ureq::http::Uri;
25
26use crate::{
27    cache::*,
28    loader::CircularLoader,
29};
30
31/// Supported image sources for [`ImageViewer`].
32///
33/// ### URI
34///
35/// Good to load remote images.
36///
37/// > Requires the `remote-asset` feature to be enabled.
38///
39/// ```rust
40/// # use freya::prelude::*;
41/// let source: ImageSource =
42///     "https://upload.wikimedia.org/wikipedia/commons/8/8a/Gecarcinus_quadratus_%28Nosara%29.jpg"
43///         .into();
44/// ```
45///
46/// ### Path
47///
48/// Good for dynamic loading.
49///
50/// ```rust
51/// # use freya::prelude::*;
52/// # use std::path::PathBuf;
53/// let source: ImageSource = PathBuf::from("./examples/rust_logo.png").into();
54/// ```
55/// ### Raw bytes
56///
57/// Good for embedded images.
58///
59/// ```rust
60/// # use freya::prelude::*;
61/// let source: ImageSource = (
62///     "rust-logo",
63///     include_bytes!("../../../examples/rust_logo.png"),
64/// )
65///     .into();
66/// ```
67///
68/// ### Dynamic bytes
69///
70/// Good for rendering custom allocated images.
71///
72/// ```rust
73/// # use freya::prelude::*;
74/// # use bytes::Bytes;
75/// fn app() -> impl IntoElement {
76///     let image_data = use_state(|| (0, Bytes::from(vec![/* ... */])));
77///     let source: ImageSource = image_data.read().clone().into();
78///     ImageViewer::new(source)
79/// }
80/// ```
81#[derive(PartialEq, Clone)]
82pub enum ImageSource {
83    /// Remote image loaded from a URI.
84    ///
85    /// Requires the `remote-asset` feature.
86    #[cfg(feature = "remote-asset")]
87    Uri(Uri),
88
89    Path(PathBuf),
90
91    Bytes(u64, Bytes),
92}
93
94impl<H: Hash> From<(H, Bytes)> for ImageSource {
95    fn from((id, bytes): (H, Bytes)) -> Self {
96        let mut hasher = DefaultHasher::default();
97        id.hash(&mut hasher);
98        Self::Bytes(hasher.finish(), bytes)
99    }
100}
101
102impl<H: Hash> From<(H, &'static [u8])> for ImageSource {
103    fn from((id, bytes): (H, &'static [u8])) -> Self {
104        let mut hasher = DefaultHasher::default();
105        id.hash(&mut hasher);
106        Self::Bytes(hasher.finish(), Bytes::from_static(bytes))
107    }
108}
109
110impl<const N: usize, H: Hash> From<(H, &'static [u8; N])> for ImageSource {
111    fn from((id, bytes): (H, &'static [u8; N])) -> Self {
112        let mut hasher = DefaultHasher::default();
113        id.hash(&mut hasher);
114        Self::Bytes(hasher.finish(), Bytes::from_static(bytes))
115    }
116}
117
118#[cfg_attr(feature = "docs", doc(cfg(feature = "remote-asset")))]
119#[cfg(feature = "remote-asset")]
120impl From<Uri> for ImageSource {
121    fn from(uri: Uri) -> Self {
122        Self::Uri(uri)
123    }
124}
125
126#[cfg_attr(feature = "docs", doc(cfg(feature = "remote-asset")))]
127#[cfg(feature = "remote-asset")]
128impl From<&'static str> for ImageSource {
129    fn from(src: &'static str) -> Self {
130        Self::Uri(Uri::from_static(src))
131    }
132}
133
134impl From<PathBuf> for ImageSource {
135    fn from(path: PathBuf) -> Self {
136        Self::Path(path)
137    }
138}
139
140impl Hash for ImageSource {
141    fn hash<H: Hasher>(&self, state: &mut H) {
142        match self {
143            #[cfg(feature = "remote-asset")]
144            Self::Uri(uri) => uri.hash(state),
145            Self::Path(path) => path.hash(state),
146            Self::Bytes(id, _) => id.hash(state),
147        }
148    }
149}
150
151impl ImageSource {
152    pub async fn bytes(&self) -> anyhow::Result<(SkImage, Bytes)> {
153        let source = self.clone();
154        blocking::unblock(move || {
155            let bytes = match source {
156                #[cfg(feature = "remote-asset")]
157                Self::Uri(uri) => ureq::get(uri)
158                    .call()?
159                    .body_mut()
160                    .read_to_vec()
161                    .map(Bytes::from)?,
162                Self::Path(path) => fs::read(path).map(Bytes::from)?,
163                Self::Bytes(_, bytes) => bytes,
164            };
165            let image = SkImage::from_encoded(unsafe { SkData::new_bytes(&bytes) })
166                .context("Failed to decode Image.")?;
167            let image = image.make_raster_image(None, None).unwrap_or(image);
168            Ok((image, bytes))
169        })
170        .await
171    }
172}
173
174/// Image viewer component.
175///
176/// Handles async loading, caching, and error states for images.
177/// See [`ImageSource`] for all supported image sources.
178///
179/// # Example
180///
181/// ```rust
182/// # use freya::prelude::*;
183/// fn app() -> impl IntoElement {
184///     let source: ImageSource = (
185///         "rust-logo",
186///         include_bytes!("../../../examples/rust_logo.png"),
187///     )
188///         .into();
189///
190///     ImageViewer::new(source)
191/// }
192/// # use freya::prelude::*;
193/// # use freya_testing::prelude::*;
194/// # use std::path::PathBuf;
195/// # launch_doc(|| {
196/// #   rect().center().expanded().child(ImageViewer::new(("rust-logo", include_bytes!("../../../examples/rust_logo.png"))))
197/// # }, "./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();
198/// ```
199///
200/// # Preview
201/// ![ImageViewer Preview][image_viewer]
202#[cfg_attr(feature = "docs",
203    doc = embed_doc_image::embed_image!("image_viewer", "images/gallery_image_viewer.png")
204)]
205#[derive(PartialEq)]
206pub struct ImageViewer {
207    source: ImageSource,
208    asset_age: AssetAge,
209
210    layout: LayoutData,
211    image_data: ImageData,
212    accessibility: AccessibilityData,
213    effect: EffectData,
214    corner_radius: Option<CornerRadius>,
215
216    children: Vec<Element>,
217    loading_placeholder: Option<Element>,
218    error_renderer: Option<Callback<String, Element>>,
219
220    key: DiffKey,
221}
222
223impl ImageViewer {
224    pub fn new(source: impl Into<ImageSource>) -> Self {
225        ImageViewer {
226            source: source.into(),
227            asset_age: AssetAge::default(),
228            layout: LayoutData::default(),
229            image_data: ImageData::default(),
230            accessibility: AccessibilityData::default(),
231            effect: EffectData::default(),
232            corner_radius: None,
233            children: Vec::new(),
234            loading_placeholder: None,
235            error_renderer: None,
236            key: DiffKey::None,
237        }
238    }
239}
240
241impl KeyExt for ImageViewer {
242    fn write_key(&mut self) -> &mut DiffKey {
243        &mut self.key
244    }
245}
246
247impl LayoutExt for ImageViewer {
248    fn get_layout(&mut self) -> &mut LayoutData {
249        &mut self.layout
250    }
251}
252
253impl ContainerSizeExt for ImageViewer {}
254impl ContainerWithContentExt for ImageViewer {}
255
256impl ImageExt for ImageViewer {
257    fn get_image_data(&mut self) -> &mut ImageData {
258        &mut self.image_data
259    }
260}
261
262impl AccessibilityExt for ImageViewer {
263    fn get_accessibility_data(&mut self) -> &mut AccessibilityData {
264        &mut self.accessibility
265    }
266}
267
268impl ChildrenExt for ImageViewer {
269    fn get_children(&mut self) -> &mut Vec<Element> {
270        &mut self.children
271    }
272}
273
274impl EffectExt for ImageViewer {
275    fn get_effect(&mut self) -> &mut EffectData {
276        &mut self.effect
277    }
278}
279
280impl ImageViewer {
281    pub fn corner_radius(mut self, corner_radius: impl Into<CornerRadius>) -> Self {
282        self.corner_radius = Some(corner_radius.into());
283        self
284    }
285
286    /// Custom element rendered while loading.
287    pub fn loading_placeholder(mut self, placeholder: impl Into<Element>) -> Self {
288        self.loading_placeholder = Some(placeholder.into());
289        self
290    }
291
292    /// Customize how long the image will remain cached after no longer being used.
293    ///
294    /// Defaults to [`AssetAge::default`] (1h).
295    pub fn asset_age(mut self, asset_age: impl Into<AssetAge>) -> Self {
296        self.asset_age = asset_age.into();
297        self
298    }
299
300    /// Custom element rendered when the image fails to load.
301    pub fn error_renderer(mut self, renderer: impl Into<Callback<String, Element>>) -> Self {
302        self.error_renderer = Some(renderer.into());
303        self
304    }
305}
306
307impl Component for ImageViewer {
308    fn render(&self) -> impl IntoElement {
309        let asset_config = AssetConfiguration::new(&self.source, self.asset_age.clone());
310        let asset = use_asset(&asset_config);
311        let mut asset_cacher = use_hook(AssetCacher::get);
312
313        use_side_effect_with_deps(
314            &(self.source.clone(), asset_config),
315            move |(source, asset_config): &(ImageSource, AssetConfiguration)| {
316                // Fetch asset if still pending or errored. The Loading state
317                // guards against duplicate in-flight fetches.
318                if matches!(
319                    asset_cacher.read_asset(asset_config),
320                    Some(Asset::Pending) | Some(Asset::Error(_))
321                ) {
322                    asset_cacher.update_asset(asset_config.clone(), Asset::Loading);
323
324                    let source = source.clone();
325                    let asset_config = asset_config.clone();
326                    spawn_forever(async move {
327                        match source.bytes().await {
328                            Ok((image, bytes)) => {
329                                // Image loaded
330                                let image_holder = ImageHolder {
331                                    bytes,
332                                    image: Rc::new(RefCell::new(image)),
333                                };
334                                asset_cacher.update_asset(
335                                    asset_config,
336                                    Asset::Cached(Rc::new(image_holder)),
337                                );
338                            }
339                            Err(err) => {
340                                // Image errored
341                                asset_cacher
342                                    .update_asset(asset_config, Asset::Error(err.to_string()));
343                            }
344                        }
345                    });
346                }
347            },
348        );
349
350        match asset {
351            Asset::Cached(asset) => {
352                let asset = asset.downcast_ref::<ImageHolder>().unwrap().clone();
353                image(asset)
354                    .accessibility(self.accessibility.clone())
355                    .a11y_role(AccessibilityRole::Image)
356                    .layout(self.layout.clone())
357                    .image_data(self.image_data.clone())
358                    .effect(self.effect.clone())
359                    .children(self.children.clone())
360                    .map(self.corner_radius, |img, corner_radius| {
361                        img.corner_radius(corner_radius)
362                    })
363                    .into_element()
364            }
365            Asset::Pending | Asset::Loading => rect()
366                .layout(self.layout.clone())
367                .center()
368                .child(
369                    self.loading_placeholder
370                        .clone()
371                        .unwrap_or_else(|| CircularLoader::new().into_element()),
372                )
373                .into(),
374            Asset::Error(err) => match &self.error_renderer {
375                Some(renderer) => renderer.call(err),
376                None => err.into(),
377            },
378        }
379    }
380
381    fn render_key(&self) -> DiffKey {
382        self.key.clone().or(self.default_key())
383    }
384}