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, px,
6};
7use anyhow::Result;
8
9use futures::Future;
10use gpui_util::ResultExt;
11use image::{
12 AnimationDecoder, DynamicImage, Frame, ImageError, ImageFormat, Rgba,
13 codecs::{gif::GifDecoder, webp::WebPDecoder},
14};
15use scheduler::Instant;
16use smallvec::SmallVec;
17use std::{
18 fs,
19 io::{self, Cursor},
20 ops::{Deref, DerefMut},
21 path::{Path, PathBuf},
22 str::FromStr,
23 sync::Arc,
24 time::Duration,
25};
26use thiserror::Error;
27
28use super::{Stateful, StatefulInteractiveElement};
29
30pub const LOADING_DELAY: Duration = Duration::from_millis(200);
32
33pub type ImgResourceLoader = AssetLogger<ImageAssetLoader>;
38
39#[derive(Clone)]
41pub enum ImageSource {
42 Resource(Resource),
44 Render(Arc<RenderImage>),
46 Image(Arc<Image>),
48 Custom(Arc<dyn Fn(&mut Window, &mut App) -> Option<Result<Arc<RenderImage>, ImageCacheError>>>),
50}
51
52fn is_uri(uri: &str) -> bool {
53 url::Url::from_str(uri).is_ok()
54}
55
56impl From<SharedUri> for ImageSource {
57 fn from(value: SharedUri) -> Self {
58 Self::Resource(Resource::Uri(value))
59 }
60}
61
62impl<'a> From<&'a str> for ImageSource {
63 fn from(s: &'a str) -> Self {
64 if is_uri(s) {
65 Self::Resource(Resource::Uri(s.to_string().into()))
66 } else {
67 Self::Resource(Resource::Embedded(s.to_string().into()))
68 }
69 }
70}
71
72impl From<String> for ImageSource {
73 fn from(s: String) -> Self {
74 if is_uri(&s) {
75 Self::Resource(Resource::Uri(s.into()))
76 } else {
77 Self::Resource(Resource::Embedded(s.into()))
78 }
79 }
80}
81
82impl From<SharedString> for ImageSource {
83 fn from(s: SharedString) -> Self {
84 s.as_ref().into()
85 }
86}
87
88impl From<&Path> for ImageSource {
89 fn from(value: &Path) -> Self {
90 Self::Resource(value.to_path_buf().into())
91 }
92}
93
94impl From<Arc<Path>> for ImageSource {
95 fn from(value: Arc<Path>) -> Self {
96 Self::Resource(value.into())
97 }
98}
99
100impl From<PathBuf> for ImageSource {
101 fn from(value: PathBuf) -> Self {
102 Self::Resource(value.into())
103 }
104}
105
106impl From<Arc<RenderImage>> for ImageSource {
107 fn from(value: Arc<RenderImage>) -> Self {
108 Self::Render(value)
109 }
110}
111
112impl From<Arc<Image>> for ImageSource {
113 fn from(value: Arc<Image>) -> Self {
114 Self::Image(value)
115 }
116}
117
118impl<F> From<F> for ImageSource
119where
120 F: Fn(&mut Window, &mut App) -> Option<Result<Arc<RenderImage>, ImageCacheError>> + 'static,
121{
122 fn from(value: F) -> Self {
123 Self::Custom(Arc::new(value))
124 }
125}
126
127pub struct ImageStyle {
129 grayscale: bool,
130 object_fit: ObjectFit,
131 loading: Option<Box<dyn Fn() -> AnyElement>>,
132 fallback: Option<Box<dyn Fn() -> AnyElement>>,
133}
134
135impl Default for ImageStyle {
136 fn default() -> Self {
137 Self {
138 grayscale: false,
139 object_fit: ObjectFit::Contain,
140 loading: None,
141 fallback: None,
142 }
143 }
144}
145
146pub trait StyledImage: Sized {
148 fn image_style(&mut self) -> &mut ImageStyle;
150
151 fn grayscale(mut self, grayscale: bool) -> Self {
153 self.image_style().grayscale = grayscale;
154 self
155 }
156
157 fn object_fit(mut self, object_fit: ObjectFit) -> Self {
159 self.image_style().object_fit = object_fit;
160 self
161 }
162
163 fn with_fallback(mut self, fallback: impl Fn() -> AnyElement + 'static) -> Self {
166 self.image_style().fallback = Some(Box::new(fallback));
167 self
168 }
169
170 fn with_loading(mut self, loading: impl Fn() -> AnyElement + 'static) -> Self {
173 self.image_style().loading = Some(Box::new(loading));
174 self
175 }
176}
177
178impl StyledImage for Img {
179 fn image_style(&mut self) -> &mut ImageStyle {
180 &mut self.style
181 }
182}
183
184impl StyledImage for Stateful<Img> {
185 fn image_style(&mut self) -> &mut ImageStyle {
186 &mut self.element.style
187 }
188}
189
190pub struct Img {
192 interactivity: Interactivity,
193 source: ImageSource,
194 style: ImageStyle,
195 image_cache: Option<AnyImageCache>,
196}
197
198#[track_caller]
200pub fn img(source: impl Into<ImageSource>) -> Img {
201 Img {
202 interactivity: Interactivity::new(),
203 source: source.into(),
204 style: ImageStyle::default(),
205 image_cache: None,
206 }
207}
208
209impl Img {
210 pub fn extensions() -> &'static [&'static str] {
212 &[
214 "avif", "jpg", "jpeg", "png", "gif", "webp", "tif", "tiff", "tga", "dds", "bmp", "ico",
215 "hdr", "exr", "pbm", "pam", "ppm", "pgm", "ff", "farbfeld", "qoi", "svg",
216 ]
217 }
218
219 #[inline]
229 pub fn image_cache<I: ImageCache>(self, image_cache: &Entity<I>) -> Self {
230 Self {
231 image_cache: Some(image_cache.clone().into()),
232 ..self
233 }
234 }
235}
236
237impl Deref for Stateful<Img> {
238 type Target = Img;
239
240 fn deref(&self) -> &Self::Target {
241 &self.element
242 }
243}
244
245impl DerefMut for Stateful<Img> {
246 fn deref_mut(&mut self) -> &mut Self::Target {
247 &mut self.element
248 }
249}
250
251struct ImgState {
253 frame_index: usize,
254 last_frame_time: Option<Instant>,
255 started_loading: Option<(Instant, Task<()>)>,
256}
257
258pub struct ImgLayoutState {
260 frame_index: usize,
261 replacement: Option<AnyElement>,
262}
263
264impl Element for Img {
265 type RequestLayoutState = ImgLayoutState;
266 type PrepaintState = Option<Hitbox>;
267
268 fn id(&self) -> Option<ElementId> {
269 self.interactivity.element_id.clone()
270 }
271
272 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
273 self.interactivity.source_location()
274 }
275
276 fn request_layout(
277 &mut self,
278 global_id: Option<&GlobalElementId>,
279 inspector_id: Option<&InspectorElementId>,
280 window: &mut Window,
281 cx: &mut App,
282 ) -> (LayoutId, Self::RequestLayoutState) {
283 let mut layout_state = ImgLayoutState {
284 frame_index: 0,
285 replacement: None,
286 };
287
288 window.with_optional_element_state(global_id, |state, window| {
289 let mut state = state.map(|state| {
290 state.unwrap_or(ImgState {
291 frame_index: 0,
292 last_frame_time: None,
293 started_loading: None,
294 })
295 });
296
297 let mut frame_index = state.as_ref().map(|state| state.frame_index).unwrap_or(0);
298
299 let layout_id = self.interactivity.request_layout(
300 global_id,
301 inspector_id,
302 window,
303 cx,
304 |mut style, window, cx| {
305 let mut replacement_id = None;
306
307 match self.source.use_data(
308 self.image_cache
309 .clone()
310 .or_else(|| window.image_cache_stack.last().cloned()),
311 window,
312 cx,
313 ) {
314 Some(Ok(data)) => {
315 let frame_count = data.frame_count();
316 let max_frame_index = frame_count.saturating_sub(1);
317
318 if let Some(state) = &mut state {
319 state.frame_index = state.frame_index.min(max_frame_index);
320 if frame_count > 1 {
321 if window.is_window_active() {
322 let current_time = Instant::now();
323 if let Some(last_frame_time) = state.last_frame_time {
324 let elapsed = current_time - last_frame_time;
325 let frame_duration =
326 Duration::from(data.delay(state.frame_index));
327
328 if elapsed >= frame_duration {
329 state.frame_index =
330 (state.frame_index + 1) % frame_count;
331 state.last_frame_time =
332 Some(current_time - (elapsed - frame_duration));
333 }
334 } else {
335 state.last_frame_time = Some(current_time);
336 }
337 } else {
338 state.last_frame_time = None;
339 }
340 } else {
341 state.last_frame_time = None;
342 }
343 state.started_loading = None;
344 frame_index = state.frame_index;
345 }
346
347 let image_size = data.render_size(frame_index);
348 style.aspect_ratio = Some(image_size.width / image_size.height);
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 {
382 window.request_animation_frame();
383 }
384 }
385 Some(_err) => {
386 if let Some(fallback) = self.style.fallback.as_ref() {
387 let mut element = fallback();
388 replacement_id = Some(element.request_layout(window, cx));
389 layout_state.replacement = Some(element);
390 }
391 if let Some(state) = &mut state {
392 state.started_loading = None;
393 }
394 }
395 None => {
396 if let Some(state) = &mut state {
397 if let Some((started_loading, _)) = state.started_loading {
398 if started_loading.elapsed() > LOADING_DELAY
399 && let Some(loading) = self.style.loading.as_ref()
400 {
401 let mut element = loading();
402 replacement_id = Some(element.request_layout(window, cx));
403 layout_state.replacement = Some(element);
404 }
405 } else {
406 let current_view = window.current_view();
407 let task = window.spawn(cx, async move |cx| {
408 cx.background_executor().timer(LOADING_DELAY).await;
409 cx.update(move |_, cx| {
410 cx.notify(current_view);
411 })
412 .ok();
413 });
414 state.started_loading = Some((Instant::now(), task));
415 }
416 }
417 }
418 }
419
420 window.request_layout(style, replacement_id, cx)
421 },
422 );
423
424 layout_state.frame_index = frame_index;
425
426 ((layout_id, layout_state), state)
427 })
428 }
429
430 fn prepaint(
431 &mut self,
432 global_id: Option<&GlobalElementId>,
433 inspector_id: Option<&InspectorElementId>,
434 bounds: Bounds<Pixels>,
435 request_layout: &mut Self::RequestLayoutState,
436 window: &mut Window,
437 cx: &mut App,
438 ) -> Self::PrepaintState {
439 self.interactivity.prepaint(
440 global_id,
441 inspector_id,
442 bounds,
443 bounds.size,
444 window,
445 cx,
446 |_, _, hitbox, window, cx| {
447 if let Some(replacement) = &mut request_layout.replacement {
448 replacement.prepaint(window, cx);
449 }
450
451 hitbox
452 },
453 )
454 }
455
456 fn paint(
457 &mut self,
458 global_id: Option<&GlobalElementId>,
459 inspector_id: Option<&InspectorElementId>,
460 bounds: Bounds<Pixels>,
461 layout_state: &mut Self::RequestLayoutState,
462 hitbox: &mut Self::PrepaintState,
463 window: &mut Window,
464 cx: &mut App,
465 ) {
466 let source = self.source.clone();
467 self.interactivity.paint(
468 global_id,
469 inspector_id,
470 bounds,
471 hitbox.as_ref(),
472 window,
473 cx,
474 |style, window, cx| {
475 if let Some(Ok(data)) = source.use_data(
476 self.image_cache
477 .clone()
478 .or_else(|| window.image_cache_stack.last().cloned()),
479 window,
480 cx,
481 ) {
482 if data.frame_count() == 0 {
483 return;
484 }
485 let new_bounds = self
486 .style
487 .object_fit
488 .get_bounds(bounds, data.size(layout_state.frame_index));
489 let corner_radii = style
490 .corner_radii
491 .to_pixels(window.rem_size())
492 .clamp_radii_for_quad_size(new_bounds.size);
493 window
494 .paint_image(
495 new_bounds,
496 corner_radii,
497 data,
498 layout_state.frame_index,
499 self.style.grayscale,
500 )
501 .log_err();
502 } else if let Some(replacement) = &mut layout_state.replacement {
503 replacement.paint(window, cx);
504 }
505 },
506 )
507 }
508}
509
510impl Styled for Img {
511 fn style(&mut self) -> &mut StyleRefinement {
512 &mut self.interactivity.base_style
513 }
514}
515
516impl InteractiveElement for Img {
517 fn interactivity(&mut self) -> &mut Interactivity {
518 &mut self.interactivity
519 }
520}
521
522impl IntoElement for Img {
523 type Element = Self;
524
525 fn into_element(self) -> Self::Element {
526 self
527 }
528}
529
530impl StatefulInteractiveElement for Img {}
531
532impl ImageSource {
533 pub(crate) fn use_data(
534 &self,
535 cache: Option<AnyImageCache>,
536 window: &mut Window,
537 cx: &mut App,
538 ) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
539 match self {
540 ImageSource::Resource(resource) => {
541 if let Some(cache) = cache {
542 cache.load(resource, window, cx)
543 } else {
544 window.use_asset::<ImgResourceLoader>(resource, cx)
545 }
546 }
547 ImageSource::Custom(loading_fn) => loading_fn(window, cx),
548 ImageSource::Render(data) => Some(Ok(data.to_owned())),
549 ImageSource::Image(data) => window.use_asset::<AssetLogger<ImageDecoder>>(data, cx),
550 }
551 }
552
553 pub(crate) fn get_data(
554 &self,
555 cache: Option<AnyImageCache>,
556 window: &mut Window,
557 cx: &mut App,
558 ) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
559 match self {
560 ImageSource::Resource(resource) => {
561 if let Some(cache) = cache {
562 cache.load(resource, window, cx)
563 } else {
564 window.get_asset::<ImgResourceLoader>(resource, cx)
565 }
566 }
567 ImageSource::Custom(loading_fn) => loading_fn(window, cx),
568 ImageSource::Render(data) => Some(Ok(data.to_owned())),
569 ImageSource::Image(data) => window.get_asset::<AssetLogger<ImageDecoder>>(data, cx),
570 }
571 }
572
573 pub fn remove_asset(&self, cx: &mut App) {
575 match self {
576 ImageSource::Resource(resource) => {
577 cx.remove_asset::<ImgResourceLoader>(resource);
578 }
579 ImageSource::Custom(_) | ImageSource::Render(_) => {}
580 ImageSource::Image(data) => cx.remove_asset::<AssetLogger<ImageDecoder>>(data),
581 }
582 }
583}
584
585#[derive(Clone)]
586enum ImageDecoder {}
587
588impl Asset for ImageDecoder {
589 type Source = Arc<Image>;
590 type Output = Result<Arc<RenderImage>, ImageCacheError>;
591
592 fn load(
593 source: Self::Source,
594 cx: &mut App,
595 ) -> impl Future<Output = Self::Output> + Send + 'static {
596 let renderer = cx.svg_renderer();
597 async move { source.to_image_data(renderer).map_err(Into::into) }
598 }
599}
600
601#[derive(Clone)]
603pub enum ImageAssetLoader {}
604
605impl Asset for ImageAssetLoader {
606 type Source = Resource;
607 type Output = Result<Arc<RenderImage>, ImageCacheError>;
608
609 fn load(
610 source: Self::Source,
611 cx: &mut App,
612 ) -> impl Future<Output = Self::Output> + Send + 'static {
613 let client = cx.http_client();
614 let svg_renderer = cx.svg_renderer();
617 let asset_source = cx.asset_source().clone();
618 async move {
619 let bytes = match source.clone() {
620 Resource::Path(uri) => fs::read(uri.as_ref())?,
621 Resource::Uri(uri) => {
622 use anyhow::Context as _;
623 use futures::AsyncReadExt as _;
624
625 let mut response = client
626 .get(uri.as_ref(), ().into(), true)
627 .await
628 .with_context(|| format!("loading image asset from {uri:?}"))?;
629 let mut body = Vec::new();
630 response.body_mut().read_to_end(&mut body).await?;
631 if !response.status().is_success() {
632 let mut body = String::from_utf8_lossy(&body).into_owned();
633 let first_line = body.lines().next().unwrap_or("").trim_end();
634 body.truncate(first_line.len());
635 return Err(ImageCacheError::BadStatus {
636 uri,
637 status: response.status(),
638 body,
639 });
640 }
641 body
642 }
643 Resource::Embedded(path) => {
644 let data = asset_source.load(&path).ok().flatten();
645 if let Some(data) = data {
646 data.to_vec()
647 } else {
648 return Err(ImageCacheError::Asset(
649 format!("Embedded resource not found: {}", path).into(),
650 ));
651 }
652 }
653 };
654
655 if let Ok(format) = image::guess_format(&bytes) {
656 let data = match format {
657 ImageFormat::Gif => {
658 let decoder = GifDecoder::new(Cursor::new(&bytes))?;
659 let mut frames = SmallVec::new();
660
661 for frame in decoder.into_frames() {
662 match frame {
663 Ok(mut frame) => {
664 for pixel in frame.buffer_mut().chunks_exact_mut(4) {
666 pixel.swap(0, 2);
667 }
668 frames.push(frame);
669 }
670 Err(err) => {
671 log::debug!(
672 "Skipping GIF frame in {source:?} due to decode error: {err}"
673 );
674 }
675 }
676 }
677
678 if frames.is_empty() {
679 return Err(ImageCacheError::Other(Arc::new(anyhow::anyhow!(
680 "GIF could not be decoded: all frames failed ({source:?})"
681 ))));
682 }
683
684 frames
685 }
686 ImageFormat::WebP => {
687 let mut decoder = WebPDecoder::new(Cursor::new(&bytes))?;
688
689 if decoder.has_animation() {
690 let _ = decoder.set_background_color(Rgba([0, 0, 0, 0]));
691 let mut frames = SmallVec::new();
692
693 for frame in decoder.into_frames() {
694 match frame {
695 Ok(mut frame) => {
696 for pixel in frame.buffer_mut().chunks_exact_mut(4) {
698 pixel.swap(0, 2);
699 }
700 frames.push(frame);
701 }
702 Err(err) => {
703 log::debug!(
704 "Skipping WebP frame in {source:?} due to decode error: {err}"
705 );
706 }
707 }
708 }
709
710 if frames.is_empty() {
711 return Err(ImageCacheError::Other(Arc::new(anyhow::anyhow!(
712 "WebP could not be decoded: all frames failed ({source:?})"
713 ))));
714 }
715
716 frames
717 } else {
718 let mut data = DynamicImage::from_decoder(decoder)?.into_rgba8();
719
720 for pixel in data.chunks_exact_mut(4) {
722 pixel.swap(0, 2);
723 }
724
725 SmallVec::from_elem(Frame::new(data), 1)
726 }
727 }
728 _ => {
729 let mut data =
730 image::load_from_memory_with_format(&bytes, format)?.into_rgba8();
731
732 for pixel in data.chunks_exact_mut(4) {
734 pixel.swap(0, 2);
735 }
736
737 SmallVec::from_elem(Frame::new(data), 1)
738 }
739 };
740
741 Ok(Arc::new(RenderImage::new(data)))
742 } else {
743 svg_renderer
744 .render_single_frame(&bytes, 1.0)
745 .map_err(Into::into)
746 }
747 }
748 }
749}
750
751#[derive(Debug, Error, Clone)]
753pub enum ImageCacheError {
754 #[error("error: {0}")]
756 Other(#[from] Arc<anyhow::Error>),
757 #[error("IO error: {0}")]
759 Io(Arc<std::io::Error>),
760 #[error("unexpected http status for {uri}: {status}, body: {body}")]
762 BadStatus {
763 uri: SharedUri,
765 status: http_client::StatusCode,
767 body: String,
769 },
770 #[error("asset error: {0}")]
772 Asset(SharedString),
773 #[error("image error: {0}")]
775 Image(Arc<ImageError>),
776 #[error("svg error: {0}")]
778 Usvg(Arc<usvg::Error>),
779}
780
781impl From<anyhow::Error> for ImageCacheError {
782 fn from(value: anyhow::Error) -> Self {
783 Self::Other(Arc::new(value))
784 }
785}
786
787impl From<io::Error> for ImageCacheError {
788 fn from(value: io::Error) -> Self {
789 Self::Io(Arc::new(value))
790 }
791}
792
793impl From<usvg::Error> for ImageCacheError {
794 fn from(value: usvg::Error) -> Self {
795 Self::Usvg(Arc::new(value))
796 }
797}
798
799impl From<image::ImageError> for ImageCacheError {
800 fn from(value: image::ImageError) -> Self {
801 Self::Image(Arc::new(value))
802 }
803}
804
805#[cfg(test)]
806mod tests {
807 use super::*;
808 use crate::{ParentElement as _, TestAppContext, canvas, div, point, px, size};
809 use image::{Frame, ImageBuffer, Rgba};
810
811 const TEST_IMG_ID: &str = "test-img";
812
813 fn test_image(frame_count: usize) -> Arc<RenderImage> {
814 let frame = Frame::new(ImageBuffer::from_pixel(1, 1, Rgba([0, 0, 0, 0])));
815 Arc::new(RenderImage::new(SmallVec::from_iter(
816 (0..frame_count).map(|_| frame.clone()),
817 )))
818 }
819
820 fn seed_frame_index(frame_index: usize) -> impl IntoElement {
822 canvas(
823 |_, _, _| (),
824 move |_, _, window, _| {
825 window.with_global_id(TEST_IMG_ID.into(), |id, window| {
826 window.with_element_state::<ImgState, _>(id, |state, _| {
827 let mut state = state.expect("img state should be initialized");
828 state.frame_index = frame_index;
829 ((), state)
830 });
831 });
832 },
833 )
834 }
835
836 #[gpui::test]
837 fn zero_frame_image_does_not_panic_on_paint(cx: &mut TestAppContext) {
838 cx.add_empty_window()
839 .draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| {
840 img(ImageSource::Render(test_image(0))).into_any_element()
841 });
842 }
843
844 #[gpui::test]
845 fn stale_frame_index_is_clamped_when_image_changes(cx: &mut TestAppContext) {
846 let window = cx.add_empty_window();
847
848 window.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| {
852 div()
853 .child(img(ImageSource::Render(test_image(5))).id(TEST_IMG_ID))
854 .child(seed_frame_index(4))
855 .into_any_element()
856 });
857 window.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| {
858 img(ImageSource::Render(test_image(1)))
859 .id(TEST_IMG_ID)
860 .into_any_element()
861 });
862 }
863}