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