Skip to main content

gpui_core/elements/
img.rs

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