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