1use crate::{
2 point, px, size, AbsoluteLength, Asset, Bounds, DefiniteLength, DevicePixels, Element,
3 ElementId, GlobalElementId, Hitbox, ImageData, InteractiveElement, Interactivity, IntoElement,
4 LayoutId, Length, Pixels, SharedString, SharedUri, Size, StyleRefinement, Styled, SvgSize,
5 UriOrPath, WindowContext,
6};
7use futures::{AsyncReadExt, Future};
8use http_client;
9use image::{
10 codecs::gif::GifDecoder, AnimationDecoder, Frame, ImageBuffer, ImageError, ImageFormat,
11};
12#[cfg(target_os = "macos")]
13use media::core_video::CVImageBuffer;
14use smallvec::SmallVec;
15use std::{
16 fs,
17 io::Cursor,
18 path::PathBuf,
19 sync::Arc,
20 time::{Duration, Instant},
21};
22use thiserror::Error;
23use util::ResultExt;
24
25/// A source of image content.
26#[derive(Clone, Debug)]
27pub enum ImageSource {
28 /// Image content will be loaded from provided URI at render time.
29 Uri(SharedUri),
30 /// Image content will be loaded from the provided file at render time.
31 File(Arc<PathBuf>),
32 /// Cached image data
33 Data(Arc<ImageData>),
34 /// Image content will be loaded from Asset at render time.
35 Asset(SharedString),
36 // TODO: move surface definitions into mac platform module
37 /// A CoreVideo image buffer
38 #[cfg(target_os = "macos")]
39 Surface(CVImageBuffer),
40}
41
42fn is_uri(uri: &str) -> bool {
43 uri.contains("://")
44}
45
46impl From<SharedUri> for ImageSource {
47 fn from(value: SharedUri) -> Self {
48 Self::Uri(value)
49 }
50}
51
52impl From<&'static str> for ImageSource {
53 fn from(s: &'static str) -> Self {
54 if is_uri(&s) {
55 Self::Uri(s.into())
56 } else {
57 Self::Asset(s.into())
58 }
59 }
60}
61
62impl From<String> for ImageSource {
63 fn from(s: String) -> Self {
64 if is_uri(&s) {
65 Self::Uri(s.into())
66 } else {
67 Self::Asset(s.into())
68 }
69 }
70}
71
72impl From<SharedString> for ImageSource {
73 fn from(s: SharedString) -> Self {
74 if is_uri(&s) {
75 Self::Uri(s.into())
76 } else {
77 Self::Asset(s)
78 }
79 }
80}
81
82impl From<Arc<PathBuf>> for ImageSource {
83 fn from(value: Arc<PathBuf>) -> Self {
84 Self::File(value)
85 }
86}
87
88impl From<PathBuf> for ImageSource {
89 fn from(value: PathBuf) -> Self {
90 Self::File(value.into())
91 }
92}
93
94impl From<Arc<ImageData>> for ImageSource {
95 fn from(value: Arc<ImageData>) -> Self {
96 Self::Data(value)
97 }
98}
99
100#[cfg(target_os = "macos")]
101impl From<CVImageBuffer> for ImageSource {
102 fn from(value: CVImageBuffer) -> Self {
103 Self::Surface(value)
104 }
105}
106
107/// An image element.
108pub struct Img {
109 interactivity: Interactivity,
110 source: ImageSource,
111 grayscale: bool,
112 object_fit: ObjectFit,
113}
114
115/// Create a new image element.
116pub fn img(source: impl Into<ImageSource>) -> Img {
117 Img {
118 interactivity: Interactivity::default(),
119 source: source.into(),
120 grayscale: false,
121 object_fit: ObjectFit::Contain,
122 }
123}
124
125/// How to fit the image into the bounds of the element.
126pub enum ObjectFit {
127 /// The image will be stretched to fill the bounds of the element.
128 Fill,
129 /// The image will be scaled to fit within the bounds of the element.
130 Contain,
131 /// The image will be scaled to cover the bounds of the element.
132 Cover,
133 /// The image will be scaled down to fit within the bounds of the element.
134 ScaleDown,
135 /// The image will maintain its original size.
136 None,
137}
138
139impl ObjectFit {
140 /// Get the bounds of the image within the given bounds.
141 pub fn get_bounds(
142 &self,
143 bounds: Bounds<Pixels>,
144 image_size: Size<DevicePixels>,
145 ) -> Bounds<Pixels> {
146 let image_size = image_size.map(|dimension| Pixels::from(u32::from(dimension)));
147 let image_ratio = image_size.width / image_size.height;
148 let bounds_ratio = bounds.size.width / bounds.size.height;
149
150 let result_bounds = match self {
151 ObjectFit::Fill => bounds,
152 ObjectFit::Contain => {
153 let new_size = if bounds_ratio > image_ratio {
154 size(
155 image_size.width * (bounds.size.height / image_size.height),
156 bounds.size.height,
157 )
158 } else {
159 size(
160 bounds.size.width,
161 image_size.height * (bounds.size.width / image_size.width),
162 )
163 };
164
165 Bounds {
166 origin: point(
167 bounds.origin.x + (bounds.size.width - new_size.width) / 2.0,
168 bounds.origin.y + (bounds.size.height - new_size.height) / 2.0,
169 ),
170 size: new_size,
171 }
172 }
173 ObjectFit::ScaleDown => {
174 // Check if the image is larger than the bounds in either dimension.
175 if image_size.width > bounds.size.width || image_size.height > bounds.size.height {
176 // If the image is larger, use the same logic as Contain to scale it down.
177 let new_size = if bounds_ratio > image_ratio {
178 size(
179 image_size.width * (bounds.size.height / image_size.height),
180 bounds.size.height,
181 )
182 } else {
183 size(
184 bounds.size.width,
185 image_size.height * (bounds.size.width / image_size.width),
186 )
187 };
188
189 Bounds {
190 origin: point(
191 bounds.origin.x + (bounds.size.width - new_size.width) / 2.0,
192 bounds.origin.y + (bounds.size.height - new_size.height) / 2.0,
193 ),
194 size: new_size,
195 }
196 } else {
197 // If the image is smaller than or equal to the container, display it at its original size,
198 // centered within the container.
199 let original_size = size(image_size.width, image_size.height);
200 Bounds {
201 origin: point(
202 bounds.origin.x + (bounds.size.width - original_size.width) / 2.0,
203 bounds.origin.y + (bounds.size.height - original_size.height) / 2.0,
204 ),
205 size: original_size,
206 }
207 }
208 }
209 ObjectFit::Cover => {
210 let new_size = if bounds_ratio > image_ratio {
211 size(
212 bounds.size.width,
213 image_size.height * (bounds.size.width / image_size.width),
214 )
215 } else {
216 size(
217 image_size.width * (bounds.size.height / image_size.height),
218 bounds.size.height,
219 )
220 };
221
222 Bounds {
223 origin: point(
224 bounds.origin.x + (bounds.size.width - new_size.width) / 2.0,
225 bounds.origin.y + (bounds.size.height - new_size.height) / 2.0,
226 ),
227 size: new_size,
228 }
229 }
230 ObjectFit::None => Bounds {
231 origin: bounds.origin,
232 size: image_size,
233 },
234 };
235
236 result_bounds
237 }
238}
239
240impl Img {
241 /// A list of all format extensions currently supported by this img element
242 pub fn extensions() -> &'static [&'static str] {
243 // This is the list in [image::ImageFormat::from_extension] + `svg`
244 &[
245 "avif", "jpg", "jpeg", "png", "gif", "webp", "tif", "tiff", "tga", "dds", "bmp", "ico",
246 "hdr", "exr", "pbm", "pam", "ppm", "pgm", "ff", "farbfeld", "qoi", "svg",
247 ]
248 }
249
250 /// Set the image to be displayed in grayscale.
251 pub fn grayscale(mut self, grayscale: bool) -> Self {
252 self.grayscale = grayscale;
253 self
254 }
255 /// Set the object fit for the image.
256 pub fn object_fit(mut self, object_fit: ObjectFit) -> Self {
257 self.object_fit = object_fit;
258 self
259 }
260}
261
262/// The image state between frames
263struct ImgState {
264 frame_index: usize,
265 last_frame_time: Option<Instant>,
266}
267
268impl Element for Img {
269 type RequestLayoutState = usize;
270 type PrepaintState = Option<Hitbox>;
271
272 fn id(&self) -> Option<ElementId> {
273 self.interactivity.element_id.clone()
274 }
275
276 fn request_layout(
277 &mut self,
278 global_id: Option<&GlobalElementId>,
279 cx: &mut WindowContext,
280 ) -> (LayoutId, Self::RequestLayoutState) {
281 cx.with_optional_element_state(global_id, |state, cx| {
282 let mut state = state.map(|state| {
283 state.unwrap_or(ImgState {
284 frame_index: 0,
285 last_frame_time: None,
286 })
287 });
288
289 let frame_index = state.as_ref().map(|state| state.frame_index).unwrap_or(0);
290
291 let layout_id = self
292 .interactivity
293 .request_layout(global_id, cx, |mut style, cx| {
294 if let Some(data) = self.source.data(cx) {
295 if let Some(state) = &mut state {
296 let frame_count = data.frame_count();
297 if frame_count > 1 {
298 let current_time = Instant::now();
299 if let Some(last_frame_time) = state.last_frame_time {
300 let elapsed = current_time - last_frame_time;
301 let frame_duration =
302 Duration::from(data.delay(state.frame_index));
303
304 if elapsed >= frame_duration {
305 state.frame_index = (state.frame_index + 1) % frame_count;
306 state.last_frame_time =
307 Some(current_time - (elapsed - frame_duration));
308 }
309 } else {
310 state.last_frame_time = Some(current_time);
311 }
312 }
313 }
314
315 let image_size = data.size(frame_index);
316 match (style.size.width, style.size.height) {
317 (Length::Auto, Length::Auto) => {
318 style.size = Size {
319 width: Length::Definite(DefiniteLength::Absolute(
320 AbsoluteLength::Pixels(px(image_size.width.0 as f32)),
321 )),
322 height: Length::Definite(DefiniteLength::Absolute(
323 AbsoluteLength::Pixels(px(image_size.height.0 as f32)),
324 )),
325 }
326 }
327 _ => {}
328 }
329
330 if global_id.is_some() && data.frame_count() > 1 {
331 cx.request_animation_frame();
332 }
333 }
334
335 cx.request_layout(style, [])
336 });
337
338 ((layout_id, frame_index), state)
339 })
340 }
341
342 fn prepaint(
343 &mut self,
344 global_id: Option<&GlobalElementId>,
345 bounds: Bounds<Pixels>,
346 _request_layout: &mut Self::RequestLayoutState,
347 cx: &mut WindowContext,
348 ) -> Option<Hitbox> {
349 self.interactivity
350 .prepaint(global_id, bounds, bounds.size, cx, |_, _, hitbox, _| hitbox)
351 }
352
353 fn paint(
354 &mut self,
355 global_id: Option<&GlobalElementId>,
356 bounds: Bounds<Pixels>,
357 frame_index: &mut Self::RequestLayoutState,
358 hitbox: &mut Self::PrepaintState,
359 cx: &mut WindowContext,
360 ) {
361 let source = self.source.clone();
362 self.interactivity
363 .paint(global_id, bounds, hitbox.as_ref(), cx, |style, cx| {
364 let corner_radii = style.corner_radii.to_pixels(bounds.size, cx.rem_size());
365
366 if let Some(data) = source.data(cx) {
367 let new_bounds = self.object_fit.get_bounds(bounds, data.size(*frame_index));
368 cx.paint_image(
369 new_bounds,
370 corner_radii,
371 data.clone(),
372 *frame_index,
373 self.grayscale,
374 )
375 .log_err();
376 }
377
378 match source {
379 #[cfg(target_os = "macos")]
380 ImageSource::Surface(surface) => {
381 let size = size(surface.width().into(), surface.height().into());
382 let new_bounds = self.object_fit.get_bounds(bounds, size);
383 // TODO: Add support for corner_radii and grayscale.
384 cx.paint_surface(new_bounds, surface);
385 }
386 _ => {}
387 }
388 })
389 }
390}
391
392impl IntoElement for Img {
393 type Element = Self;
394
395 fn into_element(self) -> Self::Element {
396 self
397 }
398}
399
400impl Styled for Img {
401 fn style(&mut self) -> &mut StyleRefinement {
402 &mut self.interactivity.base_style
403 }
404}
405
406impl InteractiveElement for Img {
407 fn interactivity(&mut self) -> &mut Interactivity {
408 &mut self.interactivity
409 }
410}
411
412impl ImageSource {
413 fn data(&self, cx: &mut WindowContext) -> Option<Arc<ImageData>> {
414 match self {
415 ImageSource::Uri(_) | ImageSource::Asset(_) | ImageSource::File(_) => {
416 let uri_or_path: UriOrPath = match self {
417 ImageSource::Uri(uri) => uri.clone().into(),
418 ImageSource::File(path) => path.clone().into(),
419 ImageSource::Asset(path) => UriOrPath::Asset(path.clone()),
420 _ => unreachable!(),
421 };
422
423 cx.use_cached_asset::<Image>(&uri_or_path)?.log_err()
424 }
425
426 ImageSource::Data(data) => Some(data.to_owned()),
427 #[cfg(target_os = "macos")]
428 ImageSource::Surface(_) => None,
429 }
430 }
431}
432
433#[derive(Clone)]
434enum Image {}
435
436impl Asset for Image {
437 type Source = UriOrPath;
438 type Output = Result<Arc<ImageData>, ImageCacheError>;
439
440 fn load(
441 source: Self::Source,
442 cx: &mut WindowContext,
443 ) -> impl Future<Output = Self::Output> + Send + 'static {
444 let client = cx.http_client();
445 let scale_factor = cx.scale_factor();
446 let svg_renderer = cx.svg_renderer();
447 let asset_source = cx.asset_source().clone();
448 async move {
449 let bytes = match source.clone() {
450 UriOrPath::Path(uri) => fs::read(uri.as_ref())?,
451 UriOrPath::Uri(uri) => {
452 let mut response = client.get(uri.as_ref(), ().into(), true).await?;
453 let mut body = Vec::new();
454 response.body_mut().read_to_end(&mut body).await?;
455 if !response.status().is_success() {
456 return Err(ImageCacheError::BadStatus {
457 uri,
458 status: response.status(),
459 body: String::from_utf8_lossy(&body).into_owned(),
460 });
461 }
462 body
463 }
464 UriOrPath::Asset(path) => {
465 let data = asset_source.load(&path).ok().flatten();
466 if let Some(data) = data {
467 data.to_vec()
468 } else {
469 return Err(ImageCacheError::Asset(
470 format!("not found: {}", path).into(),
471 ));
472 }
473 }
474 };
475
476 let data = if let Ok(format) = image::guess_format(&bytes) {
477 let data = match format {
478 ImageFormat::Gif => {
479 let decoder = GifDecoder::new(Cursor::new(&bytes))?;
480 let mut frames = SmallVec::new();
481
482 for frame in decoder.into_frames() {
483 let mut frame = frame?;
484 // Convert from RGBA to BGRA.
485 for pixel in frame.buffer_mut().chunks_exact_mut(4) {
486 pixel.swap(0, 2);
487 }
488 frames.push(frame);
489 }
490
491 frames
492 }
493 _ => {
494 let mut data =
495 image::load_from_memory_with_format(&bytes, format)?.into_rgba8();
496
497 // Convert from RGBA to BGRA.
498 for pixel in data.chunks_exact_mut(4) {
499 pixel.swap(0, 2);
500 }
501
502 SmallVec::from_elem(Frame::new(data), 1)
503 }
504 };
505
506 ImageData::new(data)
507 } else {
508 let pixmap =
509 svg_renderer.render_pixmap(&bytes, SvgSize::ScaleFactor(scale_factor))?;
510
511 let buffer =
512 ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take()).unwrap();
513
514 ImageData::new(SmallVec::from_elem(Frame::new(buffer), 1))
515 };
516
517 Ok(Arc::new(data))
518 }
519 }
520}
521
522/// An error that can occur when interacting with the image cache.
523#[derive(Debug, Error, Clone)]
524pub enum ImageCacheError {
525 /// An error that occurred while fetching an image from a remote source.
526 #[error("http error: {0}")]
527 Client(#[from] http_client::Error),
528 /// An error that occurred while reading the image from disk.
529 #[error("IO error: {0}")]
530 Io(Arc<std::io::Error>),
531 /// An error that occurred while processing an image.
532 #[error("unexpected http status for {uri}: {status}, body: {body}")]
533 BadStatus {
534 /// The URI of the image.
535 uri: SharedUri,
536 /// The HTTP status code.
537 status: http_client::StatusCode,
538 /// The HTTP response body.
539 body: String,
540 },
541 /// An error that occurred while processing an asset.
542 #[error("asset error: {0}")]
543 Asset(SharedString),
544 /// An error that occurred while processing an image.
545 #[error("image error: {0}")]
546 Image(Arc<ImageError>),
547 /// An error that occurred while processing an SVG.
548 #[error("svg error: {0}")]
549 Usvg(Arc<usvg::Error>),
550}
551
552impl From<std::io::Error> for ImageCacheError {
553 fn from(error: std::io::Error) -> Self {
554 Self::Io(Arc::new(error))
555 }
556}
557
558impl From<ImageError> for ImageCacheError {
559 fn from(error: ImageError) -> Self {
560 Self::Image(Arc::new(error))
561 }
562}
563
564impl From<usvg::Error> for ImageCacheError {
565 fn from(error: usvg::Error) -> Self {
566 Self::Usvg(Arc::new(error))
567 }
568}