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)]
66pub enum ImageSource {
67 #[cfg(feature = "remote-asset")]
71 Uri(Uri),
72
73 Path(PathBuf),
74
75 Bytes(u64, Bytes),
76}
77
78impl From<(&'static str, Bytes)> for ImageSource {
79 fn from((id, bytes): (&'static str, Bytes)) -> Self {
80 let mut hasher = DefaultHasher::default();
81 id.hash(&mut hasher);
82 Self::Bytes(hasher.finish(), bytes)
83 }
84}
85
86impl From<(&'static str, &'static [u8])> for ImageSource {
87 fn from((id, bytes): (&'static str, &'static [u8])) -> Self {
88 let mut hasher = DefaultHasher::default();
89 id.hash(&mut hasher);
90 Self::Bytes(hasher.finish(), Bytes::from_static(bytes))
91 }
92}
93
94impl<const N: usize> From<(&'static str, &'static [u8; N])> for ImageSource {
95 fn from((id, bytes): (&'static str, &'static [u8; N])) -> Self {
96 let mut hasher = DefaultHasher::default();
97 id.hash(&mut hasher);
98 Self::Bytes(hasher.finish(), Bytes::from_static(bytes))
99 }
100}
101
102#[cfg_attr(feature = "docs", doc(cfg(feature = "remote-asset")))]
103#[cfg(feature = "remote-asset")]
104impl From<Uri> for ImageSource {
105 fn from(uri: Uri) -> Self {
106 Self::Uri(uri)
107 }
108}
109
110#[cfg_attr(feature = "docs", doc(cfg(feature = "remote-asset")))]
111#[cfg(feature = "remote-asset")]
112impl From<&'static str> for ImageSource {
113 fn from(src: &'static str) -> Self {
114 Self::Uri(Uri::from_static(src))
115 }
116}
117
118impl From<PathBuf> for ImageSource {
119 fn from(path: PathBuf) -> Self {
120 Self::Path(path)
121 }
122}
123
124impl Hash for ImageSource {
125 fn hash<H: Hasher>(&self, state: &mut H) {
126 match self {
127 #[cfg(feature = "remote-asset")]
128 Self::Uri(uri) => uri.hash(state),
129 Self::Path(path) => path.hash(state),
130 Self::Bytes(id, _) => id.hash(state),
131 }
132 }
133}
134
135impl ImageSource {
136 pub async fn bytes(&self) -> anyhow::Result<(SkImage, Bytes)> {
137 let source = self.clone();
138 blocking::unblock(move || {
139 let bytes = match source {
140 #[cfg(feature = "remote-asset")]
141 Self::Uri(uri) => ureq::get(uri)
142 .call()?
143 .body_mut()
144 .read_to_vec()
145 .map(Bytes::from)?,
146 Self::Path(path) => fs::read(path).map(Bytes::from)?,
147 Self::Bytes(_, bytes) => bytes.clone(),
148 };
149 let image = SkImage::from_encoded(unsafe { SkData::new_bytes(&bytes) })
150 .context("Failed to decode Image.")?;
151 Ok((image, bytes))
152 })
153 .await
154 }
155}
156
157#[cfg_attr(feature = "docs",
181 doc = embed_doc_image::embed_image!("image_viewer", "images/gallery_image_viewer.png")
182)]
183#[derive(PartialEq)]
184pub struct ImageViewer {
185 source: ImageSource,
186
187 layout: LayoutData,
188 image_data: ImageData,
189 accessibility: AccessibilityData,
190 effect: EffectData,
191 corner_radius: Option<CornerRadius>,
192
193 children: Vec<Element>,
194
195 key: DiffKey,
196}
197
198impl ImageViewer {
199 pub fn new(source: impl Into<ImageSource>) -> Self {
200 ImageViewer {
201 source: source.into(),
202 layout: LayoutData::default(),
203 image_data: ImageData::default(),
204 accessibility: AccessibilityData::default(),
205 effect: EffectData::default(),
206 corner_radius: None,
207 children: Vec::new(),
208 key: DiffKey::None,
209 }
210 }
211}
212
213impl KeyExt for ImageViewer {
214 fn write_key(&mut self) -> &mut DiffKey {
215 &mut self.key
216 }
217}
218
219impl LayoutExt for ImageViewer {
220 fn get_layout(&mut self) -> &mut LayoutData {
221 &mut self.layout
222 }
223}
224
225impl ContainerSizeExt for ImageViewer {}
226impl ContainerWithContentExt for ImageViewer {}
227
228impl ImageExt for ImageViewer {
229 fn get_image_data(&mut self) -> &mut ImageData {
230 &mut self.image_data
231 }
232}
233
234impl AccessibilityExt for ImageViewer {
235 fn get_accessibility_data(&mut self) -> &mut AccessibilityData {
236 &mut self.accessibility
237 }
238}
239
240impl ChildrenExt for ImageViewer {
241 fn get_children(&mut self) -> &mut Vec<Element> {
242 &mut self.children
243 }
244}
245
246impl EffectExt for ImageViewer {
247 fn get_effect(&mut self) -> &mut EffectData {
248 &mut self.effect
249 }
250}
251
252impl ImageViewer {
253 pub fn corner_radius(mut self, corner_radius: impl Into<CornerRadius>) -> Self {
254 self.corner_radius = Some(corner_radius.into());
255 self
256 }
257}
258
259impl Component for ImageViewer {
260 fn render(&self) -> impl IntoElement {
261 let asset_config = AssetConfiguration::new(&self.source, AssetAge::default());
262 let asset = use_asset(&asset_config);
263 let mut asset_cacher = use_hook(AssetCacher::get);
264 let mut assets_tasks = use_state::<Vec<TaskHandle>>(Vec::new);
265
266 use_side_effect_with_deps(&self.source, move |source| {
267 let source = source.clone();
268
269 for asset_task in assets_tasks.write().drain(..) {
271 asset_task.cancel();
272 }
273
274 if matches!(
276 asset_cacher.read_asset(&asset_config),
277 Some(Asset::Pending) | Some(Asset::Error(_))
278 ) {
279 asset_cacher.update_asset(asset_config.clone(), Asset::Loading);
281
282 let asset_config = asset_config.clone();
283 let asset_task = spawn(async move {
284 match source.bytes().await {
285 Ok((image, bytes)) => {
286 let image_holder = ImageHolder {
288 bytes,
289 image: Rc::new(RefCell::new(image)),
290 };
291 asset_cacher.update_asset(
292 asset_config.clone(),
293 Asset::Cached(Rc::new(image_holder)),
294 );
295 }
296 Err(err) => {
297 asset_cacher.update_asset(asset_config, Asset::Error(err.to_string()));
299 }
300 }
301 });
302
303 assets_tasks.write().push(asset_task);
304 }
305 });
306
307 match asset {
308 Asset::Cached(asset) => {
309 let asset = asset.downcast_ref::<ImageHolder>().unwrap().clone();
310 image(asset)
311 .accessibility(self.accessibility.clone())
312 .a11y_role(AccessibilityRole::Image)
313 .a11y_focusable(true)
314 .layout(self.layout.clone())
315 .image_data(self.image_data.clone())
316 .effect(self.effect.clone())
317 .children(self.children.clone())
318 .map(self.corner_radius, |img, corner_radius| {
319 img.corner_radius(corner_radius)
320 })
321 .into_element()
322 }
323 Asset::Pending | Asset::Loading => rect()
324 .layout(self.layout.clone())
325 .center()
326 .child(CircularLoader::new())
327 .into(),
328 Asset::Error(err) => err.into(),
329 }
330 }
331
332 fn render_key(&self) -> DiffKey {
333 self.key.clone().or(self.default_key())
334 }
335}