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#[derive(PartialEq, Clone)]
82pub enum ImageSource {
83 #[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 let image = image.make_raster_image(None, None).unwrap_or(image);
168 Ok((image, bytes))
169 })
170 .await
171 }
172}
173
174#[cfg_attr(feature = "docs",
203 doc = embed_doc_image::embed_image!("image_viewer", "images/gallery_image_viewer.png")
204)]
205#[derive(PartialEq)]
206pub struct ImageViewer {
207 source: ImageSource,
208
209 layout: LayoutData,
210 image_data: ImageData,
211 accessibility: AccessibilityData,
212 effect: EffectData,
213 corner_radius: Option<CornerRadius>,
214
215 children: Vec<Element>,
216 loading_placeholder: Option<Element>,
217 error_renderer: Option<Callback<String, Element>>,
218
219 key: DiffKey,
220}
221
222impl ImageViewer {
223 pub fn new(source: impl Into<ImageSource>) -> Self {
224 ImageViewer {
225 source: source.into(),
226 layout: LayoutData::default(),
227 image_data: ImageData::default(),
228 accessibility: AccessibilityData::default(),
229 effect: EffectData::default(),
230 corner_radius: None,
231 children: Vec::new(),
232 loading_placeholder: None,
233 error_renderer: None,
234 key: DiffKey::None,
235 }
236 }
237}
238
239impl KeyExt for ImageViewer {
240 fn write_key(&mut self) -> &mut DiffKey {
241 &mut self.key
242 }
243}
244
245impl LayoutExt for ImageViewer {
246 fn get_layout(&mut self) -> &mut LayoutData {
247 &mut self.layout
248 }
249}
250
251impl ContainerSizeExt for ImageViewer {}
252impl ContainerWithContentExt for ImageViewer {}
253
254impl ImageExt for ImageViewer {
255 fn get_image_data(&mut self) -> &mut ImageData {
256 &mut self.image_data
257 }
258}
259
260impl AccessibilityExt for ImageViewer {
261 fn get_accessibility_data(&mut self) -> &mut AccessibilityData {
262 &mut self.accessibility
263 }
264}
265
266impl ChildrenExt for ImageViewer {
267 fn get_children(&mut self) -> &mut Vec<Element> {
268 &mut self.children
269 }
270}
271
272impl EffectExt for ImageViewer {
273 fn get_effect(&mut self) -> &mut EffectData {
274 &mut self.effect
275 }
276}
277
278impl ImageViewer {
279 pub fn corner_radius(mut self, corner_radius: impl Into<CornerRadius>) -> Self {
280 self.corner_radius = Some(corner_radius.into());
281 self
282 }
283
284 pub fn loading_placeholder(mut self, placeholder: impl Into<Element>) -> Self {
286 self.loading_placeholder = Some(placeholder.into());
287 self
288 }
289
290 pub fn error_renderer(mut self, renderer: impl Into<Callback<String, Element>>) -> Self {
292 self.error_renderer = Some(renderer.into());
293 self
294 }
295}
296
297impl Component for ImageViewer {
298 fn render(&self) -> impl IntoElement {
299 let asset_config = AssetConfiguration::new(&self.source, AssetAge::default());
300 let asset = use_asset(&asset_config);
301 let mut asset_cacher = use_hook(AssetCacher::get);
302
303 use_side_effect_with_deps(
304 &(self.source.clone(), asset_config),
305 move |(source, asset_config): &(ImageSource, AssetConfiguration)| {
306 if matches!(
309 asset_cacher.read_asset(asset_config),
310 Some(Asset::Pending) | Some(Asset::Error(_))
311 ) {
312 asset_cacher.update_asset(asset_config.clone(), Asset::Loading);
313
314 let source = source.clone();
315 let asset_config = asset_config.clone();
316 spawn_forever(async move {
317 match source.bytes().await {
318 Ok((image, bytes)) => {
319 let image_holder = ImageHolder {
321 bytes,
322 image: Rc::new(RefCell::new(image)),
323 };
324 asset_cacher.update_asset(
325 asset_config,
326 Asset::Cached(Rc::new(image_holder)),
327 );
328 }
329 Err(err) => {
330 asset_cacher
332 .update_asset(asset_config, Asset::Error(err.to_string()));
333 }
334 }
335 });
336 }
337 },
338 );
339
340 match asset {
341 Asset::Cached(asset) => {
342 let asset = asset.downcast_ref::<ImageHolder>().unwrap().clone();
343 image(asset)
344 .accessibility(self.accessibility.clone())
345 .a11y_role(AccessibilityRole::Image)
346 .a11y_focusable(true)
347 .layout(self.layout.clone())
348 .image_data(self.image_data.clone())
349 .effect(self.effect.clone())
350 .children(self.children.clone())
351 .map(self.corner_radius, |img, corner_radius| {
352 img.corner_radius(corner_radius)
353 })
354 .into_element()
355 }
356 Asset::Pending | Asset::Loading => rect()
357 .layout(self.layout.clone())
358 .center()
359 .child(
360 self.loading_placeholder
361 .clone()
362 .unwrap_or_else(|| CircularLoader::new().into_element()),
363 )
364 .into(),
365 Asset::Error(err) => match &self.error_renderer {
366 Some(renderer) => renderer.call(err),
367 None => err.into(),
368 },
369 }
370 }
371
372 fn render_key(&self) -> DiffKey {
373 self.key.clone().or(self.default_key())
374 }
375}