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