Skip to main content

gpui/elements/
img.rs

1use crate::{
2    AnyElement, AnyImageCache, App, AppContext, Asset, AssetLogger, Bounds, DefiniteLength,
3    Element, ElementId, Entity, GlobalElementId, Hitbox, Image, ImageCache, InspectorElementId,
4    InteractiveElement, Interactivity, IntoElement, LayoutId, Length, ObjectFit, Pixels,
5    RenderImage, Resource, SharedString, SharedUri, StyleRefinement, Styled, Task, Window,
6    decode_static_image, decode_static_image_from_decoder, px,
7};
8use anyhow::Result;
9
10use futures::Future;
11use gpui_util::ResultExt;
12use image::{
13    AnimationDecoder, ImageError, ImageFormat, Rgba,
14    codecs::{gif::GifDecoder, webp::WebPDecoder},
15};
16use scheduler::Instant;
17use smallvec::SmallVec;
18use std::{
19    fs,
20    io::{self, Cursor},
21    ops::{Deref, DerefMut},
22    path::{Path, PathBuf},
23    str::FromStr,
24    sync::Arc,
25    time::Duration,
26};
27use thiserror::Error;
28
29use super::{Stateful, StatefulInteractiveElement};
30
31/// How long an image may take to load before the [`StyledImage::with_loading`] placeholder appears.
32///
33/// An image that finishes within this delay never shows the placeholder, so fast loads don't flash.
34pub const LOADING_DELAY: Duration = Duration::from_millis(200);
35
36/// The asset loader that [`img`] uses for [`ImageSource::Resource`] sources: URLs, file paths and
37/// [`crate::AssetRegistry`] entries.
38///
39/// The other [`ImageSource`] variants don't use it. Use `App::remove_asset::<ImgResourceLoader>`
40/// with a [`Resource`] to evict one entry.
41pub type ImgResourceLoader = AssetLogger<ImageAssetLoader>;
42
43/// Where an [`img`] element gets its pixels.
44///
45/// You rarely build this yourself. Each argument to [`img`] converts into one variant: a string,
46/// a [`PathBuf`](std::path::PathBuf) or a [`SharedUri`] becomes [`ImageSource::Resource`], an
47/// `Arc<Image>` becomes [`ImageSource::Image`], and an `Arc<RenderImage>` becomes
48/// [`ImageSource::Render`].
49///
50/// # How a string is read
51///
52/// A `&str`, `String` or `SharedString` is a URL if the `url` crate parses it, and an asset path
53/// otherwise. Any scheme counts, so `"https://example.com/a.png"` is a URL, and so are
54/// `"c:/logo.png"` (a Windows drive path) and `"data:..."`. URLs go to the app's HTTP client, which
55/// normally rejects schemes it doesn't handle. `"images/logo.png"` has no scheme, so it is looked
56/// up in the [`crate::AssetRegistry`].
57///
58/// To skip that check, convert a [`PathBuf`](std::path::PathBuf) or a [`SharedUri`] instead. A
59/// `PathBuf` always reads the file system, and a `SharedUri` always fetches.
60#[derive(Clone)]
61pub enum ImageSource {
62    /// Load the image from a [`Resource`]: a URL, a file path or a name in the [`crate::AssetRegistry`].
63    ///
64    /// The bytes load in the background and are cached by source.
65    Resource(Resource),
66    /// Pixels that are already decoded. Nothing is loaded, and the frames are drawn as they are.
67    Render(Arc<RenderImage>),
68    /// Encoded bytes in an [`Image`], such as a PNG or an SVG. The image is decoded in the
69    /// background and cached under the [`Image`]'s ID, which is a hash of its bytes.
70    Image(Arc<Image>),
71    /// A function that the element calls on every layout and paint to get the image.
72    ///
73    /// Return `None` while the image is not ready, `Some(Ok(..))` with the image, or
74    /// `Some(Err(..))` to show the fallback. gpui doesn't cache the result, so the function must be
75    /// fast or cache its own result.
76    Custom(Arc<dyn Fn(&mut Window, &mut App) -> Option<Result<Arc<RenderImage>, ImageCacheError>>>),
77}
78
79fn is_uri(uri: &str) -> bool {
80    url::Url::from_str(uri).is_ok()
81}
82
83impl From<SharedUri> for ImageSource {
84    fn from(value: SharedUri) -> Self {
85        Self::Resource(Resource::Uri(value))
86    }
87}
88
89impl<'a> From<&'a str> for ImageSource {
90    fn from(s: &'a str) -> Self {
91        if is_uri(s) {
92            Self::Resource(Resource::Uri(s.to_string().into()))
93        } else {
94            Self::Resource(Resource::Embedded(s.to_string().into()))
95        }
96    }
97}
98
99impl From<String> for ImageSource {
100    fn from(s: String) -> Self {
101        if is_uri(&s) {
102            Self::Resource(Resource::Uri(s.into()))
103        } else {
104            Self::Resource(Resource::Embedded(s.into()))
105        }
106    }
107}
108
109impl From<SharedString> for ImageSource {
110    fn from(s: SharedString) -> Self {
111        s.as_ref().into()
112    }
113}
114
115impl From<&Path> for ImageSource {
116    fn from(value: &Path) -> Self {
117        Self::Resource(value.to_path_buf().into())
118    }
119}
120
121impl From<Arc<Path>> for ImageSource {
122    fn from(value: Arc<Path>) -> Self {
123        Self::Resource(value.into())
124    }
125}
126
127impl From<PathBuf> for ImageSource {
128    fn from(value: PathBuf) -> Self {
129        Self::Resource(value.into())
130    }
131}
132
133impl From<Arc<RenderImage>> for ImageSource {
134    fn from(value: Arc<RenderImage>) -> Self {
135        Self::Render(value)
136    }
137}
138
139impl From<Arc<Image>> for ImageSource {
140    fn from(value: Arc<Image>) -> Self {
141        Self::Image(value)
142    }
143}
144
145impl<F> From<F> for ImageSource
146where
147    F: Fn(&mut Window, &mut App) -> Option<Result<Arc<RenderImage>, ImageCacheError>> + 'static,
148{
149    fn from(value: F) -> Self {
150        Self::Custom(Arc::new(value))
151    }
152}
153
154/// The settings that the [`StyledImage`] methods set on an image element.
155///
156/// Reach it through [`StyledImage::image_style`]. You don't create one directly.
157pub struct ImageStyle {
158    grayscale: bool,
159    object_fit: ObjectFit,
160    loading: Option<Box<dyn Fn() -> AnyElement>>,
161    fallback: Option<Box<dyn Fn() -> AnyElement>>,
162}
163
164impl Default for ImageStyle {
165    fn default() -> Self {
166        Self {
167            grayscale: false,
168            object_fit: ObjectFit::Contain,
169            loading: None,
170            fallback: None,
171        }
172    }
173}
174
175/// Styling methods for image elements, implemented by [`Img`] and `Stateful<Img>`.
176///
177/// Each method returns `self`, so calls chain after [`img`].
178pub trait StyledImage: Sized {
179    /// Returns the [`ImageStyle`] that the methods on this trait write to.
180    fn image_style(&mut self) -> &mut ImageStyle;
181
182    /// Draws the image in grayscale when `grayscale` is `true`. The default is `false`.
183    fn grayscale(mut self, grayscale: bool) -> Self {
184        self.image_style().grayscale = grayscale;
185        self
186    }
187
188    /// Sets how the image fills its box. The default is [`ObjectFit::Contain`].
189    ///
190    /// The fit matters only when the box's aspect ratio differs from the image's, so set both a
191    /// width and a height. With no size set, the box is the image's natural size, and every fit
192    /// looks the same.
193    ///
194    /// # Examples
195    ///
196    /// ```
197    /// # use gpui::*;
198    /// let avatar = img("images/avatar.png")
199    ///     .size(px(48.))
200    ///     .rounded_full()
201    ///     .object_fit(ObjectFit::Cover);
202    /// ```
203    fn object_fit(mut self, object_fit: ObjectFit) -> Self {
204        self.image_style().object_fit = object_fit;
205        self
206    }
207
208    /// Sets the closure that builds the element to show when the image fails to load.
209    ///
210    /// Failures include a missing asset, a non-success HTTP status, bytes that don't decode, and a
211    /// URL that the HTTP client can't fetch. The closure runs again on each layout where the
212    /// fallback is shown, so keep it cheap.
213    fn with_fallback(mut self, fallback: impl Fn() -> AnyElement + 'static) -> Self {
214        self.image_style().fallback = Some(Box::new(fallback));
215        self
216    }
217
218    /// Sets the closure that builds a placeholder to show while the image loads.
219    ///
220    /// The placeholder appears only if loading takes longer than [`LOADING_DELAY`], so a quick load
221    /// never shows it. The placeholder and the fallback are children of the image element, so the
222    /// element's own size and style still apply.
223    ///
224    /// # Examples
225    ///
226    /// ```
227    /// # use gpui::*;
228    /// let photo = img("https://example.com/photo.jpg")
229    ///     .size(px(240.))
230    ///     .with_loading(|| {
231    ///         div()
232    ///             .size(px(240.))
233    ///             .bg(rgb(0xe5e7eb))
234    ///             .into_any_element()
235    ///     })
236    ///     .with_fallback(|| {
237    ///         div()
238    ///             .size(px(240.))
239    ///             .flex()
240    ///             .items_center()
241    ///             .justify_center()
242    ///             .child("Image unavailable")
243    ///             .into_any_element()
244    ///     });
245    /// ```
246    fn with_loading(mut self, loading: impl Fn() -> AnyElement + 'static) -> Self {
247        self.image_style().loading = Some(Box::new(loading));
248        self
249    }
250}
251
252impl StyledImage for Img {
253    fn image_style(&mut self) -> &mut ImageStyle {
254        &mut self.style
255    }
256}
257
258impl StyledImage for Stateful<Img> {
259    fn image_style(&mut self) -> &mut ImageStyle {
260        &mut self.element.style
261    }
262}
263
264/// An image element, made by [`img`].
265///
266/// It is a styled, interactive element. Style it with [`Styled`] and [`StyledImage`], and attach
267/// event handlers with [`InteractiveElement`].
268pub struct Img {
269    interactivity: Interactivity,
270    source: ImageSource,
271    style: ImageStyle,
272    image_cache: Option<AnyImageCache>,
273}
274
275/// Creates an image element that draws `source`.
276///
277/// Building the element does no work. The image starts loading the first time the element is laid
278/// out, on a background thread. Until it loads, the element has no size unless you set one, and it
279/// draws nothing unless you set a [`StyledImage::with_loading`] placeholder. Once the image loads,
280/// the element takes the image's natural size. If you set only a width or only a height, the other
281/// dimension follows the aspect ratio.
282///
283/// `source` is anything that converts into an [`ImageSource`]. That type explains how a string is
284/// read.
285///
286/// Loaded images are cached, so a second `img` with the same source reuses the decoded image. See
287/// the [images guide](crate::_documentation::guides::images_and_assets) for the cache rules, and
288/// [`crate::image_cache`] to scope them to a group of images.
289///
290/// # Examples
291///
292/// ```
293/// # use gpui::*;
294/// let remote = img("https://example.com/avatar.png");
295/// let bundled = img("images/logo.png");
296/// let local = img(std::path::PathBuf::from("assets/banner.jpg"));
297/// ```
298#[track_caller]
299pub fn img(source: impl Into<ImageSource>) -> Img {
300    Img {
301        interactivity: Interactivity::new(),
302        source: source.into(),
303        style: ImageStyle::default(),
304        image_cache: None,
305    }
306}
307
308impl Img {
309    /// Returns the file extensions that `img` can decode, in lowercase and without the dot.
310    ///
311    /// The list is the extensions of `image::ImageFormat` plus `svg`. gpui decides whether a file
312    /// loads by reading its bytes, not its extension, so the list is a guide.
313    ///
314    /// # Examples
315    ///
316    /// ```
317    /// # use gpui::*;
318    /// assert!(Img::extensions().contains(&"png"));
319    /// assert!(Img::extensions().contains(&"svg"));
320    /// ```
321    pub fn extensions() -> &'static [&'static str] {
322        // This is the list in [image::ImageFormat::from_extension] + `svg`
323        &[
324            "avif", "jpg", "jpeg", "png", "gif", "webp", "tif", "tiff", "tga", "dds", "bmp", "ico",
325            "hdr", "exr", "pbm", "pam", "ppm", "pgm", "ff", "farbfeld", "qoi", "svg",
326        ]
327    }
328
329    /// Uses `image_cache` for this image, instead of the cache from an ancestor.
330    ///
331    /// Without this call, the image uses the cache of the nearest ancestor [`crate::image_cache`] element,
332    /// or the app-wide asset cache if there is none. Only [`ImageSource::Resource`] sources go
333    /// through an image cache. The other variants ignore it.
334    #[inline]
335    pub fn image_cache<I: ImageCache>(self, image_cache: &Entity<I>) -> Self {
336        Self {
337            image_cache: Some(image_cache.clone().into()),
338            ..self
339        }
340    }
341}
342
343impl Deref for Stateful<Img> {
344    type Target = Img;
345
346    fn deref(&self) -> &Self::Target {
347        &self.element
348    }
349}
350
351impl DerefMut for Stateful<Img> {
352    fn deref_mut(&mut self) -> &mut Self::Target {
353        &mut self.element
354    }
355}
356
357/// Frames shorter than this are treated as this long, so zero-delay frames can't stall playback.
358const MIN_FRAME_DELAY: Duration = Duration::from_millis(1);
359
360/// Steps an animation forward by `elapsed`, returning the new frame index and the time already
361/// spent in that frame. Advances through every frame covered by `elapsed`, including multiple
362/// frames when one update spans several frame intervals.
363fn advance_animation(
364    mut frame_index: usize,
365    elapsed: Duration,
366    frame_count: usize,
367    frame_delay: impl Fn(usize) -> Duration,
368) -> (usize, Duration) {
369    if frame_count == 0 {
370        return (0, elapsed);
371    }
372
373    let delay = |index| frame_delay(index).max(MIN_FRAME_DELAY);
374    frame_index %= frame_count;
375
376    // Whole cycles end on the same frame, so skip them rather than stepping through each.
377    let cycle_nanos = (0..frame_count)
378        .map(|index| delay(index).as_nanos())
379        .sum::<u128>();
380    let remainder_nanos = elapsed.as_nanos() % cycle_nanos;
381    let mut remaining = Duration::new(
382        (remainder_nanos / 1_000_000_000) as u64,
383        (remainder_nanos % 1_000_000_000) as u32,
384    );
385
386    while remaining >= delay(frame_index) {
387        remaining -= delay(frame_index);
388        frame_index = (frame_index + 1) % frame_count;
389    }
390    (frame_index, remaining)
391}
392
393/// The image state between frames
394struct ImgState {
395    frame_index: usize,
396    last_frame_time: Option<Instant>,
397    started_loading: Option<(Instant, Task<()>)>,
398}
399
400/// The per-frame state that an [`Img`] passes from layout to paint.
401///
402/// It holds the frame of an animated image to draw, and the placeholder or fallback element when
403/// one is showing.
404pub struct ImgLayoutState {
405    frame_index: usize,
406    replacement: Option<AnyElement>,
407}
408
409impl Element for Img {
410    type RequestLayoutState = ImgLayoutState;
411    type PrepaintState = Option<Hitbox>;
412
413    fn id(&self) -> Option<ElementId> {
414        self.interactivity.element_id.clone()
415    }
416
417    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
418        self.interactivity.source_location()
419    }
420
421    fn request_layout(
422        &mut self,
423        global_id: Option<&GlobalElementId>,
424        inspector_id: Option<&InspectorElementId>,
425        window: &mut Window,
426        cx: &mut App,
427    ) -> (LayoutId, Self::RequestLayoutState) {
428        let mut layout_state = ImgLayoutState {
429            frame_index: 0,
430            replacement: None,
431        };
432
433        window.with_optional_element_state(global_id, |state, window| {
434            let mut state = state.map(|state| {
435                state.unwrap_or(ImgState {
436                    frame_index: 0,
437                    last_frame_time: None,
438                    started_loading: None,
439                })
440            });
441
442            let mut frame_index = state.as_ref().map(|state| state.frame_index).unwrap_or(0);
443
444            let layout_id = self.interactivity.request_layout(
445                global_id,
446                inspector_id,
447                window,
448                cx,
449                |mut style, window, cx| {
450                    let mut replacement_id = None;
451
452                    match self.source.use_data(
453                        self.image_cache
454                            .clone()
455                            .or_else(|| window.image_cache_stack.last().cloned()),
456                        window,
457                        cx,
458                    ) {
459                        Some(Ok(data)) => {
460                            let frame_count = data.frame_count();
461                            let max_frame_index = frame_count.saturating_sub(1);
462
463                            if let Some(state) = &mut state {
464                                state.frame_index = state.frame_index.min(max_frame_index);
465                                if frame_count > 1 && !cx.reduce_motion() {
466                                    if window.is_window_active() {
467                                        let current_time = cx.background_executor().now();
468                                        if let Some(last_frame_time) = state.last_frame_time {
469                                            let elapsed = current_time - last_frame_time;
470                                            let (frame_index, remainder) = advance_animation(
471                                                state.frame_index,
472                                                elapsed,
473                                                frame_count,
474                                                |index| Duration::from(data.delay(index)),
475                                            );
476                                            state.frame_index = frame_index;
477                                            state.last_frame_time = Some(current_time - remainder);
478                                        } else {
479                                            state.last_frame_time = Some(current_time);
480                                        }
481                                    } else {
482                                        state.last_frame_time = None;
483                                    }
484                                } else {
485                                    state.last_frame_time = None;
486                                }
487                                state.started_loading = None;
488                                frame_index = state.frame_index;
489                            }
490
491                            let image_size = data.render_size(frame_index);
492
493                            if style.aspect_ratio.is_none() {
494                                style.aspect_ratio = Some(image_size.width / image_size.height);
495                            }
496
497                            if let Length::Auto = style.size.width {
498                                style.size.width = match style.size.height {
499                                    Length::Definite(DefiniteLength::Absolute(abs_length)) => {
500                                        let height_px = abs_length.to_pixels(window.rem_size());
501                                        Length::Definite(
502                                            px(image_size.width.0 * height_px.0
503                                                / image_size.height.0)
504                                            .into(),
505                                        )
506                                    }
507                                    _ => Length::Definite(image_size.width.into()),
508                                };
509                            }
510
511                            if let Length::Auto = style.size.height {
512                                style.size.height = match style.size.width {
513                                    Length::Definite(DefiniteLength::Absolute(abs_length)) => {
514                                        let width_px = abs_length.to_pixels(window.rem_size());
515                                        Length::Definite(
516                                            px(image_size.height.0 * width_px.0
517                                                / image_size.width.0)
518                                            .into(),
519                                        )
520                                    }
521                                    _ => Length::Definite(image_size.height.into()),
522                                };
523                            }
524
525                            if global_id.is_some()
526                                && data.frame_count() > 1
527                                && window.is_window_active()
528                                && !cx.reduce_motion()
529                            {
530                                window.request_animation_frame();
531                            }
532                        }
533                        Some(_err) => {
534                            if let Some(fallback) = self.style.fallback.as_ref() {
535                                let mut element = fallback();
536                                replacement_id = Some(element.request_layout(window, cx));
537                                layout_state.replacement = Some(element);
538                            }
539                            if let Some(state) = &mut state {
540                                state.started_loading = None;
541                            }
542                        }
543                        None => {
544                            if let Some(state) = &mut state {
545                                if let Some((started_loading, _)) = state.started_loading {
546                                    if cx.background_executor().now() - started_loading
547                                        > LOADING_DELAY
548                                        && let Some(loading) = self.style.loading.as_ref()
549                                    {
550                                        let mut element = loading();
551                                        replacement_id = Some(element.request_layout(window, cx));
552                                        layout_state.replacement = Some(element);
553                                    }
554                                } else {
555                                    let current_view = window.current_view();
556                                    let task = window.spawn(cx, async move |cx| {
557                                        cx.background_executor().timer(LOADING_DELAY).await;
558                                        cx.update(move |_, cx| {
559                                            cx.notify(current_view);
560                                        })
561                                        .ok();
562                                    });
563                                    state.started_loading =
564                                        Some((cx.background_executor().now(), task));
565                                }
566                            }
567                        }
568                    }
569
570                    window.request_layout(style, replacement_id, cx)
571                },
572            );
573
574            layout_state.frame_index = frame_index;
575
576            ((layout_id, layout_state), state)
577        })
578    }
579
580    fn prepaint(
581        &mut self,
582        global_id: Option<&GlobalElementId>,
583        inspector_id: Option<&InspectorElementId>,
584        bounds: Bounds<Pixels>,
585        request_layout: &mut Self::RequestLayoutState,
586        window: &mut Window,
587        cx: &mut App,
588    ) -> Self::PrepaintState {
589        self.interactivity.prepaint(
590            global_id,
591            inspector_id,
592            bounds,
593            bounds.size,
594            window,
595            cx,
596            |_, _, hitbox, window, cx| {
597                if let Some(replacement) = &mut request_layout.replacement {
598                    replacement.prepaint(window, cx);
599                }
600
601                hitbox
602            },
603        )
604    }
605
606    fn paint(
607        &mut self,
608        global_id: Option<&GlobalElementId>,
609        inspector_id: Option<&InspectorElementId>,
610        bounds: Bounds<Pixels>,
611        layout_state: &mut Self::RequestLayoutState,
612        hitbox: &mut Self::PrepaintState,
613        window: &mut Window,
614        cx: &mut App,
615    ) {
616        let source = self.source.clone();
617        self.interactivity.paint(
618            global_id,
619            inspector_id,
620            bounds,
621            hitbox.as_ref(),
622            window,
623            cx,
624            |style, window, cx| {
625                if let Some(Ok(data)) = source.use_data(
626                    self.image_cache
627                        .clone()
628                        .or_else(|| window.image_cache_stack.last().cloned()),
629                    window,
630                    cx,
631                ) {
632                    if data.frame_count() == 0 {
633                        return;
634                    }
635                    let new_bounds = self
636                        .style
637                        .object_fit
638                        .get_bounds(bounds, data.size(layout_state.frame_index));
639                    let corner_radii = style.corner_radii.to_pixels(window.rem_size());
640                    window
641                        .paint_image(
642                            bounds,
643                            new_bounds,
644                            corner_radii,
645                            style.corner_smoothing.unwrap_or_default(),
646                            data,
647                            layout_state.frame_index,
648                            self.style.grayscale,
649                        )
650                        .log_err();
651                } else if let Some(replacement) = &mut layout_state.replacement {
652                    replacement.paint(window, cx);
653                }
654            },
655        )
656    }
657}
658
659impl Styled for Img {
660    fn style(&mut self) -> &mut StyleRefinement {
661        &mut self.interactivity.base_style
662    }
663}
664
665impl InteractiveElement for Img {
666    fn interactivity(&mut self) -> &mut Interactivity {
667        &mut self.interactivity
668    }
669}
670
671impl IntoElement for Img {
672    type Element = Self;
673
674    fn into_element(self) -> Self::Element {
675        self
676    }
677}
678
679impl StatefulInteractiveElement for Img {}
680
681impl ImageSource {
682    pub(crate) fn use_data(
683        &self,
684        cache: Option<AnyImageCache>,
685        window: &mut Window,
686        cx: &mut App,
687    ) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
688        match self {
689            ImageSource::Resource(resource) => {
690                if let Some(cache) = cache {
691                    cache.load(resource, window, cx)
692                } else {
693                    window.use_asset::<ImgResourceLoader>(resource, cx)
694                }
695            }
696            ImageSource::Custom(loading_fn) => loading_fn(window, cx),
697            ImageSource::Render(data) => Some(Ok(data.to_owned())),
698            ImageSource::Image(data) => window.use_asset::<AssetLogger<ImageDecoder>>(data, cx),
699        }
700    }
701
702    pub(crate) fn get_data(
703        &self,
704        cache: Option<AnyImageCache>,
705        window: &mut Window,
706        cx: &mut App,
707    ) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
708        match self {
709            ImageSource::Resource(resource) => {
710                if let Some(cache) = cache {
711                    cache.load(resource, window, cx)
712                } else {
713                    window.get_asset::<ImgResourceLoader>(resource, cx)
714                }
715            }
716            ImageSource::Custom(loading_fn) => loading_fn(window, cx),
717            ImageSource::Render(data) => Some(Ok(data.to_owned())),
718            ImageSource::Image(data) => window.get_asset::<AssetLogger<ImageDecoder>>(data, cx),
719        }
720    }
721
722    /// Evicts this source from the app's asset cache, so the next `img` that uses it loads it again.
723    ///
724    /// Only [`ImageSource::Resource`] and [`ImageSource::Image`] are in that cache, so the other
725    /// variants do nothing. Images held by an [`crate::image_cache`] element are not affected.
726    pub fn remove_asset(&self, cx: &mut App) {
727        match self {
728            ImageSource::Resource(resource) => {
729                cx.remove_asset::<ImgResourceLoader>(resource);
730            }
731            ImageSource::Custom(_) | ImageSource::Render(_) => {}
732            ImageSource::Image(data) => cx.remove_asset::<AssetLogger<ImageDecoder>>(data),
733        }
734    }
735
736    /// Returns `true` if this source is loading or loaded in the app's asset cache. Starts no load.
737    ///
738    /// Available with the `test-support` feature. Use it in tests to check that an image was
739    /// requested or evicted.
740    #[cfg(any(test, feature = "test-support"))]
741    pub fn is_asset_cached(&self, cx: &App) -> bool {
742        match self {
743            ImageSource::Resource(resource) => cx.has_asset::<ImgResourceLoader>(resource),
744            ImageSource::Custom(_) | ImageSource::Render(_) => false,
745            ImageSource::Image(data) => cx.has_asset::<AssetLogger<ImageDecoder>>(data),
746        }
747    }
748}
749
750#[derive(Clone)]
751enum ImageDecoder {}
752
753impl Asset for ImageDecoder {
754    type Source = Arc<Image>;
755    type Output = Result<Arc<RenderImage>, ImageCacheError>;
756
757    fn load(
758        source: Self::Source,
759        cx: &mut App,
760    ) -> impl Future<Output = Self::Output> + Send + 'static {
761        let renderer = cx.svg_renderer();
762        async move { source.to_image_data(renderer).map_err(Into::into) }
763    }
764}
765
766/// Loads and decodes a [`Resource`] into a [`RenderImage`]. [`img`] uses it, through
767/// [`ImgResourceLoader`], for URLs, file paths and registry entries.
768///
769/// The bytes come from the source. URLs go through the HTTP client and must return a success
770/// status. Paths are read from disk. Registry names are read from the [`crate::AssetRegistry`]. The format
771/// is then guessed from the bytes. Anything the `image` crate recognizes is decoded, and every
772/// frame that decodes is kept for GIF and animated WebP. Anything else is parsed as SVG.
773#[derive(Clone)]
774pub enum ImageAssetLoader {}
775
776impl Asset for ImageAssetLoader {
777    type Source = Resource;
778    type Output = Result<Arc<RenderImage>, ImageCacheError>;
779
780    fn load(
781        source: Self::Source,
782        cx: &mut App,
783    ) -> impl Future<Output = Self::Output> + Send + 'static {
784        let client = cx.http_client();
785        // TODO: Can we make SVGs always rescale?
786        // let scale_factor = cx.scale_factor();
787        let svg_renderer = cx.svg_renderer();
788        let asset_registry = cx.assets().clone();
789        async move {
790            let bytes = match source.clone() {
791                Resource::Path(uri) => fs::read(uri.as_ref())?,
792                Resource::Uri(uri) => {
793                    use anyhow::Context as _;
794
795                    let response = client
796                        .get(uri.as_ref(), true)
797                        .await
798                        .with_context(|| format!("loading image asset from {uri:?}"))?;
799                    if !response.status.is_success() {
800                        let mut error_body = String::from_utf8_lossy(&response.body).into_owned();
801                        let first_line = error_body.lines().next().unwrap_or("").trim_end();
802                        error_body.truncate(first_line.len());
803                        return Err(ImageCacheError::BadStatus {
804                            uri,
805                            status: response.status,
806                            body: error_body,
807                        });
808                    }
809                    response.body
810                }
811                Resource::Embedded(path) => {
812                    let data = asset_registry.load(&path);
813                    if let Some(data) = data {
814                        data.to_vec()
815                    } else {
816                        return Err(ImageCacheError::Asset(
817                            format!("Embedded resource not found: {}", path).into(),
818                        ));
819                    }
820                }
821            };
822
823            if let Ok(format) = image::guess_format(&bytes) {
824                let data = match format {
825                    ImageFormat::Gif => {
826                        let decoder = GifDecoder::new(Cursor::new(&bytes))?;
827                        let mut frames = SmallVec::new();
828
829                        for frame in decoder.into_frames() {
830                            match frame {
831                                Ok(mut frame) => {
832                                    // Convert from RGBA to BGRA.
833                                    for pixel in frame.buffer_mut().chunks_exact_mut(4) {
834                                        pixel.swap(0, 2);
835                                    }
836                                    frames.push(frame);
837                                }
838                                Err(err) => {
839                                    log::debug!(
840                                        "Skipping GIF frame in {source:?} due to decode error: {err}"
841                                    );
842                                }
843                            }
844                        }
845
846                        if frames.is_empty() {
847                            return Err(ImageCacheError::Other(Arc::new(anyhow::anyhow!(
848                                "GIF could not be decoded: all frames failed ({source:?})"
849                            ))));
850                        }
851
852                        frames
853                    }
854                    ImageFormat::WebP => {
855                        let mut decoder = WebPDecoder::new(Cursor::new(&bytes))?;
856
857                        if decoder.has_animation() {
858                            let _ = decoder.set_background_color(Rgba([0, 0, 0, 0]));
859                            let mut frames = SmallVec::new();
860
861                            for frame in decoder.into_frames() {
862                                match frame {
863                                    Ok(mut frame) => {
864                                        // Convert from RGBA to BGRA.
865                                        for pixel in frame.buffer_mut().chunks_exact_mut(4) {
866                                            pixel.swap(0, 2);
867                                        }
868                                        frames.push(frame);
869                                    }
870                                    Err(err) => {
871                                        log::debug!(
872                                            "Skipping WebP frame in {source:?} due to decode error: {err}"
873                                        );
874                                    }
875                                }
876                            }
877
878                            if frames.is_empty() {
879                                return Err(ImageCacheError::Other(Arc::new(anyhow::anyhow!(
880                                    "WebP could not be decoded: all frames failed ({source:?})"
881                                ))));
882                            }
883
884                            frames
885                        } else {
886                            decode_static_image_from_decoder(decoder)?
887                        }
888                    }
889                    _ => decode_static_image(&bytes, format)?,
890                };
891
892                Ok(Arc::new(RenderImage::new(data)))
893            } else {
894                svg_renderer
895                    .render_single_frame(&bytes, 1.0)
896                    .map_err(Into::into)
897            }
898        }
899    }
900}
901
902/// Why an image failed to load or decode.
903///
904/// [`StyledImage::with_fallback`] is shown when you get one of these. The cache keeps the error
905/// with the image, so every element that shows the image sees the same error.
906#[derive(Debug, Error, Clone)]
907pub enum ImageCacheError {
908    /// A general failure, such as a failed HTTP request, or a GIF or WebP whose frames all failed
909    /// to decode.
910    #[error("error: {0}")]
911    Other(#[from] Arc<anyhow::Error>),
912    /// A file could not be read, such as a [`Resource::Path`] that doesn't exist.
913    #[error("IO error: {0}")]
914    Io(Arc<std::io::Error>),
915    /// The HTTP response had a status other than success.
916    #[error("unexpected http status for {uri}: {status}, body: {body}")]
917    BadStatus {
918        /// The URI that was requested.
919        uri: SharedUri,
920        /// The HTTP status code.
921        status: http::StatusCode,
922        /// The first line of the response body.
923        body: String,
924    },
925    /// A name was not found in the [`crate::AssetRegistry`].
926    #[error("asset error: {0}")]
927    Asset(SharedString),
928    /// The `image` crate could not decode the bytes.
929    #[error("image error: {0}")]
930    Image(Arc<ImageError>),
931    /// The SVG could not be parsed or rendered.
932    #[error("svg error: {0}")]
933    Usvg(Arc<usvg::Error>),
934}
935
936impl From<anyhow::Error> for ImageCacheError {
937    fn from(value: anyhow::Error) -> Self {
938        Self::Other(Arc::new(value))
939    }
940}
941
942impl From<io::Error> for ImageCacheError {
943    fn from(value: io::Error) -> Self {
944        Self::Io(Arc::new(value))
945    }
946}
947
948impl From<usvg::Error> for ImageCacheError {
949    fn from(value: usvg::Error) -> Self {
950        Self::Usvg(Arc::new(value))
951    }
952}
953
954impl From<image::ImageError> for ImageCacheError {
955    fn from(value: image::ImageError) -> Self {
956        Self::Image(Arc::new(value))
957    }
958}
959
960#[cfg(test)]
961mod tests {
962    use super::*;
963    use crate::{
964        Context, ParentElement as _, Render, TestAppContext, Window, canvas, div, point, px, size,
965    };
966    use image::{Delay, Frame, ImageBuffer, Rgba};
967
968    const TEST_IMG_ID: &str = "test-img";
969
970    fn test_image(frame_count: usize) -> Arc<RenderImage> {
971        let frame = Frame::new(ImageBuffer::from_pixel(1, 1, Rgba([0, 0, 0, 0])));
972        Arc::new(RenderImage::new(SmallVec::from_iter(
973            (0..frame_count).map(|_| frame.clone()),
974        )))
975    }
976
977    fn test_image_with_size(width: u32, height: u32) -> Arc<RenderImage> {
978        let frame = Frame::new(ImageBuffer::from_pixel(width, height, Rgba([0, 0, 0, 0])));
979        Arc::new(RenderImage::new(SmallVec::from_elem(frame, 1)))
980    }
981
982    /// An animation whose frames are 1, 2 and 3 pixels wide, lasting 100, 200 and 300 ms.
983    fn animated_image() -> Arc<RenderImage> {
984        let frames = [(1, 100), (2, 200), (3, 300)].map(|(width, delay_ms)| {
985            Frame::from_parts(
986                ImageBuffer::from_pixel(width, 1, Rgba([255, 0, 0, 255])),
987                0,
988                0,
989                Delay::from_numer_denom_ms(delay_ms, 1),
990            )
991        });
992        Arc::new(RenderImage::new(SmallVec::from_iter(frames)))
993    }
994
995    struct AnimatedImageView(Arc<RenderImage>);
996
997    impl Render for AnimatedImageView {
998        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
999            img(ImageSource::Render(self.0.clone()))
1000                .id(TEST_IMG_ID)
1001                .size_full()
1002        }
1003    }
1004
1005    #[test]
1006    fn advance_animation_steps_through_every_covered_frame() {
1007        let delays = [100, 200, 300].map(Duration::from_millis);
1008        let advance = |frame, elapsed_ms| {
1009            advance_animation(frame, Duration::from_millis(elapsed_ms), 3, |ix| delays[ix])
1010        };
1011
1012        assert_eq!(advance(0, 0), (0, Duration::ZERO));
1013        assert_eq!(advance(0, 99), (0, Duration::from_millis(99)));
1014        assert_eq!(advance(0, 340), (2, Duration::from_millis(40)));
1015        assert_eq!(advance(2, 375), (0, Duration::from_millis(75)));
1016        // A full cycle plus 150 ms: back to the first frame, 50 ms into the second.
1017        assert_eq!(advance(0, 750), (1, Duration::from_millis(50)));
1018    }
1019
1020    #[test]
1021    fn advance_animation_skips_whole_cycles_and_bounds_zero_delay_frames() {
1022        let delays = [
1023            Duration::ZERO,
1024            Duration::from_millis(2),
1025            Duration::from_millis(3),
1026        ];
1027        // Zero delays count as 1 ms, so a cycle lasts 6 ms.
1028        let elapsed = Duration::from_millis(4);
1029        let expected = advance_animation(1, elapsed, 3, |ix| delays[ix]);
1030        assert_eq!(
1031            advance_animation(1, elapsed + Duration::from_millis(6) * 500_000, 3, |ix| {
1032                delays[ix]
1033            }),
1034            expected
1035        );
1036        assert_eq!(
1037            advance_animation(0, Duration::from_millis(1), 2, |_| Duration::ZERO),
1038            (1, Duration::ZERO)
1039        );
1040    }
1041
1042    #[gpui::test]
1043    fn animated_image_follows_the_executor_clock(cx: &mut TestAppContext) {
1044        let (_, window) = cx.add_window_view(|_, _| AnimatedImageView(animated_image()));
1045        window.update(|window, _| window.activate());
1046        let clock = window.cx.background_executor.clone();
1047        let frame_width_after = |window: &mut crate::VisualTestContext, elapsed_ms: u64| {
1048            clock.advance_clock(Duration::from_millis(elapsed_ms));
1049            window.update(|window, cx| {
1050                window.simulate_next_frame(cx);
1051                window.draw(cx).clear(cx);
1052                window
1053                    .rendered_frame
1054                    .scene
1055                    .polychrome_sprites
1056                    .last()
1057                    .expect("animated image should paint a sprite")
1058                    .tile
1059                    .bounds
1060                    .size
1061                    .width
1062                    .0
1063            })
1064        };
1065
1066        for (elapsed_ms, expected_width) in [(0, 1), (99, 1), (1, 2), (1_100, 1), (99, 1), (1, 2)] {
1067            assert_eq!(
1068                frame_width_after(window, elapsed_ms),
1069                expected_width,
1070                "after {elapsed_ms} ms"
1071            );
1072        }
1073    }
1074
1075    /// Overwrites the cached `frame_index` of the sibling `img` during paint.
1076    fn seed_frame_index(frame_index: usize) -> impl IntoElement {
1077        canvas(
1078            |_, _, _| (),
1079            move |_, _, window, _| {
1080                window.with_global_id(TEST_IMG_ID.into(), |id, window| {
1081                    window.with_element_state::<ImgState, _>(id, |state, _| {
1082                        let mut state = state.expect("img state should be initialized");
1083                        state.frame_index = frame_index;
1084                        ((), state)
1085                    });
1086                });
1087            },
1088        )
1089    }
1090
1091    #[gpui::test]
1092    fn zero_frame_image_does_not_panic_on_paint(cx: &mut TestAppContext) {
1093        cx.add_empty_window()
1094            .draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| {
1095                img(ImageSource::Render(test_image(0))).into_any_element()
1096            });
1097    }
1098
1099    #[gpui::test]
1100    fn image_object_fit_cover_crops_to_element_bounds(cx: &mut TestAppContext) {
1101        let window = cx.add_empty_window();
1102        let image = test_image_with_size(200, 100);
1103        window.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| {
1104            img(ImageSource::Render(image.clone()))
1105                .size_full()
1106                .object_fit(ObjectFit::Fill)
1107                .into_any_element()
1108        });
1109        let full_tile_bounds = window.update(|window, _| {
1110            window
1111                .rendered_frame
1112                .scene
1113                .polychrome_sprites
1114                .last()
1115                .expect("fill image should paint a sprite")
1116                .tile
1117                .bounds
1118        });
1119
1120        window.draw(point(px(10.), px(20.)), size(px(100.), px(100.)), |_, _| {
1121            img(ImageSource::Render(image))
1122                .size_full()
1123                .object_fit(ObjectFit::Cover)
1124                .into_any_element()
1125        });
1126
1127        let (rendered_bounds, rendered_tile_bounds, scale_factor) = window.update(|window, _| {
1128            let sprite = window
1129                .rendered_frame
1130                .scene
1131                .polychrome_sprites
1132                .last()
1133                .expect("cover image should paint a sprite");
1134            (sprite.bounds, sprite.tile.bounds, window.scale_factor())
1135        });
1136        assert_eq!(
1137            rendered_bounds,
1138            Bounds {
1139                origin: point(px(10.).scale(scale_factor), px(20.).scale(scale_factor)),
1140                size: size(px(100.).scale(scale_factor), px(100.).scale(scale_factor)),
1141            }
1142        );
1143        assert_eq!(
1144            (
1145                rendered_tile_bounds.origin.x.0 - full_tile_bounds.origin.x.0,
1146                rendered_tile_bounds.origin.y.0 - full_tile_bounds.origin.y.0,
1147                rendered_tile_bounds.size.width.0,
1148                rendered_tile_bounds.size.height.0,
1149            ),
1150            (50, 0, 100, 100),
1151        );
1152    }
1153
1154    #[gpui::test]
1155    fn explicit_aspect_ratio_is_not_overridden_by_intrinsic_ratio(cx: &mut TestAppContext) {
1156        let window = cx.add_empty_window();
1157
1158        // A portrait image in a square container
1159        window.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| {
1160            div()
1161                .size(px(100.))
1162                .overflow_hidden()
1163                .child(
1164                    img(ImageSource::Render(test_image_with_size(100, 200)))
1165                        .size_full()
1166                        .aspect_square()
1167                        .object_fit(ObjectFit::Contain),
1168                )
1169                .into_any_element()
1170        });
1171
1172        let (rendered_bounds, scale_factor) = window.update(|window, _| {
1173            let sprite = window
1174                .rendered_frame
1175                .scene
1176                .polychrome_sprites
1177                .last()
1178                .expect("contained image should paint a sprite");
1179            (sprite.bounds, window.scale_factor())
1180        });
1181
1182        // The element stays 100x100, so the image is letterboxed horizontally
1183        assert_eq!(
1184            rendered_bounds,
1185            Bounds {
1186                origin: point(px(25.).scale(scale_factor), px(0.).scale(scale_factor)),
1187                size: size(px(50.).scale(scale_factor), px(100.).scale(scale_factor)),
1188            }
1189        );
1190    }
1191
1192    #[gpui::test]
1193    fn image_object_fit_cover_clamps_corner_radii_to_visible_bounds(cx: &mut TestAppContext) {
1194        let window = cx.add_empty_window();
1195        window.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| {
1196            img(ImageSource::Render(test_image_with_size(200, 100)))
1197                .size_full()
1198                .rounded(px(100.))
1199                .object_fit(ObjectFit::Cover)
1200                .into_any_element()
1201        });
1202
1203        let (corner_radius, expected_corner_radius) = window.update(|window, _| {
1204            (
1205                window
1206                    .rendered_frame
1207                    .scene
1208                    .polychrome_sprites
1209                    .last()
1210                    .map(|sprite| sprite.corner_radii.top_left),
1211                px(50.).scale(window.scale_factor()),
1212            )
1213        });
1214        assert_eq!(corner_radius, Some(expected_corner_radius));
1215    }
1216
1217    #[gpui::test]
1218    fn stale_frame_index_is_clamped_when_image_changes(cx: &mut TestAppContext) {
1219        let window = cx.add_empty_window();
1220
1221        // Assert that a cached frame_index from a previous multi-frame image
1222        // does not cause an out-of-bounds panic when the image is replaced
1223        // with one that has fewer frames.
1224        window.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| {
1225            div()
1226                .child(img(ImageSource::Render(test_image(5))).id(TEST_IMG_ID))
1227                .child(seed_frame_index(4))
1228                .into_any_element()
1229        });
1230        window.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| {
1231            img(ImageSource::Render(test_image(1)))
1232                .id(TEST_IMG_ID)
1233                .into_any_element()
1234        });
1235    }
1236}