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
30/// The delay before showing the loading state.
31pub const LOADING_DELAY: Duration = Duration::from_millis(200);
32
33/// A type alias to the resource loader that the `img()` element uses.
34///
35/// Note: that this is only for Resources, like URLs or file paths.
36/// Custom loaders, or external images will not use this asset loader
37pub type ImgResourceLoader = AssetLogger<ImageAssetLoader>;
38
39/// A source of image content.
40#[derive(Clone)]
41pub enum ImageSource {
42 /// The image content will be loaded from some resource location
43 Resource(Resource),
44 /// Cached image data
45 Render(Arc<RenderImage>),
46 /// Cached image data
47 Image(Arc<Image>),
48 /// A custom loading function to use
49 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
127/// The style of an image element.
128pub 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
146/// Style an image element.
147pub trait StyledImage: Sized {
148 /// Get a mutable [ImageStyle] from the element.
149 fn image_style(&mut self) -> &mut ImageStyle;
150
151 /// Set the image to be displayed in grayscale.
152 fn grayscale(mut self, grayscale: bool) -> Self {
153 self.image_style().grayscale = grayscale;
154 self
155 }
156
157 /// Set the object fit for the image.
158 fn object_fit(mut self, object_fit: ObjectFit) -> Self {
159 self.image_style().object_fit = object_fit;
160 self
161 }
162
163 /// Set a fallback function that will be invoked to render an error view should
164 /// the image fail to load.
165 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 /// Set a fallback function that will be invoked to render a view while the image
171 /// is still being loaded.
172 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
190/// An image element.
191pub struct Img {
192 interactivity: Interactivity,
193 source: ImageSource,
194 style: ImageStyle,
195 image_cache: Option<AnyImageCache>,
196}
197
198/// Create a new image element.
199#[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 /// A list of all format extensions currently supported by this img element
211 pub fn extensions() -> &'static [&'static str] {
212 // This is the list in [image::ImageFormat::from_extension] + `svg`
213 &[
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 /// Sets the image cache for the current node.
220 ///
221 /// If the `image_cache` is not explicitly provided, the function will determine the image cache by:
222 ///
223 /// 1. Checking if any ancestor node of the current node contains an `ImageCacheElement`, If such a node exists, the image cache specified by that ancestor will be used.
224 /// 2. If no ancestor node contains an `ImageCacheElement`, the global image cache will be used as a fallback.
225 ///
226 /// This mechanism provides a flexible way to manage image caching, allowing precise control when needed,
227 /// while ensuring a default behavior when no cache is explicitly specified.
228 #[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
251/// The image state between frames
252struct ImgState {
253 frame_index: usize,
254 last_frame_time: Option<Instant>,
255 started_loading: Option<(Instant, Task<()>)>,
256}
257
258/// The image layout state between frames
259pub 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 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 if let Some(state) = &mut state {
316 let frame_count = data.frame_count();
317 if frame_count > 1 {
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 }
338 state.started_loading = None;
339 }
340
341 let image_size = data.render_size(frame_index);
342 style.aspect_ratio = Some(image_size.width / image_size.height);
343
344 if let Length::Auto = style.size.width {
345 style.size.width = match style.size.height {
346 Length::Definite(DefiniteLength::Absolute(abs_length)) => {
347 let height_px = abs_length.to_pixels(window.rem_size());
348 Length::Definite(
349 px(image_size.width.0 * height_px.0
350 / image_size.height.0)
351 .into(),
352 )
353 }
354 _ => Length::Definite(image_size.width.into()),
355 };
356 }
357
358 if let Length::Auto = style.size.height {
359 style.size.height = match style.size.width {
360 Length::Definite(DefiniteLength::Absolute(abs_length)) => {
361 let width_px = abs_length.to_pixels(window.rem_size());
362 Length::Definite(
363 px(image_size.height.0 * width_px.0
364 / image_size.width.0)
365 .into(),
366 )
367 }
368 _ => Length::Definite(image_size.height.into()),
369 };
370 }
371
372 if global_id.is_some()
373 && data.frame_count() > 1
374 && window.is_window_active()
375 {
376 window.request_animation_frame();
377 }
378 }
379 Some(_err) => {
380 if let Some(fallback) = self.style.fallback.as_ref() {
381 let mut element = fallback();
382 replacement_id = Some(element.request_layout(window, cx));
383 layout_state.replacement = Some(element);
384 }
385 if let Some(state) = &mut state {
386 state.started_loading = None;
387 }
388 }
389 None => {
390 if let Some(state) = &mut state {
391 if let Some((started_loading, _)) = state.started_loading {
392 if started_loading.elapsed() > LOADING_DELAY
393 && let Some(loading) = self.style.loading.as_ref()
394 {
395 let mut element = loading();
396 replacement_id = Some(element.request_layout(window, cx));
397 layout_state.replacement = Some(element);
398 }
399 } else {
400 let current_view = window.current_view();
401 let task = window.spawn(cx, async move |cx| {
402 cx.background_executor().timer(LOADING_DELAY).await;
403 cx.update(move |_, cx| {
404 cx.notify(current_view);
405 })
406 .ok();
407 });
408 state.started_loading = Some((Instant::now(), task));
409 }
410 }
411 }
412 }
413
414 window.request_layout(style, replacement_id, cx)
415 },
416 );
417
418 layout_state.frame_index = frame_index;
419
420 ((layout_id, layout_state), state)
421 })
422 }
423
424 fn prepaint(
425 &mut self,
426 global_id: Option<&GlobalElementId>,
427 inspector_id: Option<&InspectorElementId>,
428 bounds: Bounds<Pixels>,
429 request_layout: &mut Self::RequestLayoutState,
430 window: &mut Window,
431 cx: &mut App,
432 ) -> Self::PrepaintState {
433 self.interactivity.prepaint(
434 global_id,
435 inspector_id,
436 bounds,
437 bounds.size,
438 window,
439 cx,
440 |_, _, hitbox, window, cx| {
441 if let Some(replacement) = &mut request_layout.replacement {
442 replacement.prepaint(window, cx);
443 }
444
445 hitbox
446 },
447 )
448 }
449
450 fn paint(
451 &mut self,
452 global_id: Option<&GlobalElementId>,
453 inspector_id: Option<&InspectorElementId>,
454 bounds: Bounds<Pixels>,
455 layout_state: &mut Self::RequestLayoutState,
456 hitbox: &mut Self::PrepaintState,
457 window: &mut Window,
458 cx: &mut App,
459 ) {
460 let source = self.source.clone();
461 self.interactivity.paint(
462 global_id,
463 inspector_id,
464 bounds,
465 hitbox.as_ref(),
466 window,
467 cx,
468 |style, window, cx| {
469 if let Some(Ok(data)) = source.use_data(
470 self.image_cache
471 .clone()
472 .or_else(|| window.image_cache_stack.last().cloned()),
473 window,
474 cx,
475 ) {
476 let new_bounds = self
477 .style
478 .object_fit
479 .get_bounds(bounds, data.size(layout_state.frame_index));
480 let corner_radii = style
481 .corner_radii
482 .to_pixels(window.rem_size())
483 .clamp_radii_for_quad_size(new_bounds.size);
484 window
485 .paint_image(
486 new_bounds,
487 corner_radii,
488 data,
489 layout_state.frame_index,
490 self.style.grayscale,
491 )
492 .log_err();
493 } else if let Some(replacement) = &mut layout_state.replacement {
494 replacement.paint(window, cx);
495 }
496 },
497 )
498 }
499}
500
501impl Styled for Img {
502 fn style(&mut self) -> &mut StyleRefinement {
503 &mut self.interactivity.base_style
504 }
505}
506
507impl InteractiveElement for Img {
508 fn interactivity(&mut self) -> &mut Interactivity {
509 &mut self.interactivity
510 }
511}
512
513impl IntoElement for Img {
514 type Element = Self;
515
516 fn into_element(self) -> Self::Element {
517 self
518 }
519}
520
521impl StatefulInteractiveElement for Img {}
522
523impl ImageSource {
524 pub(crate) fn use_data(
525 &self,
526 cache: Option<AnyImageCache>,
527 window: &mut Window,
528 cx: &mut App,
529 ) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
530 match self {
531 ImageSource::Resource(resource) => {
532 if let Some(cache) = cache {
533 cache.load(resource, window, cx)
534 } else {
535 window.use_asset::<ImgResourceLoader>(resource, cx)
536 }
537 }
538 ImageSource::Custom(loading_fn) => loading_fn(window, cx),
539 ImageSource::Render(data) => Some(Ok(data.to_owned())),
540 ImageSource::Image(data) => window.use_asset::<AssetLogger<ImageDecoder>>(data, cx),
541 }
542 }
543
544 pub(crate) fn get_data(
545 &self,
546 cache: Option<AnyImageCache>,
547 window: &mut Window,
548 cx: &mut App,
549 ) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
550 match self {
551 ImageSource::Resource(resource) => {
552 if let Some(cache) = cache {
553 cache.load(resource, window, cx)
554 } else {
555 window.get_asset::<ImgResourceLoader>(resource, cx)
556 }
557 }
558 ImageSource::Custom(loading_fn) => loading_fn(window, cx),
559 ImageSource::Render(data) => Some(Ok(data.to_owned())),
560 ImageSource::Image(data) => window.get_asset::<AssetLogger<ImageDecoder>>(data, cx),
561 }
562 }
563
564 /// Remove this image source from the asset system
565 pub fn remove_asset(&self, cx: &mut App) {
566 match self {
567 ImageSource::Resource(resource) => {
568 cx.remove_asset::<ImgResourceLoader>(resource);
569 }
570 ImageSource::Custom(_) | ImageSource::Render(_) => {}
571 ImageSource::Image(data) => cx.remove_asset::<AssetLogger<ImageDecoder>>(data),
572 }
573 }
574}
575
576#[derive(Clone)]
577enum ImageDecoder {}
578
579impl Asset for ImageDecoder {
580 type Source = Arc<Image>;
581 type Output = Result<Arc<RenderImage>, ImageCacheError>;
582
583 fn load(
584 source: Self::Source,
585 cx: &mut App,
586 ) -> impl Future<Output = Self::Output> + Send + 'static {
587 let renderer = cx.svg_renderer();
588 async move { source.to_image_data(renderer).map_err(Into::into) }
589 }
590}
591
592/// An image loader for the GPUI asset system
593#[derive(Clone)]
594pub enum ImageAssetLoader {}
595
596impl Asset for ImageAssetLoader {
597 type Source = Resource;
598 type Output = Result<Arc<RenderImage>, ImageCacheError>;
599
600 fn load(
601 source: Self::Source,
602 cx: &mut App,
603 ) -> impl Future<Output = Self::Output> + Send + 'static {
604 let client = cx.http_client();
605 // TODO: Can we make SVGs always rescale?
606 // let scale_factor = cx.scale_factor();
607 let svg_renderer = cx.svg_renderer();
608 let asset_source = cx.asset_source().clone();
609 async move {
610 let bytes = match source.clone() {
611 Resource::Path(uri) => fs::read(uri.as_ref())?,
612 Resource::Uri(uri) => {
613 use anyhow::Context as _;
614 use futures::AsyncReadExt as _;
615
616 let mut response = client
617 .get(uri.as_ref(), ().into(), true)
618 .await
619 .with_context(|| format!("loading image asset from {uri:?}"))?;
620 let mut body = Vec::new();
621 response.body_mut().read_to_end(&mut body).await?;
622 if !response.status().is_success() {
623 let mut body = String::from_utf8_lossy(&body).into_owned();
624 let first_line = body.lines().next().unwrap_or("").trim_end();
625 body.truncate(first_line.len());
626 return Err(ImageCacheError::BadStatus {
627 uri,
628 status: response.status(),
629 body,
630 });
631 }
632 body
633 }
634 Resource::Embedded(path) => {
635 let data = asset_source.load(&path).ok().flatten();
636 if let Some(data) = data {
637 data.to_vec()
638 } else {
639 return Err(ImageCacheError::Asset(
640 format!("Embedded resource not found: {}", path).into(),
641 ));
642 }
643 }
644 };
645
646 if let Ok(format) = image::guess_format(&bytes) {
647 let data = match format {
648 ImageFormat::Gif => {
649 let decoder = GifDecoder::new(Cursor::new(&bytes))?;
650 let mut frames = SmallVec::new();
651
652 for frame in decoder.into_frames() {
653 let mut frame = frame?;
654 // Convert from RGBA to BGRA.
655 for pixel in frame.buffer_mut().chunks_exact_mut(4) {
656 pixel.swap(0, 2);
657 }
658 frames.push(frame);
659 }
660
661 frames
662 }
663 ImageFormat::WebP => {
664 let mut decoder = WebPDecoder::new(Cursor::new(&bytes))?;
665
666 if decoder.has_animation() {
667 let _ = decoder.set_background_color(Rgba([0, 0, 0, 0]));
668 let mut frames = SmallVec::new();
669
670 for frame in decoder.into_frames() {
671 let mut frame = frame?;
672 // Convert from RGBA to BGRA.
673 for pixel in frame.buffer_mut().chunks_exact_mut(4) {
674 pixel.swap(0, 2);
675 }
676 frames.push(frame);
677 }
678
679 frames
680 } else {
681 let mut data = DynamicImage::from_decoder(decoder)?.into_rgba8();
682
683 // Convert from RGBA to BGRA.
684 for pixel in data.chunks_exact_mut(4) {
685 pixel.swap(0, 2);
686 }
687
688 SmallVec::from_elem(Frame::new(data), 1)
689 }
690 }
691 _ => {
692 let mut data =
693 image::load_from_memory_with_format(&bytes, format)?.into_rgba8();
694
695 // Convert from RGBA to BGRA.
696 for pixel in data.chunks_exact_mut(4) {
697 pixel.swap(0, 2);
698 }
699
700 SmallVec::from_elem(Frame::new(data), 1)
701 }
702 };
703
704 Ok(Arc::new(RenderImage::new(data)))
705 } else {
706 svg_renderer
707 .render_single_frame(&bytes, 1.0)
708 .map_err(Into::into)
709 }
710 }
711 }
712}
713
714/// An error that can occur when interacting with the image cache.
715#[derive(Debug, Error, Clone)]
716pub enum ImageCacheError {
717 /// Some other kind of error occurred
718 #[error("error: {0}")]
719 Other(#[from] Arc<anyhow::Error>),
720 /// An error that occurred while reading the image from disk.
721 #[error("IO error: {0}")]
722 Io(Arc<std::io::Error>),
723 /// An error that occurred while processing an image.
724 #[error("unexpected http status for {uri}: {status}, body: {body}")]
725 BadStatus {
726 /// The URI of the image.
727 uri: SharedUri,
728 /// The HTTP status code.
729 status: http_client::StatusCode,
730 /// The HTTP response body.
731 body: String,
732 },
733 /// An error that occurred while processing an asset.
734 #[error("asset error: {0}")]
735 Asset(SharedString),
736 /// An error that occurred while processing an image.
737 #[error("image error: {0}")]
738 Image(Arc<ImageError>),
739 /// An error that occurred while processing an SVG.
740 #[error("svg error: {0}")]
741 Usvg(Arc<usvg::Error>),
742}
743
744impl From<anyhow::Error> for ImageCacheError {
745 fn from(value: anyhow::Error) -> Self {
746 Self::Other(Arc::new(value))
747 }
748}
749
750impl From<io::Error> for ImageCacheError {
751 fn from(value: io::Error) -> Self {
752 Self::Io(Arc::new(value))
753 }
754}
755
756impl From<usvg::Error> for ImageCacheError {
757 fn from(value: usvg::Error) -> Self {
758 Self::Usvg(Arc::new(value))
759 }
760}
761
762impl From<image::ImageError> for ImageCacheError {
763 fn from(value: image::ImageError) -> Self {
764 Self::Image(Arc::new(value))
765 }
766}