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
209    layout: LayoutData,
210    image_data: ImageData,
211    accessibility: AccessibilityData,
212    effect: EffectData,
213    corner_radius: Option<CornerRadius>,
214
215    children: Vec<Element>,
216    loading_placeholder: Option<Element>,
217    error_renderer: Option<Callback<String, Element>>,
218
219    key: DiffKey,
220}
221
222impl ImageViewer {
223    pub fn new(source: impl Into<ImageSource>) -> Self {
224        ImageViewer {
225            source: source.into(),
226            layout: LayoutData::default(),
227            image_data: ImageData::default(),
228            accessibility: AccessibilityData::default(),
229            effect: EffectData::default(),
230            corner_radius: None,
231            children: Vec::new(),
232            loading_placeholder: None,
233            error_renderer: None,
234            key: DiffKey::None,
235        }
236    }
237}
238
239impl KeyExt for ImageViewer {
240    fn write_key(&mut self) -> &mut DiffKey {
241        &mut self.key
242    }
243}
244
245impl LayoutExt for ImageViewer {
246    fn get_layout(&mut self) -> &mut LayoutData {
247        &mut self.layout
248    }
249}
250
251impl ContainerSizeExt for ImageViewer {}
252impl ContainerWithContentExt for ImageViewer {}
253
254impl ImageExt for ImageViewer {
255    fn get_image_data(&mut self) -> &mut ImageData {
256        &mut self.image_data
257    }
258}
259
260impl AccessibilityExt for ImageViewer {
261    fn get_accessibility_data(&mut self) -> &mut AccessibilityData {
262        &mut self.accessibility
263    }
264}
265
266impl ChildrenExt for ImageViewer {
267    fn get_children(&mut self) -> &mut Vec<Element> {
268        &mut self.children
269    }
270}
271
272impl EffectExt for ImageViewer {
273    fn get_effect(&mut self) -> &mut EffectData {
274        &mut self.effect
275    }
276}
277
278impl ImageViewer {
279    pub fn corner_radius(mut self, corner_radius: impl Into<CornerRadius>) -> Self {
280        self.corner_radius = Some(corner_radius.into());
281        self
282    }
283
284    /// Custom element rendered while loading.
285    pub fn loading_placeholder(mut self, placeholder: impl Into<Element>) -> Self {
286        self.loading_placeholder = Some(placeholder.into());
287        self
288    }
289
290    /// Custom element rendered when the image fails to load.
291    pub fn error_renderer(mut self, renderer: impl Into<Callback<String, Element>>) -> Self {
292        self.error_renderer = Some(renderer.into());
293        self
294    }
295}
296
297impl Component for ImageViewer {
298    fn render(&self) -> impl IntoElement {
299        let asset_config = AssetConfiguration::new(&self.source, AssetAge::default());
300        let asset = use_asset(&asset_config);
301        let mut asset_cacher = use_hook(AssetCacher::get);
302
303        use_side_effect_with_deps(
304            &(self.source.clone(), asset_config),
305            move |(source, asset_config): &(ImageSource, AssetConfiguration)| {
306                // Fetch asset if still pending or errored. The Loading state
307                // guards against duplicate in-flight fetches.
308                if matches!(
309                    asset_cacher.read_asset(asset_config),
310                    Some(Asset::Pending) | Some(Asset::Error(_))
311                ) {
312                    asset_cacher.update_asset(asset_config.clone(), Asset::Loading);
313
314                    let source = source.clone();
315                    let asset_config = asset_config.clone();
316                    spawn_forever(async move {
317                        match source.bytes().await {
318                            Ok((image, bytes)) => {
319                                // Image loaded
320                                let image_holder = ImageHolder {
321                                    bytes,
322                                    image: Rc::new(RefCell::new(image)),
323                                };
324                                asset_cacher.update_asset(
325                                    asset_config,
326                                    Asset::Cached(Rc::new(image_holder)),
327                                );
328                            }
329                            Err(err) => {
330                                // Image errored
331                                asset_cacher
332                                    .update_asset(asset_config, Asset::Error(err.to_string()));
333                            }
334                        }
335                    });
336                }
337            },
338        );
339
340        match asset {
341            Asset::Cached(asset) => {
342                let asset = asset.downcast_ref::<ImageHolder>().unwrap().clone();
343                image(asset)
344                    .accessibility(self.accessibility.clone())
345                    .a11y_role(AccessibilityRole::Image)
346                    .a11y_focusable(true)
347                    .layout(self.layout.clone())
348                    .image_data(self.image_data.clone())
349                    .effect(self.effect.clone())
350                    .children(self.children.clone())
351                    .map(self.corner_radius, |img, corner_radius| {
352                        img.corner_radius(corner_radius)
353                    })
354                    .into_element()
355            }
356            Asset::Pending | Asset::Loading => rect()
357                .layout(self.layout.clone())
358                .center()
359                .child(
360                    self.loading_placeholder
361                        .clone()
362                        .unwrap_or_else(|| CircularLoader::new().into_element()),
363                )
364                .into(),
365            Asset::Error(err) => match &self.error_renderer {
366                Some(renderer) => renderer.call(err),
367                None => err.into(),
368            },
369        }
370    }
371
372    fn render_key(&self) -> DiffKey {
373        self.key.clone().or(self.default_key())
374    }
375}