freya_components/
image_viewer.rs

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