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