1use crate::{
2 AbsoluteLength, AnyElement, AnyImageCache, App, Asset, AssetLogger, Bounds, DefiniteLength,
3 Element, ElementId, Entity, GlobalElementId, Hitbox, Image, ImageCache, InteractiveElement,
4 Interactivity, IntoElement, LayoutId, Length, ObjectFit, Pixels, RenderImage, Resource,
5 SMOOTH_SVG_SCALE_FACTOR, SharedString, SharedUri, StyleRefinement, Styled, SvgSize, Task,
6 Window, px, swap_rgba_pa_to_bgra,
7};
8use anyhow::{Result, anyhow};
9
10use futures::{AsyncReadExt, Future};
11use image::{
12 AnimationDecoder, DynamicImage, Frame, ImageBuffer, ImageError, ImageFormat, Rgba,
13 codecs::{gif::GifDecoder, webp::WebPDecoder},
14};
15use smallvec::SmallVec;
16use std::{
17 fs,
18 io::{self, Cursor},
19 ops::{Deref, DerefMut},
20 path::{Path, PathBuf},
21 str::FromStr,
22 sync::Arc,
23 time::{Duration, Instant},
24};
25use thiserror::Error;
26use util::ResultExt;
27
28use super::{FocusableElement, Stateful, StatefulInteractiveElement};
29
30/// The delay before showing the loading state.
31pub const LOADING_DELAY: Duration = Duration::from_millis(200);
32
33/// A type alias to the resource loader that the `img()` element uses.
34///
35/// Note: that this is only for Resources, like URLs or file paths.
36/// Custom loaders, or external images will not use this asset loader
37pub type ImgResourceLoader = AssetLogger<ImageAssetLoader>;
38
39/// A source of image content.
40#[derive(Clone)]
41pub enum ImageSource {
42 /// The image content will be loaded from some resource location
43 Resource(Resource),
44 /// Cached image data
45 Render(Arc<RenderImage>),
46 /// Cached image data
47 Image(Arc<Image>),
48 /// A custom loading function to use
49 Custom(Arc<dyn Fn(&mut Window, &mut App) -> Option<Result<Arc<RenderImage>, ImageCacheError>>>),
50}
51
52fn is_uri(uri: &str) -> bool {
53 http_client::Uri::from_str(uri).is_ok()
54}
55
56impl From<SharedUri> for ImageSource {
57 fn from(value: SharedUri) -> Self {
58 Self::Resource(Resource::Uri(value))
59 }
60}
61
62impl<'a> From<&'a str> for ImageSource {
63 fn from(s: &'a str) -> Self {
64 if is_uri(s) {
65 Self::Resource(Resource::Uri(s.to_string().into()))
66 } else {
67 Self::Resource(Resource::Embedded(s.to_string().into()))
68 }
69 }
70}
71
72impl From<String> for ImageSource {
73 fn from(s: String) -> Self {
74 if is_uri(&s) {
75 Self::Resource(Resource::Uri(s.into()))
76 } else {
77 Self::Resource(Resource::Embedded(s.into()))
78 }
79 }
80}
81
82impl From<SharedString> for ImageSource {
83 fn from(s: SharedString) -> Self {
84 s.as_ref().into()
85 }
86}
87
88impl From<&Path> for ImageSource {
89 fn from(value: &Path) -> Self {
90 Self::Resource(value.to_path_buf().into())
91 }
92}
93
94impl From<Arc<Path>> for ImageSource {
95 fn from(value: Arc<Path>) -> Self {
96 Self::Resource(value.into())
97 }
98}
99
100impl From<PathBuf> for ImageSource {
101 fn from(value: PathBuf) -> Self {
102 Self::Resource(value.into())
103 }
104}
105
106impl From<Arc<RenderImage>> for ImageSource {
107 fn from(value: Arc<RenderImage>) -> Self {
108 Self::Render(value)
109 }
110}
111
112impl From<Arc<Image>> for ImageSource {
113 fn from(value: Arc<Image>) -> Self {
114 Self::Image(value)
115 }
116}
117
118impl<F> From<F> for ImageSource
119where
120 F: Fn(&mut Window, &mut App) -> Option<Result<Arc<RenderImage>, ImageCacheError>> + 'static,
121{
122 fn from(value: F) -> Self {
123 Self::Custom(Arc::new(value))
124 }
125}
126
127/// The style of an image element.
128pub struct ImageStyle {
129 grayscale: bool,
130 object_fit: ObjectFit,
131 loading: Option<Box<dyn Fn() -> AnyElement>>,
132 fallback: Option<Box<dyn Fn() -> AnyElement>>,
133}
134
135impl Default for ImageStyle {
136 fn default() -> Self {
137 Self {
138 grayscale: false,
139 object_fit: ObjectFit::Contain,
140 loading: None,
141 fallback: None,
142 }
143 }
144}
145
146/// Style an image element.
147pub trait StyledImage: Sized {
148 /// Get a mutable [ImageStyle] from the element.
149 fn image_style(&mut self) -> &mut ImageStyle;
150
151 /// Set the image to be displayed in grayscale.
152 fn grayscale(mut self, grayscale: bool) -> Self {
153 self.image_style().grayscale = grayscale;
154 self
155 }
156
157 /// Set the object fit for the image.
158 fn object_fit(mut self, object_fit: ObjectFit) -> Self {
159 self.image_style().object_fit = object_fit;
160 self
161 }
162
163 /// Set the object fit for the image.
164 fn with_fallback(mut self, fallback: impl Fn() -> AnyElement + 'static) -> Self {
165 self.image_style().fallback = Some(Box::new(fallback));
166 self
167 }
168
169 /// Set the object fit for the image.
170 fn with_loading(mut self, loading: impl Fn() -> AnyElement + 'static) -> Self {
171 self.image_style().loading = Some(Box::new(loading));
172 self
173 }
174}
175
176impl StyledImage for Img {
177 fn image_style(&mut self) -> &mut ImageStyle {
178 &mut self.style
179 }
180}
181
182impl StyledImage for Stateful<Img> {
183 fn image_style(&mut self) -> &mut ImageStyle {
184 &mut self.element.style
185 }
186}
187
188/// An image element.
189pub struct Img {
190 interactivity: Interactivity,
191 source: ImageSource,
192 style: ImageStyle,
193 image_cache: Option<AnyImageCache>,
194}
195
196/// Create a new image element.
197pub fn img(source: impl Into<ImageSource>) -> Img {
198 Img {
199 interactivity: Interactivity::default(),
200 source: source.into(),
201 style: ImageStyle::default(),
202 image_cache: None,
203 }
204}
205
206impl Img {
207 /// A list of all format extensions currently supported by this img element
208 pub fn extensions() -> &'static [&'static str] {
209 // This is the list in [image::ImageFormat::from_extension] + `svg`
210 &[
211 "avif", "jpg", "jpeg", "png", "gif", "webp", "tif", "tiff", "tga", "dds", "bmp", "ico",
212 "hdr", "exr", "pbm", "pam", "ppm", "pgm", "ff", "farbfeld", "qoi", "svg",
213 ]
214 }
215
216 /// Sets the image cache for the current node.
217 ///
218 /// If the `image_cache` is not explicitly provided, the function will determine the image cache by:
219 ///
220 /// 1. Checking if any ancestor node of the current node contains an `ImageCacheElement`, If such a node exists, the image cache specified by that ancestor will be used.
221 /// 2. If no ancestor node contains an `ImageCacheElement`, the global image cache will be used as a fallback.
222 ///
223 /// This mechanism provides a flexible way to manage image caching, allowing precise control when needed,
224 /// while ensuring a default behavior when no cache is explicitly specified.
225 #[inline]
226 pub fn image_cache<I: ImageCache>(self, image_cache: &Entity<I>) -> Self {
227 Self {
228 image_cache: Some(image_cache.clone().into()),
229 ..self
230 }
231 }
232}
233
234impl Deref for Stateful<Img> {
235 type Target = Img;
236
237 fn deref(&self) -> &Self::Target {
238 &self.element
239 }
240}
241
242impl DerefMut for Stateful<Img> {
243 fn deref_mut(&mut self) -> &mut Self::Target {
244 &mut self.element
245 }
246}
247
248/// The image state between frames
249struct ImgState {
250 frame_index: usize,
251 last_frame_time: Option<Instant>,
252 started_loading: Option<(Instant, Task<()>)>,
253}
254
255/// The image layout state between frames
256pub struct ImgLayoutState {
257 frame_index: usize,
258 replacement: Option<AnyElement>,
259}
260
261impl Element for Img {
262 type RequestLayoutState = ImgLayoutState;
263 type PrepaintState = Option<Hitbox>;
264
265 fn id(&self) -> Option<ElementId> {
266 self.interactivity.element_id.clone()
267 }
268
269 fn request_layout(
270 &mut self,
271 global_id: Option<&GlobalElementId>,
272 window: &mut Window,
273 cx: &mut App,
274 ) -> (LayoutId, Self::RequestLayoutState) {
275 let mut layout_state = ImgLayoutState {
276 frame_index: 0,
277 replacement: None,
278 };
279
280 window.with_optional_element_state(global_id, |state, window| {
281 let mut state = state.map(|state| {
282 state.unwrap_or(ImgState {
283 frame_index: 0,
284 last_frame_time: None,
285 started_loading: 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.interactivity.request_layout(
292 global_id,
293 window,
294 cx,
295 |mut style, window, cx| {
296 let mut replacement_id = None;
297
298 match self.source.use_data(
299 self.image_cache
300 .clone()
301 .or_else(|| window.image_cache_stack.last().cloned()),
302 window,
303 cx,
304 ) {
305 Some(Ok(data)) => {
306 if let Some(state) = &mut state {
307 let frame_count = data.frame_count();
308 if frame_count > 1 {
309 let current_time = Instant::now();
310 if let Some(last_frame_time) = state.last_frame_time {
311 let elapsed = current_time - last_frame_time;
312 let frame_duration =
313 Duration::from(data.delay(state.frame_index));
314
315 if elapsed >= frame_duration {
316 state.frame_index =
317 (state.frame_index + 1) % frame_count;
318 state.last_frame_time =
319 Some(current_time - (elapsed - frame_duration));
320 }
321 } else {
322 state.last_frame_time = Some(current_time);
323 }
324 }
325 state.started_loading = None;
326 }
327
328 let image_size = data.size(frame_index);
329 style.aspect_ratio =
330 Some(image_size.width.0 as f32 / image_size.height.0 as f32);
331
332 if let Length::Auto = style.size.width {
333 style.size.width = match style.size.height {
334 Length::Definite(DefiniteLength::Absolute(
335 AbsoluteLength::Pixels(height),
336 )) => Length::Definite(
337 px(image_size.width.0 as f32 * height.0
338 / image_size.height.0 as f32)
339 .into(),
340 ),
341 _ => Length::Definite(px(image_size.width.0 as f32).into()),
342 };
343 }
344
345 if let Length::Auto = style.size.height {
346 style.size.height = match style.size.width {
347 Length::Definite(DefiniteLength::Absolute(
348 AbsoluteLength::Pixels(width),
349 )) => Length::Definite(
350 px(image_size.height.0 as f32 * width.0
351 / image_size.width.0 as f32)
352 .into(),
353 ),
354 _ => Length::Definite(px(image_size.height.0 as f32).into()),
355 };
356 }
357
358 if global_id.is_some() && data.frame_count() > 1 {
359 window.request_animation_frame();
360 }
361 }
362 Some(_err) => {
363 if let Some(fallback) = self.style.fallback.as_ref() {
364 let mut element = fallback();
365 replacement_id = Some(element.request_layout(window, cx));
366 layout_state.replacement = Some(element);
367 }
368 if let Some(state) = &mut state {
369 state.started_loading = None;
370 }
371 }
372 None => {
373 if let Some(state) = &mut state {
374 if let Some((started_loading, _)) = state.started_loading {
375 if started_loading.elapsed() > LOADING_DELAY {
376 if let Some(loading) = self.style.loading.as_ref() {
377 let mut element = loading();
378 replacement_id =
379 Some(element.request_layout(window, cx));
380 layout_state.replacement = Some(element);
381 }
382 }
383 } else {
384 let current_view = window.current_view();
385 let task = window.spawn(cx, async move |cx| {
386 cx.background_executor().timer(LOADING_DELAY).await;
387 cx.update(move |_, cx| {
388 cx.notify(current_view);
389 })
390 .ok();
391 });
392 state.started_loading = Some((Instant::now(), task));
393 }
394 }
395 }
396 }
397
398 window.request_layout(style, replacement_id, cx)
399 },
400 );
401
402 layout_state.frame_index = frame_index;
403
404 ((layout_id, layout_state), state)
405 })
406 }
407
408 fn prepaint(
409 &mut self,
410 global_id: Option<&GlobalElementId>,
411 bounds: Bounds<Pixels>,
412 request_layout: &mut Self::RequestLayoutState,
413 window: &mut Window,
414 cx: &mut App,
415 ) -> Self::PrepaintState {
416 self.interactivity.prepaint(
417 global_id,
418 bounds,
419 bounds.size,
420 window,
421 cx,
422 |_, _, hitbox, window, cx| {
423 if let Some(replacement) = &mut request_layout.replacement {
424 replacement.prepaint(window, cx);
425 }
426
427 hitbox
428 },
429 )
430 }
431
432 fn paint(
433 &mut self,
434 global_id: Option<&GlobalElementId>,
435 bounds: Bounds<Pixels>,
436 layout_state: &mut Self::RequestLayoutState,
437 hitbox: &mut Self::PrepaintState,
438 window: &mut Window,
439 cx: &mut App,
440 ) {
441 let source = self.source.clone();
442 self.interactivity.paint(
443 global_id,
444 bounds,
445 hitbox.as_ref(),
446 window,
447 cx,
448 |style, window, cx| {
449 if let Some(Ok(data)) = source.use_data(
450 self.image_cache
451 .clone()
452 .or_else(|| window.image_cache_stack.last().cloned()),
453 window,
454 cx,
455 ) {
456 let new_bounds = self
457 .style
458 .object_fit
459 .get_bounds(bounds, data.size(layout_state.frame_index));
460 let corner_radii = style
461 .corner_radii
462 .to_pixels(window.rem_size())
463 .clamp_radii_for_quad_size(new_bounds.size);
464 window
465 .paint_image(
466 new_bounds,
467 corner_radii,
468 data.clone(),
469 layout_state.frame_index,
470 self.style.grayscale,
471 )
472 .log_err();
473 } else if let Some(replacement) = &mut layout_state.replacement {
474 replacement.paint(window, cx);
475 }
476 },
477 )
478 }
479}
480
481impl Styled for Img {
482 fn style(&mut self) -> &mut StyleRefinement {
483 &mut self.interactivity.base_style
484 }
485}
486
487impl InteractiveElement for Img {
488 fn interactivity(&mut self) -> &mut Interactivity {
489 &mut self.interactivity
490 }
491}
492
493impl IntoElement for Img {
494 type Element = Self;
495
496 fn into_element(self) -> Self::Element {
497 self
498 }
499}
500
501impl FocusableElement for Img {}
502
503impl StatefulInteractiveElement for Img {}
504
505impl ImageSource {
506 pub(crate) fn use_data(
507 &self,
508 cache: Option<AnyImageCache>,
509 window: &mut Window,
510 cx: &mut App,
511 ) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
512 match self {
513 ImageSource::Resource(resource) => {
514 if let Some(cache) = cache {
515 cache.load(resource, window, cx)
516 } else {
517 window.use_asset::<ImgResourceLoader>(resource, cx)
518 }
519 }
520 ImageSource::Custom(loading_fn) => loading_fn(window, cx),
521 ImageSource::Render(data) => Some(Ok(data.to_owned())),
522 ImageSource::Image(data) => window.use_asset::<AssetLogger<ImageDecoder>>(data, cx),
523 }
524 }
525}
526
527#[derive(Clone)]
528enum ImageDecoder {}
529
530impl Asset for ImageDecoder {
531 type Source = Arc<Image>;
532 type Output = Result<Arc<RenderImage>, ImageCacheError>;
533
534 fn load(
535 source: Self::Source,
536 cx: &mut App,
537 ) -> impl Future<Output = Self::Output> + Send + 'static {
538 let renderer = cx.svg_renderer();
539 async move { source.to_image_data(renderer).map_err(Into::into) }
540 }
541}
542
543/// An image loader for the GPUI asset system
544#[derive(Clone)]
545pub enum ImageAssetLoader {}
546
547impl Asset for ImageAssetLoader {
548 type Source = Resource;
549 type Output = Result<Arc<RenderImage>, ImageCacheError>;
550
551 fn load(
552 source: Self::Source,
553 cx: &mut App,
554 ) -> impl Future<Output = Self::Output> + Send + 'static {
555 let client = cx.http_client();
556 // TODO: Can we make SVGs always rescale?
557 // let scale_factor = cx.scale_factor();
558 let svg_renderer = cx.svg_renderer();
559 let asset_source = cx.asset_source().clone();
560 async move {
561 let bytes = match source.clone() {
562 Resource::Path(uri) => fs::read(uri.as_ref())?,
563 Resource::Uri(uri) => {
564 let mut response = client
565 .get(uri.as_ref(), ().into(), true)
566 .await
567 .map_err(|e| anyhow!(e))?;
568 let mut body = Vec::new();
569 response.body_mut().read_to_end(&mut body).await?;
570 if !response.status().is_success() {
571 let mut body = String::from_utf8_lossy(&body).into_owned();
572 let first_line = body.lines().next().unwrap_or("").trim_end();
573 body.truncate(first_line.len());
574 return Err(ImageCacheError::BadStatus {
575 uri,
576 status: response.status(),
577 body,
578 });
579 }
580 body
581 }
582 Resource::Embedded(path) => {
583 let data = asset_source.load(&path).ok().flatten();
584 if let Some(data) = data {
585 data.to_vec()
586 } else {
587 return Err(ImageCacheError::Asset(
588 format!("Embedded resource not found: {}", path).into(),
589 ));
590 }
591 }
592 };
593
594 let data = if let Ok(format) = image::guess_format(&bytes) {
595 let data = match format {
596 ImageFormat::Gif => {
597 let decoder = GifDecoder::new(Cursor::new(&bytes))?;
598 let mut frames = SmallVec::new();
599
600 for frame in decoder.into_frames() {
601 let mut frame = frame?;
602 // Convert from RGBA to BGRA.
603 for pixel in frame.buffer_mut().chunks_exact_mut(4) {
604 pixel.swap(0, 2);
605 }
606 frames.push(frame);
607 }
608
609 frames
610 }
611 ImageFormat::WebP => {
612 let mut decoder = WebPDecoder::new(Cursor::new(&bytes))?;
613
614 if decoder.has_animation() {
615 let _ = decoder.set_background_color(Rgba([0, 0, 0, 0]));
616 let mut frames = SmallVec::new();
617
618 for frame in decoder.into_frames() {
619 let mut frame = frame?;
620 // Convert from RGBA to BGRA.
621 for pixel in frame.buffer_mut().chunks_exact_mut(4) {
622 pixel.swap(0, 2);
623 }
624 frames.push(frame);
625 }
626
627 frames
628 } else {
629 let mut data = DynamicImage::from_decoder(decoder)?.into_rgba8();
630
631 // Convert from RGBA to BGRA.
632 for pixel in data.chunks_exact_mut(4) {
633 pixel.swap(0, 2);
634 }
635
636 SmallVec::from_elem(Frame::new(data), 1)
637 }
638 }
639 _ => {
640 let mut data =
641 image::load_from_memory_with_format(&bytes, format)?.into_rgba8();
642
643 // Convert from RGBA to BGRA.
644 for pixel in data.chunks_exact_mut(4) {
645 pixel.swap(0, 2);
646 }
647
648 SmallVec::from_elem(Frame::new(data), 1)
649 }
650 };
651
652 RenderImage::new(data)
653 } else {
654 let pixmap =
655 // TODO: Can we make svgs always rescale?
656 svg_renderer.render_pixmap(&bytes, SvgSize::ScaleFactor(SMOOTH_SVG_SCALE_FACTOR))?;
657
658 let mut buffer =
659 ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take()).unwrap();
660
661 for pixel in buffer.chunks_exact_mut(4) {
662 swap_rgba_pa_to_bgra(pixel);
663 }
664
665 RenderImage::new(SmallVec::from_elem(Frame::new(buffer), 1))
666 };
667
668 Ok(Arc::new(data))
669 }
670 }
671}
672
673/// An error that can occur when interacting with the image cache.
674#[derive(Debug, Error, Clone)]
675pub enum ImageCacheError {
676 /// Some other kind of error occurred
677 #[error("error: {0}")]
678 Other(#[from] Arc<anyhow::Error>),
679 /// An error that occurred while reading the image from disk.
680 #[error("IO error: {0}")]
681 Io(Arc<std::io::Error>),
682 /// An error that occurred while processing an image.
683 #[error("unexpected http status for {uri}: {status}, body: {body}")]
684 BadStatus {
685 /// The URI of the image.
686 uri: SharedUri,
687 /// The HTTP status code.
688 status: http_client::StatusCode,
689 /// The HTTP response body.
690 body: String,
691 },
692 /// An error that occurred while processing an asset.
693 #[error("asset error: {0}")]
694 Asset(SharedString),
695 /// An error that occurred while processing an image.
696 #[error("image error: {0}")]
697 Image(Arc<ImageError>),
698 /// An error that occurred while processing an SVG.
699 #[error("svg error: {0}")]
700 Usvg(Arc<usvg::Error>),
701}
702
703impl From<anyhow::Error> for ImageCacheError {
704 fn from(value: anyhow::Error) -> Self {
705 Self::Other(Arc::new(value))
706 }
707}
708
709impl From<io::Error> for ImageCacheError {
710 fn from(value: io::Error) -> Self {
711 Self::Io(Arc::new(value))
712 }
713}
714
715impl From<usvg::Error> for ImageCacheError {
716 fn from(value: usvg::Error) -> Self {
717 Self::Usvg(Arc::new(value))
718 }
719}
720
721impl From<image::ImageError> for ImageCacheError {
722 fn from(value: image::ImageError) -> Self {
723 Self::Image(Arc::new(value))
724 }
725}