freya_components/
image_viewer.rs1use 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#[derive(PartialEq, Clone)]
80pub enum ImageSource {
81 #[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,
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#[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(
281 &(self.source.clone(), asset_config),
282 move |(source, asset_config): &(ImageSource, AssetConfiguration)| {
283 let source = source.clone();
284
285 for asset_task in assets_tasks.write().drain(..) {
287 asset_task.cancel();
288 }
289
290 if matches!(
292 asset_cacher.read_asset(asset_config),
293 Some(Asset::Pending) | Some(Asset::Error(_))
294 ) {
295 asset_cacher.update_asset(asset_config.clone(), Asset::Loading);
297
298 let asset_config = asset_config.clone();
299 let asset_task = spawn(async move {
300 match source.bytes().await {
301 Ok((image, bytes)) => {
302 let image_holder = ImageHolder {
304 bytes,
305 image: Rc::new(RefCell::new(image)),
306 };
307 asset_cacher.update_asset(
308 asset_config.clone(),
309 Asset::Cached(Rc::new(image_holder)),
310 );
311 }
312 Err(err) => {
313 asset_cacher
315 .update_asset(asset_config, Asset::Error(err.to_string()));
316 }
317 }
318 });
319
320 assets_tasks.write().push(asset_task);
321 }
322 },
323 );
324
325 match asset {
326 Asset::Cached(asset) => {
327 let asset = asset.downcast_ref::<ImageHolder>().unwrap().clone();
328 image(asset)
329 .accessibility(self.accessibility.clone())
330 .a11y_role(AccessibilityRole::Image)
331 .a11y_focusable(true)
332 .layout(self.layout.clone())
333 .image_data(self.image_data.clone())
334 .effect(self.effect.clone())
335 .children(self.children.clone())
336 .map(self.corner_radius, |img, corner_radius| {
337 img.corner_radius(corner_radius)
338 })
339 .into_element()
340 }
341 Asset::Pending | Asset::Loading => rect()
342 .layout(self.layout.clone())
343 .center()
344 .child(CircularLoader::new())
345 .into(),
346 Asset::Error(err) => err.into(),
347 }
348 }
349
350 fn render_key(&self) -> DiffKey {
351 self.key.clone().or(self.default_key())
352 }
353}