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            Ok((image, bytes))
168        })
169        .await
170    }
171}
172
173/// Image viewer component.
174///
175/// Handles async loading, caching, and error states for images.
176/// See [`ImageSource`] for all supported image sources.
177///
178/// # Example
179///
180/// ```rust
181/// # use freya::prelude::*;
182/// fn app() -> impl IntoElement {
183///     let source: ImageSource = (
184///         "rust-logo",
185///         include_bytes!("../../../examples/rust_logo.png"),
186///     )
187///         .into();
188///
189///     ImageViewer::new(source)
190/// }
191/// # use freya::prelude::*;
192/// # use freya_testing::prelude::*;
193/// # use std::path::PathBuf;
194/// # launch_doc(|| {
195/// #   rect().center().expanded().child(ImageViewer::new(("rust-logo", include_bytes!("../../../examples/rust_logo.png"))))
196/// # }, "./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();
197/// ```
198///
199/// # Preview
200/// ![ImageViewer Preview][image_viewer]
201#[cfg_attr(feature = "docs",
202    doc = embed_doc_image::embed_image!("image_viewer", "images/gallery_image_viewer.png")
203)]
204#[derive(PartialEq)]
205pub struct ImageViewer {
206    source: ImageSource,
207
208    layout: LayoutData,
209    image_data: ImageData,
210    accessibility: AccessibilityData,
211    effect: EffectData,
212    corner_radius: Option<CornerRadius>,
213
214    children: Vec<Element>,
215
216    key: DiffKey,
217}
218
219impl ImageViewer {
220    pub fn new(source: impl Into<ImageSource>) -> Self {
221        ImageViewer {
222            source: source.into(),
223            layout: LayoutData::default(),
224            image_data: ImageData::default(),
225            accessibility: AccessibilityData::default(),
226            effect: EffectData::default(),
227            corner_radius: None,
228            children: Vec::new(),
229            key: DiffKey::None,
230        }
231    }
232}
233
234impl KeyExt for ImageViewer {
235    fn write_key(&mut self) -> &mut DiffKey {
236        &mut self.key
237    }
238}
239
240impl LayoutExt for ImageViewer {
241    fn get_layout(&mut self) -> &mut LayoutData {
242        &mut self.layout
243    }
244}
245
246impl ContainerSizeExt for ImageViewer {}
247impl ContainerWithContentExt for ImageViewer {}
248
249impl ImageExt for ImageViewer {
250    fn get_image_data(&mut self) -> &mut ImageData {
251        &mut self.image_data
252    }
253}
254
255impl AccessibilityExt for ImageViewer {
256    fn get_accessibility_data(&mut self) -> &mut AccessibilityData {
257        &mut self.accessibility
258    }
259}
260
261impl ChildrenExt for ImageViewer {
262    fn get_children(&mut self) -> &mut Vec<Element> {
263        &mut self.children
264    }
265}
266
267impl EffectExt for ImageViewer {
268    fn get_effect(&mut self) -> &mut EffectData {
269        &mut self.effect
270    }
271}
272
273impl ImageViewer {
274    pub fn corner_radius(mut self, corner_radius: impl Into<CornerRadius>) -> Self {
275        self.corner_radius = Some(corner_radius.into());
276        self
277    }
278}
279
280impl Component for ImageViewer {
281    fn render(&self) -> impl IntoElement {
282        let asset_config = AssetConfiguration::new(&self.source, AssetAge::default());
283        let asset = use_asset(&asset_config);
284        let mut asset_cacher = use_hook(AssetCacher::get);
285        let mut assets_tasks = use_state::<Vec<TaskHandle>>(Vec::new);
286
287        use_side_effect_with_deps(
288            &(self.source.clone(), asset_config),
289            move |(source, asset_config): &(ImageSource, AssetConfiguration)| {
290                let source = source.clone();
291
292                // Cancel previous asset fetching requests
293                for asset_task in assets_tasks.write().drain(..) {
294                    asset_task.cancel();
295                }
296
297                // Fetch asset if still pending or errored
298                if matches!(
299                    asset_cacher.read_asset(asset_config),
300                    Some(Asset::Pending) | Some(Asset::Error(_))
301                ) {
302                    // Mark asset as loading
303                    asset_cacher.update_asset(asset_config.clone(), Asset::Loading);
304
305                    let asset_config = asset_config.clone();
306                    let asset_task = spawn(async move {
307                        match source.bytes().await {
308                            Ok((image, bytes)) => {
309                                // Image loaded
310                                let image_holder = ImageHolder {
311                                    bytes,
312                                    image: Rc::new(RefCell::new(image)),
313                                };
314                                asset_cacher.update_asset(
315                                    asset_config.clone(),
316                                    Asset::Cached(Rc::new(image_holder)),
317                                );
318                            }
319                            Err(err) => {
320                                // Image errored asset_cacher
321                                asset_cacher
322                                    .update_asset(asset_config, Asset::Error(err.to_string()));
323                            }
324                        }
325                    });
326
327                    assets_tasks.write().push(asset_task);
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(CircularLoader::new())
352                .into(),
353            Asset::Error(err) => err.into(),
354        }
355    }
356
357    fn render_key(&self) -> DiffKey {
358        self.key.clone().or(self.default_key())
359    }
360}