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