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::{Context as _, Result};
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 pub(crate) fn get_data(
527 &self,
528 cache: Option<AnyImageCache>,
529 window: &mut Window,
530 cx: &mut App,
531 ) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
532 match self {
533 ImageSource::Resource(resource) => {
534 if let Some(cache) = cache {
535 cache.load(resource, window, cx)
536 } else {
537 window.get_asset::<ImgResourceLoader>(resource, cx)
538 }
539 }
540 ImageSource::Custom(loading_fn) => loading_fn(window, cx),
541 ImageSource::Render(data) => Some(Ok(data.to_owned())),
542 ImageSource::Image(data) => window.get_asset::<AssetLogger<ImageDecoder>>(data, cx),
543 }
544 }
545
546 /// Remove this image source from the asset system
547 pub fn remove_asset(&self, cx: &mut App) {
548 match self {
549 ImageSource::Resource(resource) => {
550 cx.remove_asset::<ImgResourceLoader>(resource);
551 }
552 ImageSource::Custom(_) | ImageSource::Render(_) => {}
553 ImageSource::Image(data) => cx.remove_asset::<AssetLogger<ImageDecoder>>(data),
554 }
555 }
556}
557
558#[derive(Clone)]
559enum ImageDecoder {}
560
561impl Asset for ImageDecoder {
562 type Source = Arc<Image>;
563 type Output = Result<Arc<RenderImage>, ImageCacheError>;
564
565 fn load(
566 source: Self::Source,
567 cx: &mut App,
568 ) -> impl Future<Output = Self::Output> + Send + 'static {
569 let renderer = cx.svg_renderer();
570 async move { source.to_image_data(renderer).map_err(Into::into) }
571 }
572}
573
574/// An image loader for the GPUI asset system
575#[derive(Clone)]
576pub enum ImageAssetLoader {}
577
578impl Asset for ImageAssetLoader {
579 type Source = Resource;
580 type Output = Result<Arc<RenderImage>, ImageCacheError>;
581
582 fn load(
583 source: Self::Source,
584 cx: &mut App,
585 ) -> impl Future<Output = Self::Output> + Send + 'static {
586 let client = cx.http_client();
587 // TODO: Can we make SVGs always rescale?
588 // let scale_factor = cx.scale_factor();
589 let svg_renderer = cx.svg_renderer();
590 let asset_source = cx.asset_source().clone();
591 async move {
592 let bytes = match source.clone() {
593 Resource::Path(uri) => fs::read(uri.as_ref())?,
594 Resource::Uri(uri) => {
595 let mut response = client
596 .get(uri.as_ref(), ().into(), true)
597 .await
598 .with_context(|| format!("loading image asset from {uri:?}"))?;
599 let mut body = Vec::new();
600 response.body_mut().read_to_end(&mut body).await?;
601 if !response.status().is_success() {
602 let mut body = String::from_utf8_lossy(&body).into_owned();
603 let first_line = body.lines().next().unwrap_or("").trim_end();
604 body.truncate(first_line.len());
605 return Err(ImageCacheError::BadStatus {
606 uri,
607 status: response.status(),
608 body,
609 });
610 }
611 body
612 }
613 Resource::Embedded(path) => {
614 let data = asset_source.load(&path).ok().flatten();
615 if let Some(data) = data {
616 data.to_vec()
617 } else {
618 return Err(ImageCacheError::Asset(
619 format!("Embedded resource not found: {}", path).into(),
620 ));
621 }
622 }
623 };
624
625 let data = if let Ok(format) = image::guess_format(&bytes) {
626 let data = match format {
627 ImageFormat::Gif => {
628 let decoder = GifDecoder::new(Cursor::new(&bytes))?;
629 let mut frames = SmallVec::new();
630
631 for frame in decoder.into_frames() {
632 let mut frame = frame?;
633 // Convert from RGBA to BGRA.
634 for pixel in frame.buffer_mut().chunks_exact_mut(4) {
635 pixel.swap(0, 2);
636 }
637 frames.push(frame);
638 }
639
640 frames
641 }
642 ImageFormat::WebP => {
643 let mut decoder = WebPDecoder::new(Cursor::new(&bytes))?;
644
645 if decoder.has_animation() {
646 let _ = decoder.set_background_color(Rgba([0, 0, 0, 0]));
647 let mut frames = SmallVec::new();
648
649 for frame in decoder.into_frames() {
650 let mut frame = frame?;
651 // Convert from RGBA to BGRA.
652 for pixel in frame.buffer_mut().chunks_exact_mut(4) {
653 pixel.swap(0, 2);
654 }
655 frames.push(frame);
656 }
657
658 frames
659 } else {
660 let mut data = DynamicImage::from_decoder(decoder)?.into_rgba8();
661
662 // Convert from RGBA to BGRA.
663 for pixel in data.chunks_exact_mut(4) {
664 pixel.swap(0, 2);
665 }
666
667 SmallVec::from_elem(Frame::new(data), 1)
668 }
669 }
670 _ => {
671 let mut data =
672 image::load_from_memory_with_format(&bytes, format)?.into_rgba8();
673
674 // Convert from RGBA to BGRA.
675 for pixel in data.chunks_exact_mut(4) {
676 pixel.swap(0, 2);
677 }
678
679 SmallVec::from_elem(Frame::new(data), 1)
680 }
681 };
682
683 RenderImage::new(data)
684 } else {
685 let pixmap =
686 // TODO: Can we make svgs always rescale?
687 svg_renderer.render_pixmap(&bytes, SvgSize::ScaleFactor(SMOOTH_SVG_SCALE_FACTOR))?;
688
689 let mut buffer =
690 ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take()).unwrap();
691
692 for pixel in buffer.chunks_exact_mut(4) {
693 swap_rgba_pa_to_bgra(pixel);
694 }
695
696 RenderImage::new(SmallVec::from_elem(Frame::new(buffer), 1))
697 };
698
699 Ok(Arc::new(data))
700 }
701 }
702}
703
704/// An error that can occur when interacting with the image cache.
705#[derive(Debug, Error, Clone)]
706pub enum ImageCacheError {
707 /// Some other kind of error occurred
708 #[error("error: {0}")]
709 Other(#[from] Arc<anyhow::Error>),
710 /// An error that occurred while reading the image from disk.
711 #[error("IO error: {0}")]
712 Io(Arc<std::io::Error>),
713 /// An error that occurred while processing an image.
714 #[error("unexpected http status for {uri}: {status}, body: {body}")]
715 BadStatus {
716 /// The URI of the image.
717 uri: SharedUri,
718 /// The HTTP status code.
719 status: http_client::StatusCode,
720 /// The HTTP response body.
721 body: String,
722 },
723 /// An error that occurred while processing an asset.
724 #[error("asset error: {0}")]
725 Asset(SharedString),
726 /// An error that occurred while processing an image.
727 #[error("image error: {0}")]
728 Image(Arc<ImageError>),
729 /// An error that occurred while processing an SVG.
730 #[error("svg error: {0}")]
731 Usvg(Arc<usvg::Error>),
732}
733
734impl From<anyhow::Error> for ImageCacheError {
735 fn from(value: anyhow::Error) -> Self {
736 Self::Other(Arc::new(value))
737 }
738}
739
740impl From<io::Error> for ImageCacheError {
741 fn from(value: io::Error) -> Self {
742 Self::Io(Arc::new(value))
743 }
744}
745
746impl From<usvg::Error> for ImageCacheError {
747 fn from(value: usvg::Error) -> Self {
748 Self::Usvg(Arc::new(value))
749 }
750}
751
752impl From<image::ImageError> for ImageCacheError {
753 fn from(value: image::ImageError) -> Self {
754 Self::Image(Arc::new(value))
755 }
756}