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