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