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)]
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 Ok((image, bytes))
168 })
169 .await
170 }
171}
172
173#[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 for asset_task in assets_tasks.write().drain(..) {
294 asset_task.cancel();
295 }
296
297 if matches!(
299 asset_cacher.read_asset(asset_config),
300 Some(Asset::Pending) | Some(Asset::Error(_))
301 ) {
302 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 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 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}