1use crate::{
2 AnyElement, AnyImageCache, App, AppContext, Asset, AssetLogger, Bounds, DefiniteLength,
3 Element, ElementId, Entity, GlobalElementId, Hitbox, Image, ImageCache, InspectorElementId,
4 InteractiveElement, Interactivity, IntoElement, LayoutId, Length, ObjectFit, Pixels,
5 RenderImage, Resource, SharedString, SharedUri, StyleRefinement, Styled, Task, Window,
6 decode_static_image, decode_static_image_from_decoder, px,
7};
8use anyhow::Result;
9
10use futures::Future;
11use gpui_util::ResultExt;
12use image::{
13 AnimationDecoder, ImageError, ImageFormat, Rgba,
14 codecs::{gif::GifDecoder, webp::WebPDecoder},
15};
16use scheduler::Instant;
17use smallvec::SmallVec;
18use std::{
19 fs,
20 io::{self, Cursor},
21 ops::{Deref, DerefMut},
22 path::{Path, PathBuf},
23 str::FromStr,
24 sync::Arc,
25 time::Duration,
26};
27use thiserror::Error;
28
29use super::{Stateful, StatefulInteractiveElement};
30
31pub const LOADING_DELAY: Duration = Duration::from_millis(200);
35
36pub type ImgResourceLoader = AssetLogger<ImageAssetLoader>;
42
43#[derive(Clone)]
61pub enum ImageSource {
62 Resource(Resource),
66 Render(Arc<RenderImage>),
68 Image(Arc<Image>),
71 Custom(Arc<dyn Fn(&mut Window, &mut App) -> Option<Result<Arc<RenderImage>, ImageCacheError>>>),
77}
78
79fn is_uri(uri: &str) -> bool {
80 url::Url::from_str(uri).is_ok()
81}
82
83impl From<SharedUri> for ImageSource {
84 fn from(value: SharedUri) -> Self {
85 Self::Resource(Resource::Uri(value))
86 }
87}
88
89impl<'a> From<&'a str> for ImageSource {
90 fn from(s: &'a str) -> Self {
91 if is_uri(s) {
92 Self::Resource(Resource::Uri(s.to_string().into()))
93 } else {
94 Self::Resource(Resource::Embedded(s.to_string().into()))
95 }
96 }
97}
98
99impl From<String> for ImageSource {
100 fn from(s: String) -> Self {
101 if is_uri(&s) {
102 Self::Resource(Resource::Uri(s.into()))
103 } else {
104 Self::Resource(Resource::Embedded(s.into()))
105 }
106 }
107}
108
109impl From<SharedString> for ImageSource {
110 fn from(s: SharedString) -> Self {
111 s.as_ref().into()
112 }
113}
114
115impl From<&Path> for ImageSource {
116 fn from(value: &Path) -> Self {
117 Self::Resource(value.to_path_buf().into())
118 }
119}
120
121impl From<Arc<Path>> for ImageSource {
122 fn from(value: Arc<Path>) -> Self {
123 Self::Resource(value.into())
124 }
125}
126
127impl From<PathBuf> for ImageSource {
128 fn from(value: PathBuf) -> Self {
129 Self::Resource(value.into())
130 }
131}
132
133impl From<Arc<RenderImage>> for ImageSource {
134 fn from(value: Arc<RenderImage>) -> Self {
135 Self::Render(value)
136 }
137}
138
139impl From<Arc<Image>> for ImageSource {
140 fn from(value: Arc<Image>) -> Self {
141 Self::Image(value)
142 }
143}
144
145impl<F> From<F> for ImageSource
146where
147 F: Fn(&mut Window, &mut App) -> Option<Result<Arc<RenderImage>, ImageCacheError>> + 'static,
148{
149 fn from(value: F) -> Self {
150 Self::Custom(Arc::new(value))
151 }
152}
153
154pub struct ImageStyle {
158 grayscale: bool,
159 object_fit: ObjectFit,
160 loading: Option<Box<dyn Fn() -> AnyElement>>,
161 fallback: Option<Box<dyn Fn() -> AnyElement>>,
162}
163
164impl Default for ImageStyle {
165 fn default() -> Self {
166 Self {
167 grayscale: false,
168 object_fit: ObjectFit::Contain,
169 loading: None,
170 fallback: None,
171 }
172 }
173}
174
175pub trait StyledImage: Sized {
179 fn image_style(&mut self) -> &mut ImageStyle;
181
182 fn grayscale(mut self, grayscale: bool) -> Self {
184 self.image_style().grayscale = grayscale;
185 self
186 }
187
188 fn object_fit(mut self, object_fit: ObjectFit) -> Self {
204 self.image_style().object_fit = object_fit;
205 self
206 }
207
208 fn with_fallback(mut self, fallback: impl Fn() -> AnyElement + 'static) -> Self {
214 self.image_style().fallback = Some(Box::new(fallback));
215 self
216 }
217
218 fn with_loading(mut self, loading: impl Fn() -> AnyElement + 'static) -> Self {
247 self.image_style().loading = Some(Box::new(loading));
248 self
249 }
250}
251
252impl StyledImage for Img {
253 fn image_style(&mut self) -> &mut ImageStyle {
254 &mut self.style
255 }
256}
257
258impl StyledImage for Stateful<Img> {
259 fn image_style(&mut self) -> &mut ImageStyle {
260 &mut self.element.style
261 }
262}
263
264pub struct Img {
269 interactivity: Interactivity,
270 source: ImageSource,
271 style: ImageStyle,
272 image_cache: Option<AnyImageCache>,
273}
274
275#[track_caller]
299pub fn img(source: impl Into<ImageSource>) -> Img {
300 Img {
301 interactivity: Interactivity::new(),
302 source: source.into(),
303 style: ImageStyle::default(),
304 image_cache: None,
305 }
306}
307
308impl Img {
309 pub fn extensions() -> &'static [&'static str] {
322 &[
324 "avif", "jpg", "jpeg", "png", "gif", "webp", "tif", "tiff", "tga", "dds", "bmp", "ico",
325 "hdr", "exr", "pbm", "pam", "ppm", "pgm", "ff", "farbfeld", "qoi", "svg",
326 ]
327 }
328
329 #[inline]
335 pub fn image_cache<I: ImageCache>(self, image_cache: &Entity<I>) -> Self {
336 Self {
337 image_cache: Some(image_cache.clone().into()),
338 ..self
339 }
340 }
341}
342
343impl Deref for Stateful<Img> {
344 type Target = Img;
345
346 fn deref(&self) -> &Self::Target {
347 &self.element
348 }
349}
350
351impl DerefMut for Stateful<Img> {
352 fn deref_mut(&mut self) -> &mut Self::Target {
353 &mut self.element
354 }
355}
356
357const MIN_FRAME_DELAY: Duration = Duration::from_millis(1);
359
360fn advance_animation(
364 mut frame_index: usize,
365 elapsed: Duration,
366 frame_count: usize,
367 frame_delay: impl Fn(usize) -> Duration,
368) -> (usize, Duration) {
369 if frame_count == 0 {
370 return (0, elapsed);
371 }
372
373 let delay = |index| frame_delay(index).max(MIN_FRAME_DELAY);
374 frame_index %= frame_count;
375
376 let cycle_nanos = (0..frame_count)
378 .map(|index| delay(index).as_nanos())
379 .sum::<u128>();
380 let remainder_nanos = elapsed.as_nanos() % cycle_nanos;
381 let mut remaining = Duration::new(
382 (remainder_nanos / 1_000_000_000) as u64,
383 (remainder_nanos % 1_000_000_000) as u32,
384 );
385
386 while remaining >= delay(frame_index) {
387 remaining -= delay(frame_index);
388 frame_index = (frame_index + 1) % frame_count;
389 }
390 (frame_index, remaining)
391}
392
393struct ImgState {
395 frame_index: usize,
396 last_frame_time: Option<Instant>,
397 started_loading: Option<(Instant, Task<()>)>,
398}
399
400pub struct ImgLayoutState {
405 frame_index: usize,
406 replacement: Option<AnyElement>,
407}
408
409impl Element for Img {
410 type RequestLayoutState = ImgLayoutState;
411 type PrepaintState = Option<Hitbox>;
412
413 fn id(&self) -> Option<ElementId> {
414 self.interactivity.element_id.clone()
415 }
416
417 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
418 self.interactivity.source_location()
419 }
420
421 fn request_layout(
422 &mut self,
423 global_id: Option<&GlobalElementId>,
424 inspector_id: Option<&InspectorElementId>,
425 window: &mut Window,
426 cx: &mut App,
427 ) -> (LayoutId, Self::RequestLayoutState) {
428 let mut layout_state = ImgLayoutState {
429 frame_index: 0,
430 replacement: None,
431 };
432
433 window.with_optional_element_state(global_id, |state, window| {
434 let mut state = state.map(|state| {
435 state.unwrap_or(ImgState {
436 frame_index: 0,
437 last_frame_time: None,
438 started_loading: None,
439 })
440 });
441
442 let mut frame_index = state.as_ref().map(|state| state.frame_index).unwrap_or(0);
443
444 let layout_id = self.interactivity.request_layout(
445 global_id,
446 inspector_id,
447 window,
448 cx,
449 |mut style, window, cx| {
450 let mut replacement_id = None;
451
452 match self.source.use_data(
453 self.image_cache
454 .clone()
455 .or_else(|| window.image_cache_stack.last().cloned()),
456 window,
457 cx,
458 ) {
459 Some(Ok(data)) => {
460 let frame_count = data.frame_count();
461 let max_frame_index = frame_count.saturating_sub(1);
462
463 if let Some(state) = &mut state {
464 state.frame_index = state.frame_index.min(max_frame_index);
465 if frame_count > 1 && !cx.reduce_motion() {
466 if window.is_window_active() {
467 let current_time = cx.background_executor().now();
468 if let Some(last_frame_time) = state.last_frame_time {
469 let elapsed = current_time - last_frame_time;
470 let (frame_index, remainder) = advance_animation(
471 state.frame_index,
472 elapsed,
473 frame_count,
474 |index| Duration::from(data.delay(index)),
475 );
476 state.frame_index = frame_index;
477 state.last_frame_time = Some(current_time - remainder);
478 } else {
479 state.last_frame_time = Some(current_time);
480 }
481 } else {
482 state.last_frame_time = None;
483 }
484 } else {
485 state.last_frame_time = None;
486 }
487 state.started_loading = None;
488 frame_index = state.frame_index;
489 }
490
491 let image_size = data.render_size(frame_index);
492
493 if style.aspect_ratio.is_none() {
494 style.aspect_ratio = Some(image_size.width / image_size.height);
495 }
496
497 if let Length::Auto = style.size.width {
498 style.size.width = match style.size.height {
499 Length::Definite(DefiniteLength::Absolute(abs_length)) => {
500 let height_px = abs_length.to_pixels(window.rem_size());
501 Length::Definite(
502 px(image_size.width.0 * height_px.0
503 / image_size.height.0)
504 .into(),
505 )
506 }
507 _ => Length::Definite(image_size.width.into()),
508 };
509 }
510
511 if let Length::Auto = style.size.height {
512 style.size.height = match style.size.width {
513 Length::Definite(DefiniteLength::Absolute(abs_length)) => {
514 let width_px = abs_length.to_pixels(window.rem_size());
515 Length::Definite(
516 px(image_size.height.0 * width_px.0
517 / image_size.width.0)
518 .into(),
519 )
520 }
521 _ => Length::Definite(image_size.height.into()),
522 };
523 }
524
525 if global_id.is_some()
526 && data.frame_count() > 1
527 && window.is_window_active()
528 && !cx.reduce_motion()
529 {
530 window.request_animation_frame();
531 }
532 }
533 Some(_err) => {
534 if let Some(fallback) = self.style.fallback.as_ref() {
535 let mut element = fallback();
536 replacement_id = Some(element.request_layout(window, cx));
537 layout_state.replacement = Some(element);
538 }
539 if let Some(state) = &mut state {
540 state.started_loading = None;
541 }
542 }
543 None => {
544 if let Some(state) = &mut state {
545 if let Some((started_loading, _)) = state.started_loading {
546 if cx.background_executor().now() - started_loading
547 > LOADING_DELAY
548 && let Some(loading) = self.style.loading.as_ref()
549 {
550 let mut element = loading();
551 replacement_id = Some(element.request_layout(window, cx));
552 layout_state.replacement = Some(element);
553 }
554 } else {
555 let current_view = window.current_view();
556 let task = window.spawn(cx, async move |cx| {
557 cx.background_executor().timer(LOADING_DELAY).await;
558 cx.update(move |_, cx| {
559 cx.notify(current_view);
560 })
561 .ok();
562 });
563 state.started_loading =
564 Some((cx.background_executor().now(), task));
565 }
566 }
567 }
568 }
569
570 window.request_layout(style, replacement_id, cx)
571 },
572 );
573
574 layout_state.frame_index = frame_index;
575
576 ((layout_id, layout_state), state)
577 })
578 }
579
580 fn prepaint(
581 &mut self,
582 global_id: Option<&GlobalElementId>,
583 inspector_id: Option<&InspectorElementId>,
584 bounds: Bounds<Pixels>,
585 request_layout: &mut Self::RequestLayoutState,
586 window: &mut Window,
587 cx: &mut App,
588 ) -> Self::PrepaintState {
589 self.interactivity.prepaint(
590 global_id,
591 inspector_id,
592 bounds,
593 bounds.size,
594 window,
595 cx,
596 |_, _, hitbox, window, cx| {
597 if let Some(replacement) = &mut request_layout.replacement {
598 replacement.prepaint(window, cx);
599 }
600
601 hitbox
602 },
603 )
604 }
605
606 fn paint(
607 &mut self,
608 global_id: Option<&GlobalElementId>,
609 inspector_id: Option<&InspectorElementId>,
610 bounds: Bounds<Pixels>,
611 layout_state: &mut Self::RequestLayoutState,
612 hitbox: &mut Self::PrepaintState,
613 window: &mut Window,
614 cx: &mut App,
615 ) {
616 let source = self.source.clone();
617 self.interactivity.paint(
618 global_id,
619 inspector_id,
620 bounds,
621 hitbox.as_ref(),
622 window,
623 cx,
624 |style, window, cx| {
625 if let Some(Ok(data)) = source.use_data(
626 self.image_cache
627 .clone()
628 .or_else(|| window.image_cache_stack.last().cloned()),
629 window,
630 cx,
631 ) {
632 if data.frame_count() == 0 {
633 return;
634 }
635 let new_bounds = self
636 .style
637 .object_fit
638 .get_bounds(bounds, data.size(layout_state.frame_index));
639 let corner_radii = style.corner_radii.to_pixels(window.rem_size());
640 window
641 .paint_image(
642 bounds,
643 new_bounds,
644 corner_radii,
645 style.corner_smoothing.unwrap_or_default(),
646 data,
647 layout_state.frame_index,
648 self.style.grayscale,
649 )
650 .log_err();
651 } else if let Some(replacement) = &mut layout_state.replacement {
652 replacement.paint(window, cx);
653 }
654 },
655 )
656 }
657}
658
659impl Styled for Img {
660 fn style(&mut self) -> &mut StyleRefinement {
661 &mut self.interactivity.base_style
662 }
663}
664
665impl InteractiveElement for Img {
666 fn interactivity(&mut self) -> &mut Interactivity {
667 &mut self.interactivity
668 }
669}
670
671impl IntoElement for Img {
672 type Element = Self;
673
674 fn into_element(self) -> Self::Element {
675 self
676 }
677}
678
679impl StatefulInteractiveElement for Img {}
680
681impl ImageSource {
682 pub(crate) fn use_data(
683 &self,
684 cache: Option<AnyImageCache>,
685 window: &mut Window,
686 cx: &mut App,
687 ) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
688 match self {
689 ImageSource::Resource(resource) => {
690 if let Some(cache) = cache {
691 cache.load(resource, window, cx)
692 } else {
693 window.use_asset::<ImgResourceLoader>(resource, cx)
694 }
695 }
696 ImageSource::Custom(loading_fn) => loading_fn(window, cx),
697 ImageSource::Render(data) => Some(Ok(data.to_owned())),
698 ImageSource::Image(data) => window.use_asset::<AssetLogger<ImageDecoder>>(data, cx),
699 }
700 }
701
702 pub(crate) fn get_data(
703 &self,
704 cache: Option<AnyImageCache>,
705 window: &mut Window,
706 cx: &mut App,
707 ) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
708 match self {
709 ImageSource::Resource(resource) => {
710 if let Some(cache) = cache {
711 cache.load(resource, window, cx)
712 } else {
713 window.get_asset::<ImgResourceLoader>(resource, cx)
714 }
715 }
716 ImageSource::Custom(loading_fn) => loading_fn(window, cx),
717 ImageSource::Render(data) => Some(Ok(data.to_owned())),
718 ImageSource::Image(data) => window.get_asset::<AssetLogger<ImageDecoder>>(data, cx),
719 }
720 }
721
722 pub fn remove_asset(&self, cx: &mut App) {
727 match self {
728 ImageSource::Resource(resource) => {
729 cx.remove_asset::<ImgResourceLoader>(resource);
730 }
731 ImageSource::Custom(_) | ImageSource::Render(_) => {}
732 ImageSource::Image(data) => cx.remove_asset::<AssetLogger<ImageDecoder>>(data),
733 }
734 }
735
736 #[cfg(any(test, feature = "test-support"))]
741 pub fn is_asset_cached(&self, cx: &App) -> bool {
742 match self {
743 ImageSource::Resource(resource) => cx.has_asset::<ImgResourceLoader>(resource),
744 ImageSource::Custom(_) | ImageSource::Render(_) => false,
745 ImageSource::Image(data) => cx.has_asset::<AssetLogger<ImageDecoder>>(data),
746 }
747 }
748}
749
750#[derive(Clone)]
751enum ImageDecoder {}
752
753impl Asset for ImageDecoder {
754 type Source = Arc<Image>;
755 type Output = Result<Arc<RenderImage>, ImageCacheError>;
756
757 fn load(
758 source: Self::Source,
759 cx: &mut App,
760 ) -> impl Future<Output = Self::Output> + Send + 'static {
761 let renderer = cx.svg_renderer();
762 async move { source.to_image_data(renderer).map_err(Into::into) }
763 }
764}
765
766#[derive(Clone)]
774pub enum ImageAssetLoader {}
775
776impl Asset for ImageAssetLoader {
777 type Source = Resource;
778 type Output = Result<Arc<RenderImage>, ImageCacheError>;
779
780 fn load(
781 source: Self::Source,
782 cx: &mut App,
783 ) -> impl Future<Output = Self::Output> + Send + 'static {
784 let client = cx.http_client();
785 let svg_renderer = cx.svg_renderer();
788 let asset_registry = cx.assets().clone();
789 async move {
790 let bytes = match source.clone() {
791 Resource::Path(uri) => fs::read(uri.as_ref())?,
792 Resource::Uri(uri) => {
793 use anyhow::Context as _;
794
795 let response = client
796 .get(uri.as_ref(), true)
797 .await
798 .with_context(|| format!("loading image asset from {uri:?}"))?;
799 if !response.status.is_success() {
800 let mut error_body = String::from_utf8_lossy(&response.body).into_owned();
801 let first_line = error_body.lines().next().unwrap_or("").trim_end();
802 error_body.truncate(first_line.len());
803 return Err(ImageCacheError::BadStatus {
804 uri,
805 status: response.status,
806 body: error_body,
807 });
808 }
809 response.body
810 }
811 Resource::Embedded(path) => {
812 let data = asset_registry.load(&path);
813 if let Some(data) = data {
814 data.to_vec()
815 } else {
816 return Err(ImageCacheError::Asset(
817 format!("Embedded resource not found: {}", path).into(),
818 ));
819 }
820 }
821 };
822
823 if let Ok(format) = image::guess_format(&bytes) {
824 let data = match format {
825 ImageFormat::Gif => {
826 let decoder = GifDecoder::new(Cursor::new(&bytes))?;
827 let mut frames = SmallVec::new();
828
829 for frame in decoder.into_frames() {
830 match frame {
831 Ok(mut frame) => {
832 for pixel in frame.buffer_mut().chunks_exact_mut(4) {
834 pixel.swap(0, 2);
835 }
836 frames.push(frame);
837 }
838 Err(err) => {
839 log::debug!(
840 "Skipping GIF frame in {source:?} due to decode error: {err}"
841 );
842 }
843 }
844 }
845
846 if frames.is_empty() {
847 return Err(ImageCacheError::Other(Arc::new(anyhow::anyhow!(
848 "GIF could not be decoded: all frames failed ({source:?})"
849 ))));
850 }
851
852 frames
853 }
854 ImageFormat::WebP => {
855 let mut decoder = WebPDecoder::new(Cursor::new(&bytes))?;
856
857 if decoder.has_animation() {
858 let _ = decoder.set_background_color(Rgba([0, 0, 0, 0]));
859 let mut frames = SmallVec::new();
860
861 for frame in decoder.into_frames() {
862 match frame {
863 Ok(mut frame) => {
864 for pixel in frame.buffer_mut().chunks_exact_mut(4) {
866 pixel.swap(0, 2);
867 }
868 frames.push(frame);
869 }
870 Err(err) => {
871 log::debug!(
872 "Skipping WebP frame in {source:?} due to decode error: {err}"
873 );
874 }
875 }
876 }
877
878 if frames.is_empty() {
879 return Err(ImageCacheError::Other(Arc::new(anyhow::anyhow!(
880 "WebP could not be decoded: all frames failed ({source:?})"
881 ))));
882 }
883
884 frames
885 } else {
886 decode_static_image_from_decoder(decoder)?
887 }
888 }
889 _ => decode_static_image(&bytes, format)?,
890 };
891
892 Ok(Arc::new(RenderImage::new(data)))
893 } else {
894 svg_renderer
895 .render_single_frame(&bytes, 1.0)
896 .map_err(Into::into)
897 }
898 }
899 }
900}
901
902#[derive(Debug, Error, Clone)]
907pub enum ImageCacheError {
908 #[error("error: {0}")]
911 Other(#[from] Arc<anyhow::Error>),
912 #[error("IO error: {0}")]
914 Io(Arc<std::io::Error>),
915 #[error("unexpected http status for {uri}: {status}, body: {body}")]
917 BadStatus {
918 uri: SharedUri,
920 status: http::StatusCode,
922 body: String,
924 },
925 #[error("asset error: {0}")]
927 Asset(SharedString),
928 #[error("image error: {0}")]
930 Image(Arc<ImageError>),
931 #[error("svg error: {0}")]
933 Usvg(Arc<usvg::Error>),
934}
935
936impl From<anyhow::Error> for ImageCacheError {
937 fn from(value: anyhow::Error) -> Self {
938 Self::Other(Arc::new(value))
939 }
940}
941
942impl From<io::Error> for ImageCacheError {
943 fn from(value: io::Error) -> Self {
944 Self::Io(Arc::new(value))
945 }
946}
947
948impl From<usvg::Error> for ImageCacheError {
949 fn from(value: usvg::Error) -> Self {
950 Self::Usvg(Arc::new(value))
951 }
952}
953
954impl From<image::ImageError> for ImageCacheError {
955 fn from(value: image::ImageError) -> Self {
956 Self::Image(Arc::new(value))
957 }
958}
959
960#[cfg(test)]
961mod tests {
962 use super::*;
963 use crate::{
964 Context, ParentElement as _, Render, TestAppContext, Window, canvas, div, point, px, size,
965 };
966 use image::{Delay, Frame, ImageBuffer, Rgba};
967
968 const TEST_IMG_ID: &str = "test-img";
969
970 fn test_image(frame_count: usize) -> Arc<RenderImage> {
971 let frame = Frame::new(ImageBuffer::from_pixel(1, 1, Rgba([0, 0, 0, 0])));
972 Arc::new(RenderImage::new(SmallVec::from_iter(
973 (0..frame_count).map(|_| frame.clone()),
974 )))
975 }
976
977 fn test_image_with_size(width: u32, height: u32) -> Arc<RenderImage> {
978 let frame = Frame::new(ImageBuffer::from_pixel(width, height, Rgba([0, 0, 0, 0])));
979 Arc::new(RenderImage::new(SmallVec::from_elem(frame, 1)))
980 }
981
982 fn animated_image() -> Arc<RenderImage> {
984 let frames = [(1, 100), (2, 200), (3, 300)].map(|(width, delay_ms)| {
985 Frame::from_parts(
986 ImageBuffer::from_pixel(width, 1, Rgba([255, 0, 0, 255])),
987 0,
988 0,
989 Delay::from_numer_denom_ms(delay_ms, 1),
990 )
991 });
992 Arc::new(RenderImage::new(SmallVec::from_iter(frames)))
993 }
994
995 struct AnimatedImageView(Arc<RenderImage>);
996
997 impl Render for AnimatedImageView {
998 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
999 img(ImageSource::Render(self.0.clone()))
1000 .id(TEST_IMG_ID)
1001 .size_full()
1002 }
1003 }
1004
1005 #[test]
1006 fn advance_animation_steps_through_every_covered_frame() {
1007 let delays = [100, 200, 300].map(Duration::from_millis);
1008 let advance = |frame, elapsed_ms| {
1009 advance_animation(frame, Duration::from_millis(elapsed_ms), 3, |ix| delays[ix])
1010 };
1011
1012 assert_eq!(advance(0, 0), (0, Duration::ZERO));
1013 assert_eq!(advance(0, 99), (0, Duration::from_millis(99)));
1014 assert_eq!(advance(0, 340), (2, Duration::from_millis(40)));
1015 assert_eq!(advance(2, 375), (0, Duration::from_millis(75)));
1016 assert_eq!(advance(0, 750), (1, Duration::from_millis(50)));
1018 }
1019
1020 #[test]
1021 fn advance_animation_skips_whole_cycles_and_bounds_zero_delay_frames() {
1022 let delays = [
1023 Duration::ZERO,
1024 Duration::from_millis(2),
1025 Duration::from_millis(3),
1026 ];
1027 let elapsed = Duration::from_millis(4);
1029 let expected = advance_animation(1, elapsed, 3, |ix| delays[ix]);
1030 assert_eq!(
1031 advance_animation(1, elapsed + Duration::from_millis(6) * 500_000, 3, |ix| {
1032 delays[ix]
1033 }),
1034 expected
1035 );
1036 assert_eq!(
1037 advance_animation(0, Duration::from_millis(1), 2, |_| Duration::ZERO),
1038 (1, Duration::ZERO)
1039 );
1040 }
1041
1042 #[gpui::test]
1043 fn animated_image_follows_the_executor_clock(cx: &mut TestAppContext) {
1044 let (_, window) = cx.add_window_view(|_, _| AnimatedImageView(animated_image()));
1045 window.update(|window, _| window.activate());
1046 let clock = window.cx.background_executor.clone();
1047 let frame_width_after = |window: &mut crate::VisualTestContext, elapsed_ms: u64| {
1048 clock.advance_clock(Duration::from_millis(elapsed_ms));
1049 window.update(|window, cx| {
1050 window.simulate_next_frame(cx);
1051 window.draw(cx).clear(cx);
1052 window
1053 .rendered_frame
1054 .scene
1055 .polychrome_sprites
1056 .last()
1057 .expect("animated image should paint a sprite")
1058 .tile
1059 .bounds
1060 .size
1061 .width
1062 .0
1063 })
1064 };
1065
1066 for (elapsed_ms, expected_width) in [(0, 1), (99, 1), (1, 2), (1_100, 1), (99, 1), (1, 2)] {
1067 assert_eq!(
1068 frame_width_after(window, elapsed_ms),
1069 expected_width,
1070 "after {elapsed_ms} ms"
1071 );
1072 }
1073 }
1074
1075 fn seed_frame_index(frame_index: usize) -> impl IntoElement {
1077 canvas(
1078 |_, _, _| (),
1079 move |_, _, window, _| {
1080 window.with_global_id(TEST_IMG_ID.into(), |id, window| {
1081 window.with_element_state::<ImgState, _>(id, |state, _| {
1082 let mut state = state.expect("img state should be initialized");
1083 state.frame_index = frame_index;
1084 ((), state)
1085 });
1086 });
1087 },
1088 )
1089 }
1090
1091 #[gpui::test]
1092 fn zero_frame_image_does_not_panic_on_paint(cx: &mut TestAppContext) {
1093 cx.add_empty_window()
1094 .draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| {
1095 img(ImageSource::Render(test_image(0))).into_any_element()
1096 });
1097 }
1098
1099 #[gpui::test]
1100 fn image_object_fit_cover_crops_to_element_bounds(cx: &mut TestAppContext) {
1101 let window = cx.add_empty_window();
1102 let image = test_image_with_size(200, 100);
1103 window.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| {
1104 img(ImageSource::Render(image.clone()))
1105 .size_full()
1106 .object_fit(ObjectFit::Fill)
1107 .into_any_element()
1108 });
1109 let full_tile_bounds = window.update(|window, _| {
1110 window
1111 .rendered_frame
1112 .scene
1113 .polychrome_sprites
1114 .last()
1115 .expect("fill image should paint a sprite")
1116 .tile
1117 .bounds
1118 });
1119
1120 window.draw(point(px(10.), px(20.)), size(px(100.), px(100.)), |_, _| {
1121 img(ImageSource::Render(image))
1122 .size_full()
1123 .object_fit(ObjectFit::Cover)
1124 .into_any_element()
1125 });
1126
1127 let (rendered_bounds, rendered_tile_bounds, scale_factor) = window.update(|window, _| {
1128 let sprite = window
1129 .rendered_frame
1130 .scene
1131 .polychrome_sprites
1132 .last()
1133 .expect("cover image should paint a sprite");
1134 (sprite.bounds, sprite.tile.bounds, window.scale_factor())
1135 });
1136 assert_eq!(
1137 rendered_bounds,
1138 Bounds {
1139 origin: point(px(10.).scale(scale_factor), px(20.).scale(scale_factor)),
1140 size: size(px(100.).scale(scale_factor), px(100.).scale(scale_factor)),
1141 }
1142 );
1143 assert_eq!(
1144 (
1145 rendered_tile_bounds.origin.x.0 - full_tile_bounds.origin.x.0,
1146 rendered_tile_bounds.origin.y.0 - full_tile_bounds.origin.y.0,
1147 rendered_tile_bounds.size.width.0,
1148 rendered_tile_bounds.size.height.0,
1149 ),
1150 (50, 0, 100, 100),
1151 );
1152 }
1153
1154 #[gpui::test]
1155 fn explicit_aspect_ratio_is_not_overridden_by_intrinsic_ratio(cx: &mut TestAppContext) {
1156 let window = cx.add_empty_window();
1157
1158 window.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| {
1160 div()
1161 .size(px(100.))
1162 .overflow_hidden()
1163 .child(
1164 img(ImageSource::Render(test_image_with_size(100, 200)))
1165 .size_full()
1166 .aspect_square()
1167 .object_fit(ObjectFit::Contain),
1168 )
1169 .into_any_element()
1170 });
1171
1172 let (rendered_bounds, scale_factor) = window.update(|window, _| {
1173 let sprite = window
1174 .rendered_frame
1175 .scene
1176 .polychrome_sprites
1177 .last()
1178 .expect("contained image should paint a sprite");
1179 (sprite.bounds, window.scale_factor())
1180 });
1181
1182 assert_eq!(
1184 rendered_bounds,
1185 Bounds {
1186 origin: point(px(25.).scale(scale_factor), px(0.).scale(scale_factor)),
1187 size: size(px(50.).scale(scale_factor), px(100.).scale(scale_factor)),
1188 }
1189 );
1190 }
1191
1192 #[gpui::test]
1193 fn image_object_fit_cover_clamps_corner_radii_to_visible_bounds(cx: &mut TestAppContext) {
1194 let window = cx.add_empty_window();
1195 window.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| {
1196 img(ImageSource::Render(test_image_with_size(200, 100)))
1197 .size_full()
1198 .rounded(px(100.))
1199 .object_fit(ObjectFit::Cover)
1200 .into_any_element()
1201 });
1202
1203 let (corner_radius, expected_corner_radius) = window.update(|window, _| {
1204 (
1205 window
1206 .rendered_frame
1207 .scene
1208 .polychrome_sprites
1209 .last()
1210 .map(|sprite| sprite.corner_radii.top_left),
1211 px(50.).scale(window.scale_factor()),
1212 )
1213 });
1214 assert_eq!(corner_radius, Some(expected_corner_radius));
1215 }
1216
1217 #[gpui::test]
1218 fn stale_frame_index_is_clamped_when_image_changes(cx: &mut TestAppContext) {
1219 let window = cx.add_empty_window();
1220
1221 window.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| {
1225 div()
1226 .child(img(ImageSource::Render(test_image(5))).id(TEST_IMG_ID))
1227 .child(seed_frame_index(4))
1228 .into_any_element()
1229 });
1230 window.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| {
1231 img(ImageSource::Render(test_image(1)))
1232 .id(TEST_IMG_ID)
1233 .into_any_element()
1234 });
1235 }
1236}