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
38pub 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
87pub type ImgResourceLoader = AssetLogger<ImageAssetLoader>;
92
93#[derive(Clone)]
95pub enum ImageSource {
96 Resource(Resource),
98 Render(Arc<RenderImage>),
100 Image(Arc<Image>),
102 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
181pub 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
200pub trait StyledImage: Sized {
202 fn image_style(&mut self) -> &mut ImageStyle;
204
205 fn grayscale(mut self, grayscale: bool) -> Self {
207 self.image_style().grayscale = grayscale;
208 self
209 }
210
211 fn object_fit(mut self, object_fit: ObjectFit) -> Self {
213 self.image_style().object_fit = object_fit;
214 self
215 }
216
217 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 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
242pub struct Img {
244 interactivity: Interactivity,
245 source: ImageSource,
246 style: ImageStyle,
247 image_cache: Option<AnyImageCache>,
248}
249
250#[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 pub fn extensions() -> &'static [&'static str] {
264 SUPPORTED_IMAGE_EXTENSIONS
265 }
266
267 #[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
299struct ImgState {
301 frame_index: usize,
302 last_frame_time: Option<Instant>,
303 started_loading: Option<(Instant, Task<()>)>,
304}
305
306pub 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 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#[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 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 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 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#[derive(Debug, Error, Clone)]
747pub enum ImageCacheError {
748 #[error("error: {0}")]
750 Other(#[from] Arc<anyhow::Error>),
751 #[error("IO error: {0}")]
753 Io(Arc<std::io::Error>),
754 #[error("unexpected http status for {uri}: {status}, body: {body}")]
756 BadStatus {
757 uri: SharedUri,
759 status: http_client::StatusCode,
761 body: String,
763 },
764 #[error("asset error: {0}")]
766 Asset(SharedString),
767 #[error("image error: {0}")]
769 Image(Arc<ImageError>),
770 #[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 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 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}