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
27pub const LOADING_DELAY: Duration = Duration::from_millis(200);
29
30pub type ImgResourceLoader = AssetLogger<ImageAssetLoader>;
35
36#[derive(Clone)]
38pub enum ImageSource {
39 Resource(Resource),
41 Render(Arc<RenderImage>),
43 Image(Arc<Image>),
45 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
124pub 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
143pub trait StyledImage: Sized {
145 fn image_style(&mut self) -> &mut ImageStyle;
147
148 fn grayscale(mut self, grayscale: bool) -> Self {
150 self.image_style().grayscale = grayscale;
151 self
152 }
153
154 fn object_fit(mut self, object_fit: ObjectFit) -> Self {
156 self.image_style().object_fit = object_fit;
157 self
158 }
159
160 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 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
187pub struct Img {
189 interactivity: Interactivity,
190 source: ImageSource,
191 style: ImageStyle,
192 image_cache: Option<AnyImageCache>,
193}
194
195#[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 pub fn extensions() -> &'static [&'static str] {
209 &[
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 #[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
248struct ImgState {
250 frame_index: usize,
251 last_frame_time: Option<Instant>,
252 started_loading: Option<(Instant, Task<()>)>,
253}
254
255pub 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 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 #[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#[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 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#[derive(Debug, Error, Clone)]
684pub enum ImageCacheError {
685 #[error("error: {0}")]
687 Other(#[from] Arc<anyhow::Error>),
688 #[error("IO error: {0}")]
690 Io(Arc<std::io::Error>),
691 #[error("unexpected http status for {uri}: {status}, body: {body}")]
693 BadStatus {
694 uri: SharedUri,
696 status: http_client::StatusCode,
698 body: String,
700 },
701 #[error("asset error: {0}")]
703 Asset(SharedString),
704 #[error("image error: {0}")]
706 Image(Arc<ImageError>),
707 #[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 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 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 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 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}