Skip to main content

gpui/elements/
img.rs

1use crate::{
2    AnyElement, AnyImageCache, App, Asset, AssetLogger, Bounds, DefiniteLength, Element, ElementId,
3    Entity, GlobalElementId, Hitbox, Image, ImageCache, InspectorElementId, InteractiveElement,
4    Interactivity, IntoElement, LayoutId, Length, ObjectFit, Pixels, RenderImage, Resource,
5    SharedString, SharedUri, StyleRefinement, Styled, Task, Window, decode_static_image,
6    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/// The delay before showing the loading state.
32pub const LOADING_DELAY: Duration = Duration::from_millis(200);
33
34/// A type alias to the resource loader that the `img()` element uses.
35///
36/// Note: that this is only for Resources, like URLs or file paths.
37/// Custom loaders, or external images will not use this asset loader
38pub type ImgResourceLoader = AssetLogger<ImageAssetLoader>;
39
40/// A source of image content.
41#[derive(Clone)]
42pub enum ImageSource {
43    /// The image content will be loaded from some resource location
44    Resource(Resource),
45    /// Cached image data
46    Render(Arc<RenderImage>),
47    /// Cached image data
48    Image(Arc<Image>),
49    /// A custom loading function to use
50    Custom(Arc<dyn Fn(&mut Window, &mut App) -> Option<Result<Arc<RenderImage>, ImageCacheError>>>),
51}
52
53fn is_uri(uri: &str) -> bool {
54    url::Url::from_str(uri).is_ok()
55}
56
57impl From<SharedUri> for ImageSource {
58    fn from(value: SharedUri) -> Self {
59        Self::Resource(Resource::Uri(value))
60    }
61}
62
63impl<'a> From<&'a str> for ImageSource {
64    fn from(s: &'a str) -> Self {
65        if is_uri(s) {
66            Self::Resource(Resource::Uri(s.to_string().into()))
67        } else {
68            Self::Resource(Resource::Embedded(s.to_string().into()))
69        }
70    }
71}
72
73impl From<String> for ImageSource {
74    fn from(s: String) -> Self {
75        if is_uri(&s) {
76            Self::Resource(Resource::Uri(s.into()))
77        } else {
78            Self::Resource(Resource::Embedded(s.into()))
79        }
80    }
81}
82
83impl From<SharedString> for ImageSource {
84    fn from(s: SharedString) -> Self {
85        s.as_ref().into()
86    }
87}
88
89impl From<&Path> for ImageSource {
90    fn from(value: &Path) -> Self {
91        Self::Resource(value.to_path_buf().into())
92    }
93}
94
95impl From<Arc<Path>> for ImageSource {
96    fn from(value: Arc<Path>) -> Self {
97        Self::Resource(value.into())
98    }
99}
100
101impl From<PathBuf> for ImageSource {
102    fn from(value: PathBuf) -> Self {
103        Self::Resource(value.into())
104    }
105}
106
107impl From<Arc<RenderImage>> for ImageSource {
108    fn from(value: Arc<RenderImage>) -> Self {
109        Self::Render(value)
110    }
111}
112
113impl From<Arc<Image>> for ImageSource {
114    fn from(value: Arc<Image>) -> Self {
115        Self::Image(value)
116    }
117}
118
119impl<F> From<F> for ImageSource
120where
121    F: Fn(&mut Window, &mut App) -> Option<Result<Arc<RenderImage>, ImageCacheError>> + 'static,
122{
123    fn from(value: F) -> Self {
124        Self::Custom(Arc::new(value))
125    }
126}
127
128/// The style of an image element.
129pub struct ImageStyle {
130    grayscale: bool,
131    object_fit: ObjectFit,
132    loading: Option<Box<dyn Fn() -> AnyElement>>,
133    fallback: Option<Box<dyn Fn() -> AnyElement>>,
134}
135
136impl Default for ImageStyle {
137    fn default() -> Self {
138        Self {
139            grayscale: false,
140            object_fit: ObjectFit::Contain,
141            loading: None,
142            fallback: None,
143        }
144    }
145}
146
147/// Style an image element.
148pub trait StyledImage: Sized {
149    /// Get a mutable [ImageStyle] from the element.
150    fn image_style(&mut self) -> &mut ImageStyle;
151
152    /// Set the image to be displayed in grayscale.
153    fn grayscale(mut self, grayscale: bool) -> Self {
154        self.image_style().grayscale = grayscale;
155        self
156    }
157
158    /// Set the object fit for the image.
159    fn object_fit(mut self, object_fit: ObjectFit) -> Self {
160        self.image_style().object_fit = object_fit;
161        self
162    }
163
164    /// Set a fallback function that will be invoked to render an error view should
165    /// the image fail to load.
166    fn with_fallback(mut self, fallback: impl Fn() -> AnyElement + 'static) -> Self {
167        self.image_style().fallback = Some(Box::new(fallback));
168        self
169    }
170
171    /// Set a fallback function that will be invoked to render a view while the image
172    /// is still being loaded.
173    fn with_loading(mut self, loading: impl Fn() -> AnyElement + 'static) -> Self {
174        self.image_style().loading = Some(Box::new(loading));
175        self
176    }
177}
178
179impl StyledImage for Img {
180    fn image_style(&mut self) -> &mut ImageStyle {
181        &mut self.style
182    }
183}
184
185impl StyledImage for Stateful<Img> {
186    fn image_style(&mut self) -> &mut ImageStyle {
187        &mut self.element.style
188    }
189}
190
191/// An image element.
192pub struct Img {
193    interactivity: Interactivity,
194    source: ImageSource,
195    style: ImageStyle,
196    image_cache: Option<AnyImageCache>,
197}
198
199/// Create a new image element.
200#[track_caller]
201pub fn img(source: impl Into<ImageSource>) -> Img {
202    Img {
203        interactivity: Interactivity::new(),
204        source: source.into(),
205        style: ImageStyle::default(),
206        image_cache: None,
207    }
208}
209
210impl Img {
211    /// A list of all format extensions currently supported by this img element
212    pub fn extensions() -> &'static [&'static str] {
213        // This is the list in [image::ImageFormat::from_extension] + `svg`
214        &[
215            "avif", "jpg", "jpeg", "png", "gif", "webp", "tif", "tiff", "tga", "dds", "bmp", "ico",
216            "hdr", "exr", "pbm", "pam", "ppm", "pgm", "ff", "farbfeld", "qoi", "svg",
217        ]
218    }
219
220    /// Sets the image cache for the current node.
221    ///
222    /// If the `image_cache` is not explicitly provided, the function will determine the image cache by:
223    ///
224    /// 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.
225    /// 2. If no ancestor node contains an `ImageCacheElement`, the global image cache will be used as a fallback.
226    ///
227    /// This mechanism provides a flexible way to manage image caching, allowing precise control when needed,
228    /// while ensuring a default behavior when no cache is explicitly specified.
229    #[inline]
230    pub fn image_cache<I: ImageCache>(self, image_cache: &Entity<I>) -> Self {
231        Self {
232            image_cache: Some(image_cache.clone().into()),
233            ..self
234        }
235    }
236}
237
238impl Deref for Stateful<Img> {
239    type Target = Img;
240
241    fn deref(&self) -> &Self::Target {
242        &self.element
243    }
244}
245
246impl DerefMut for Stateful<Img> {
247    fn deref_mut(&mut self) -> &mut Self::Target {
248        &mut self.element
249    }
250}
251
252/// The image state between frames
253struct ImgState {
254    frame_index: usize,
255    last_frame_time: Option<Instant>,
256    started_loading: Option<(Instant, Task<()>)>,
257}
258
259/// The image layout state between frames
260pub struct ImgLayoutState {
261    frame_index: usize,
262    replacement: Option<AnyElement>,
263}
264
265impl Element for Img {
266    type RequestLayoutState = ImgLayoutState;
267    type PrepaintState = Option<Hitbox>;
268
269    fn id(&self) -> Option<ElementId> {
270        self.interactivity.element_id.clone()
271    }
272
273    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
274        self.interactivity.source_location()
275    }
276
277    fn request_layout(
278        &mut self,
279        global_id: Option<&GlobalElementId>,
280        inspector_id: Option<&InspectorElementId>,
281        window: &mut Window,
282        cx: &mut App,
283    ) -> (LayoutId, Self::RequestLayoutState) {
284        let mut layout_state = ImgLayoutState {
285            frame_index: 0,
286            replacement: None,
287        };
288
289        window.with_optional_element_state(global_id, |state, window| {
290            let mut state = state.map(|state| {
291                state.unwrap_or(ImgState {
292                    frame_index: 0,
293                    last_frame_time: None,
294                    started_loading: None,
295                })
296            });
297
298            let mut frame_index = state.as_ref().map(|state| state.frame_index).unwrap_or(0);
299
300            let layout_id = self.interactivity.request_layout(
301                global_id,
302                inspector_id,
303                window,
304                cx,
305                |mut style, window, cx| {
306                    let mut replacement_id = None;
307
308                    match self.source.use_data(
309                        self.image_cache
310                            .clone()
311                            .or_else(|| window.image_cache_stack.last().cloned()),
312                        window,
313                        cx,
314                    ) {
315                        Some(Ok(data)) => {
316                            let frame_count = data.frame_count();
317                            let max_frame_index = frame_count.saturating_sub(1);
318
319                            if let Some(state) = &mut state {
320                                state.frame_index = state.frame_index.min(max_frame_index);
321                                if frame_count > 1 && !cx.reduce_motion() {
322                                    if window.is_window_active() {
323                                        let current_time = Instant::now();
324                                        if let Some(last_frame_time) = state.last_frame_time {
325                                            let elapsed = current_time - last_frame_time;
326                                            let frame_duration =
327                                                Duration::from(data.delay(state.frame_index));
328
329                                            if elapsed >= frame_duration {
330                                                state.frame_index =
331                                                    (state.frame_index + 1) % frame_count;
332                                                state.last_frame_time =
333                                                    Some(current_time - (elapsed - frame_duration));
334                                            }
335                                        } else {
336                                            state.last_frame_time = Some(current_time);
337                                        }
338                                    } else {
339                                        state.last_frame_time = None;
340                                    }
341                                } else {
342                                    state.last_frame_time = None;
343                                }
344                                state.started_loading = None;
345                                frame_index = state.frame_index;
346                            }
347
348                            let image_size = data.render_size(frame_index);
349
350                            if style.aspect_ratio.is_none() {
351                                style.aspect_ratio = Some(image_size.width / image_size.height);
352                            }
353
354                            if let Length::Auto = style.size.width {
355                                style.size.width = match style.size.height {
356                                    Length::Definite(DefiniteLength::Absolute(abs_length)) => {
357                                        let height_px = abs_length.to_pixels(window.rem_size());
358                                        Length::Definite(
359                                            px(image_size.width.0 * height_px.0
360                                                / image_size.height.0)
361                                            .into(),
362                                        )
363                                    }
364                                    _ => Length::Definite(image_size.width.into()),
365                                };
366                            }
367
368                            if let Length::Auto = style.size.height {
369                                style.size.height = match style.size.width {
370                                    Length::Definite(DefiniteLength::Absolute(abs_length)) => {
371                                        let width_px = abs_length.to_pixels(window.rem_size());
372                                        Length::Definite(
373                                            px(image_size.height.0 * width_px.0
374                                                / image_size.width.0)
375                                            .into(),
376                                        )
377                                    }
378                                    _ => Length::Definite(image_size.height.into()),
379                                };
380                            }
381
382                            if global_id.is_some()
383                                && data.frame_count() > 1
384                                && window.is_window_active()
385                                && !cx.reduce_motion()
386                            {
387                                window.request_animation_frame();
388                            }
389                        }
390                        Some(_err) => {
391                            if let Some(fallback) = self.style.fallback.as_ref() {
392                                let mut element = fallback();
393                                replacement_id = Some(element.request_layout(window, cx));
394                                layout_state.replacement = Some(element);
395                            }
396                            if let Some(state) = &mut state {
397                                state.started_loading = None;
398                            }
399                        }
400                        None => {
401                            if let Some(state) = &mut state {
402                                if let Some((started_loading, _)) = state.started_loading {
403                                    if started_loading.elapsed() > LOADING_DELAY
404                                        && let Some(loading) = self.style.loading.as_ref()
405                                    {
406                                        let mut element = loading();
407                                        replacement_id = Some(element.request_layout(window, cx));
408                                        layout_state.replacement = Some(element);
409                                    }
410                                } else {
411                                    let current_view = window.current_view();
412                                    let task = window.spawn(cx, async move |cx| {
413                                        cx.background_executor().timer(LOADING_DELAY).await;
414                                        cx.update(move |_, cx| {
415                                            cx.notify(current_view);
416                                        })
417                                        .ok();
418                                    });
419                                    state.started_loading = Some((Instant::now(), task));
420                                }
421                            }
422                        }
423                    }
424
425                    window.request_layout(style, replacement_id, cx)
426                },
427            );
428
429            layout_state.frame_index = frame_index;
430
431            ((layout_id, layout_state), state)
432        })
433    }
434
435    fn prepaint(
436        &mut self,
437        global_id: Option<&GlobalElementId>,
438        inspector_id: Option<&InspectorElementId>,
439        bounds: Bounds<Pixels>,
440        request_layout: &mut Self::RequestLayoutState,
441        window: &mut Window,
442        cx: &mut App,
443    ) -> Self::PrepaintState {
444        self.interactivity.prepaint(
445            global_id,
446            inspector_id,
447            bounds,
448            bounds.size,
449            window,
450            cx,
451            |_, _, hitbox, window, cx| {
452                if let Some(replacement) = &mut request_layout.replacement {
453                    replacement.prepaint(window, cx);
454                }
455
456                hitbox
457            },
458        )
459    }
460
461    fn paint(
462        &mut self,
463        global_id: Option<&GlobalElementId>,
464        inspector_id: Option<&InspectorElementId>,
465        bounds: Bounds<Pixels>,
466        layout_state: &mut Self::RequestLayoutState,
467        hitbox: &mut Self::PrepaintState,
468        window: &mut Window,
469        cx: &mut App,
470    ) {
471        let source = self.source.clone();
472        self.interactivity.paint(
473            global_id,
474            inspector_id,
475            bounds,
476            hitbox.as_ref(),
477            window,
478            cx,
479            |style, window, cx| {
480                if let Some(Ok(data)) = source.use_data(
481                    self.image_cache
482                        .clone()
483                        .or_else(|| window.image_cache_stack.last().cloned()),
484                    window,
485                    cx,
486                ) {
487                    if data.frame_count() == 0 {
488                        return;
489                    }
490                    let new_bounds = self
491                        .style
492                        .object_fit
493                        .get_bounds(bounds, data.size(layout_state.frame_index));
494                    let corner_radii = style.corner_radii.to_pixels(window.rem_size());
495                    window
496                        .paint_image(
497                            bounds,
498                            new_bounds,
499                            corner_radii,
500                            data,
501                            layout_state.frame_index,
502                            self.style.grayscale,
503                        )
504                        .log_err();
505                } else if let Some(replacement) = &mut layout_state.replacement {
506                    replacement.paint(window, cx);
507                }
508            },
509        )
510    }
511}
512
513impl Styled for Img {
514    fn style(&mut self) -> &mut StyleRefinement {
515        &mut self.interactivity.base_style
516    }
517}
518
519impl InteractiveElement for Img {
520    fn interactivity(&mut self) -> &mut Interactivity {
521        &mut self.interactivity
522    }
523}
524
525impl IntoElement for Img {
526    type Element = Self;
527
528    fn into_element(self) -> Self::Element {
529        self
530    }
531}
532
533impl StatefulInteractiveElement for Img {}
534
535impl ImageSource {
536    pub(crate) fn use_data(
537        &self,
538        cache: Option<AnyImageCache>,
539        window: &mut Window,
540        cx: &mut App,
541    ) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
542        match self {
543            ImageSource::Resource(resource) => {
544                if let Some(cache) = cache {
545                    cache.load(resource, window, cx)
546                } else {
547                    window.use_asset::<ImgResourceLoader>(resource, cx)
548                }
549            }
550            ImageSource::Custom(loading_fn) => loading_fn(window, cx),
551            ImageSource::Render(data) => Some(Ok(data.to_owned())),
552            ImageSource::Image(data) => window.use_asset::<AssetLogger<ImageDecoder>>(data, cx),
553        }
554    }
555
556    pub(crate) fn get_data(
557        &self,
558        cache: Option<AnyImageCache>,
559        window: &mut Window,
560        cx: &mut App,
561    ) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
562        match self {
563            ImageSource::Resource(resource) => {
564                if let Some(cache) = cache {
565                    cache.load(resource, window, cx)
566                } else {
567                    window.get_asset::<ImgResourceLoader>(resource, cx)
568                }
569            }
570            ImageSource::Custom(loading_fn) => loading_fn(window, cx),
571            ImageSource::Render(data) => Some(Ok(data.to_owned())),
572            ImageSource::Image(data) => window.get_asset::<AssetLogger<ImageDecoder>>(data, cx),
573        }
574    }
575
576    /// Remove this image source from the asset system
577    pub fn remove_asset(&self, cx: &mut App) {
578        match self {
579            ImageSource::Resource(resource) => {
580                cx.remove_asset::<ImgResourceLoader>(resource);
581            }
582            ImageSource::Custom(_) | ImageSource::Render(_) => {}
583            ImageSource::Image(data) => cx.remove_asset::<AssetLogger<ImageDecoder>>(data),
584        }
585    }
586
587    /// Check whether this image source is present in the asset system (loading
588    /// or loaded), without fetching it.
589    #[cfg(any(test, feature = "test-support"))]
590    pub fn is_asset_cached(&self, cx: &App) -> bool {
591        match self {
592            ImageSource::Resource(resource) => cx.has_asset::<ImgResourceLoader>(resource),
593            ImageSource::Custom(_) | ImageSource::Render(_) => false,
594            ImageSource::Image(data) => cx.has_asset::<AssetLogger<ImageDecoder>>(data),
595        }
596    }
597}
598
599#[derive(Clone)]
600enum ImageDecoder {}
601
602impl Asset for ImageDecoder {
603    type Source = Arc<Image>;
604    type Output = Result<Arc<RenderImage>, ImageCacheError>;
605
606    fn load(
607        source: Self::Source,
608        cx: &mut App,
609    ) -> impl Future<Output = Self::Output> + Send + 'static {
610        let renderer = cx.svg_renderer();
611        async move { source.to_image_data(renderer).map_err(Into::into) }
612    }
613}
614
615/// An image loader for the GPUI asset system
616#[derive(Clone)]
617pub enum ImageAssetLoader {}
618
619impl Asset for ImageAssetLoader {
620    type Source = Resource;
621    type Output = Result<Arc<RenderImage>, ImageCacheError>;
622
623    fn load(
624        source: Self::Source,
625        cx: &mut App,
626    ) -> impl Future<Output = Self::Output> + Send + 'static {
627        let client = cx.http_client();
628        // TODO: Can we make SVGs always rescale?
629        // let scale_factor = cx.scale_factor();
630        let svg_renderer = cx.svg_renderer();
631        let asset_source = cx.asset_source().clone();
632        async move {
633            let bytes = match source.clone() {
634                Resource::Path(uri) => fs::read(uri.as_ref())?,
635                Resource::Uri(uri) => {
636                    use anyhow::Context as _;
637                    use futures::AsyncReadExt as _;
638
639                    let mut response = client
640                        .get(uri.as_ref(), ().into(), true)
641                        .await
642                        .with_context(|| format!("loading image asset from {uri:?}"))?;
643                    let mut body = Vec::new();
644                    response.body_mut().read_to_end(&mut body).await?;
645                    if !response.status().is_success() {
646                        let mut body = String::from_utf8_lossy(&body).into_owned();
647                        let first_line = body.lines().next().unwrap_or("").trim_end();
648                        body.truncate(first_line.len());
649                        return Err(ImageCacheError::BadStatus {
650                            uri,
651                            status: response.status(),
652                            body,
653                        });
654                    }
655                    body
656                }
657                Resource::Embedded(path) => {
658                    let data = asset_source.load(&path).ok().flatten();
659                    if let Some(data) = data {
660                        data.to_vec()
661                    } else {
662                        return Err(ImageCacheError::Asset(
663                            format!("Embedded resource not found: {}", path).into(),
664                        ));
665                    }
666                }
667            };
668
669            if let Ok(format) = image::guess_format(&bytes) {
670                let data = match format {
671                    ImageFormat::Gif => {
672                        let decoder = GifDecoder::new(Cursor::new(&bytes))?;
673                        let mut frames = SmallVec::new();
674
675                        for frame in decoder.into_frames() {
676                            match frame {
677                                Ok(mut frame) => {
678                                    // Convert from RGBA to BGRA.
679                                    for pixel in frame.buffer_mut().chunks_exact_mut(4) {
680                                        pixel.swap(0, 2);
681                                    }
682                                    frames.push(frame);
683                                }
684                                Err(err) => {
685                                    log::debug!(
686                                        "Skipping GIF frame in {source:?} due to decode error: {err}"
687                                    );
688                                }
689                            }
690                        }
691
692                        if frames.is_empty() {
693                            return Err(ImageCacheError::Other(Arc::new(anyhow::anyhow!(
694                                "GIF could not be decoded: all frames failed ({source:?})"
695                            ))));
696                        }
697
698                        frames
699                    }
700                    ImageFormat::WebP => {
701                        let mut decoder = WebPDecoder::new(Cursor::new(&bytes))?;
702
703                        if decoder.has_animation() {
704                            let _ = decoder.set_background_color(Rgba([0, 0, 0, 0]));
705                            let mut frames = SmallVec::new();
706
707                            for frame in decoder.into_frames() {
708                                match frame {
709                                    Ok(mut frame) => {
710                                        // Convert from RGBA to BGRA.
711                                        for pixel in frame.buffer_mut().chunks_exact_mut(4) {
712                                            pixel.swap(0, 2);
713                                        }
714                                        frames.push(frame);
715                                    }
716                                    Err(err) => {
717                                        log::debug!(
718                                            "Skipping WebP frame in {source:?} due to decode error: {err}"
719                                        );
720                                    }
721                                }
722                            }
723
724                            if frames.is_empty() {
725                                return Err(ImageCacheError::Other(Arc::new(anyhow::anyhow!(
726                                    "WebP could not be decoded: all frames failed ({source:?})"
727                                ))));
728                            }
729
730                            frames
731                        } else {
732                            decode_static_image_from_decoder(decoder)?
733                        }
734                    }
735                    _ => decode_static_image(&bytes, format)?,
736                };
737
738                Ok(Arc::new(RenderImage::new(data)))
739            } else {
740                svg_renderer
741                    .render_single_frame(&bytes, 1.0)
742                    .map_err(Into::into)
743            }
744        }
745    }
746}
747
748/// An error that can occur when interacting with the image cache.
749#[derive(Debug, Error, Clone)]
750pub enum ImageCacheError {
751    /// Some other kind of error occurred
752    #[error("error: {0}")]
753    Other(#[from] Arc<anyhow::Error>),
754    /// An error that occurred while reading the image from disk.
755    #[error("IO error: {0}")]
756    Io(Arc<std::io::Error>),
757    /// An error that occurred while processing an image.
758    #[error("unexpected http status for {uri}: {status}, body: {body}")]
759    BadStatus {
760        /// The URI of the image.
761        uri: SharedUri,
762        /// The HTTP status code.
763        status: http_client::StatusCode,
764        /// The HTTP response body.
765        body: String,
766    },
767    /// An error that occurred while processing an asset.
768    #[error("asset error: {0}")]
769    Asset(SharedString),
770    /// An error that occurred while processing an image.
771    #[error("image error: {0}")]
772    Image(Arc<ImageError>),
773    /// An error that occurred while processing an SVG.
774    #[error("svg error: {0}")]
775    Usvg(Arc<usvg::Error>),
776}
777
778impl From<anyhow::Error> for ImageCacheError {
779    fn from(value: anyhow::Error) -> Self {
780        Self::Other(Arc::new(value))
781    }
782}
783
784impl From<io::Error> for ImageCacheError {
785    fn from(value: io::Error) -> Self {
786        Self::Io(Arc::new(value))
787    }
788}
789
790impl From<usvg::Error> for ImageCacheError {
791    fn from(value: usvg::Error) -> Self {
792        Self::Usvg(Arc::new(value))
793    }
794}
795
796impl From<image::ImageError> for ImageCacheError {
797    fn from(value: image::ImageError) -> Self {
798        Self::Image(Arc::new(value))
799    }
800}
801
802#[cfg(test)]
803mod tests {
804    use super::*;
805    use crate::{ParentElement as _, TestAppContext, canvas, div, point, px, size};
806    use image::{Frame, ImageBuffer, Rgba};
807
808    const TEST_IMG_ID: &str = "test-img";
809
810    fn test_image(frame_count: usize) -> Arc<RenderImage> {
811        let frame = Frame::new(ImageBuffer::from_pixel(1, 1, Rgba([0, 0, 0, 0])));
812        Arc::new(RenderImage::new(SmallVec::from_iter(
813            (0..frame_count).map(|_| frame.clone()),
814        )))
815    }
816
817    fn test_image_with_size(width: u32, height: u32) -> Arc<RenderImage> {
818        let frame = Frame::new(ImageBuffer::from_pixel(width, height, Rgba([0, 0, 0, 0])));
819        Arc::new(RenderImage::new(SmallVec::from_elem(frame, 1)))
820    }
821
822    /// Overwrites the cached `frame_index` of the sibling `img` during paint.
823    fn seed_frame_index(frame_index: usize) -> impl IntoElement {
824        canvas(
825            |_, _, _| (),
826            move |_, _, window, _| {
827                window.with_global_id(TEST_IMG_ID.into(), |id, window| {
828                    window.with_element_state::<ImgState, _>(id, |state, _| {
829                        let mut state = state.expect("img state should be initialized");
830                        state.frame_index = frame_index;
831                        ((), state)
832                    });
833                });
834            },
835        )
836    }
837
838    #[gpui::test]
839    fn zero_frame_image_does_not_panic_on_paint(cx: &mut TestAppContext) {
840        cx.add_empty_window()
841            .draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| {
842                img(ImageSource::Render(test_image(0))).into_any_element()
843            });
844    }
845
846    #[gpui::test]
847    fn image_object_fit_cover_crops_to_element_bounds(cx: &mut TestAppContext) {
848        let window = cx.add_empty_window();
849        let image = test_image_with_size(200, 100);
850        window.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| {
851            img(ImageSource::Render(image.clone()))
852                .size_full()
853                .object_fit(ObjectFit::Fill)
854                .into_any_element()
855        });
856        let full_tile_bounds = window.update(|window, _| {
857            window
858                .rendered_frame
859                .scene
860                .polychrome_sprites
861                .last()
862                .expect("fill image should paint a sprite")
863                .tile
864                .bounds
865        });
866
867        window.draw(point(px(10.), px(20.)), size(px(100.), px(100.)), |_, _| {
868            img(ImageSource::Render(image))
869                .size_full()
870                .object_fit(ObjectFit::Cover)
871                .into_any_element()
872        });
873
874        let (rendered_bounds, rendered_tile_bounds, scale_factor) = window.update(|window, _| {
875            let sprite = window
876                .rendered_frame
877                .scene
878                .polychrome_sprites
879                .last()
880                .expect("cover image should paint a sprite");
881            (sprite.bounds, sprite.tile.bounds, window.scale_factor())
882        });
883        assert_eq!(
884            rendered_bounds,
885            Bounds {
886                origin: point(px(10.).scale(scale_factor), px(20.).scale(scale_factor)),
887                size: size(px(100.).scale(scale_factor), px(100.).scale(scale_factor)),
888            }
889        );
890        assert_eq!(
891            (
892                rendered_tile_bounds.origin.x.0 - full_tile_bounds.origin.x.0,
893                rendered_tile_bounds.origin.y.0 - full_tile_bounds.origin.y.0,
894                rendered_tile_bounds.size.width.0,
895                rendered_tile_bounds.size.height.0,
896            ),
897            (50, 0, 100, 100),
898        );
899    }
900
901    #[gpui::test]
902    fn explicit_aspect_ratio_is_not_overridden_by_intrinsic_ratio(cx: &mut TestAppContext) {
903        let window = cx.add_empty_window();
904
905        // A portrait image in a square container
906        window.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| {
907            div()
908                .size(px(100.))
909                .overflow_hidden()
910                .child(
911                    img(ImageSource::Render(test_image_with_size(100, 200)))
912                        .size_full()
913                        .aspect_square()
914                        .object_fit(ObjectFit::Contain),
915                )
916                .into_any_element()
917        });
918
919        let (rendered_bounds, scale_factor) = window.update(|window, _| {
920            let sprite = window
921                .rendered_frame
922                .scene
923                .polychrome_sprites
924                .last()
925                .expect("contained image should paint a sprite");
926            (sprite.bounds, window.scale_factor())
927        });
928
929        // The element stays 100x100, so the image is letterboxed horizontally
930        assert_eq!(
931            rendered_bounds,
932            Bounds {
933                origin: point(px(25.).scale(scale_factor), px(0.).scale(scale_factor)),
934                size: size(px(50.).scale(scale_factor), px(100.).scale(scale_factor)),
935            }
936        );
937    }
938
939    #[gpui::test]
940    fn image_object_fit_cover_clamps_corner_radii_to_visible_bounds(cx: &mut TestAppContext) {
941        let window = cx.add_empty_window();
942        window.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| {
943            img(ImageSource::Render(test_image_with_size(200, 100)))
944                .size_full()
945                .rounded(px(100.))
946                .object_fit(ObjectFit::Cover)
947                .into_any_element()
948        });
949
950        let (corner_radius, expected_corner_radius) = window.update(|window, _| {
951            (
952                window
953                    .rendered_frame
954                    .scene
955                    .polychrome_sprites
956                    .last()
957                    .map(|sprite| sprite.corner_radii.top_left),
958                px(50.).scale(window.scale_factor()),
959            )
960        });
961        assert_eq!(corner_radius, Some(expected_corner_radius));
962    }
963
964    #[gpui::test]
965    fn stale_frame_index_is_clamped_when_image_changes(cx: &mut TestAppContext) {
966        let window = cx.add_empty_window();
967
968        // Assert that a cached frame_index from a previous multi-frame image
969        // does not cause an out-of-bounds panic when the image is replaced
970        // with one that has fewer frames.
971        window.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| {
972            div()
973                .child(img(ImageSource::Render(test_image(5))).id(TEST_IMG_ID))
974                .child(seed_frame_index(4))
975                .into_any_element()
976        });
977        window.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| {
978            img(ImageSource::Render(test_image(1)))
979                .id(TEST_IMG_ID)
980                .into_any_element()
981        });
982    }
983}