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 SMOOTH_SVG_SCALE_FACTOR, SharedString, SharedUri, StyleRefinement, Styled, SvgSize, Task,
6 Window, px, swap_rgba_pa_to_bgra,
7};
8use anyhow::{Context as _, Result};
9
10use futures::{AsyncReadExt, Future};
11use image::{
12 AnimationDecoder, DynamicImage, Frame, ImageBuffer, ImageError, ImageFormat, Rgba,
13 codecs::{gif::GifDecoder, webp::WebPDecoder},
14};
15use smallvec::SmallVec;
16use std::{
17 fs,
18 io::{self, Cursor},
19 ops::{Deref, DerefMut},
20 path::{Path, PathBuf},
21 str::FromStr,
22 sync::Arc,
23 time::{Duration, Instant},
24};
25use thiserror::Error;
26use util::ResultExt;
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 http_client::Uri::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 the object fit for the image.
164 fn with_fallback(mut self, fallback: impl Fn() -> AnyElement + 'static) -> Self {
165 self.image_style().fallback = Some(Box::new(fallback));
166 self
167 }
168
169 /// Set the object fit for the image.
170 fn with_loading(mut self, loading: impl Fn() -> AnyElement + 'static) -> Self {
171 self.image_style().loading = Some(Box::new(loading));
172 self
173 }
174}
175
176impl StyledImage for Img {
177 fn image_style(&mut self) -> &mut ImageStyle {
178 &mut self.style
179 }
180}
181
182impl StyledImage for Stateful<Img> {
183 fn image_style(&mut self) -> &mut ImageStyle {
184 &mut self.element.style
185 }
186}
187
188/// An image element.
189pub struct Img {
190 interactivity: Interactivity,
191 source: ImageSource,
192 style: ImageStyle,
193 image_cache: Option<AnyImageCache>,
194}
195
196/// Create a new image element.
197#[track_caller]
198pub fn img(source: impl Into<ImageSource>) -> Img {
199 Img {
200 interactivity: Interactivity::new(),
201 source: source.into(),
202 style: ImageStyle::default(),
203 image_cache: None,
204 }
205}
206
207impl Img {
208 /// A list of all format extensions currently supported by this img element
209 pub const fn extensions() -> &'static [&'static str] {
210 // This is the list in [image::ImageFormat::from_extension] + `svg`
211 &[
212 "avif", "jpg", "jpeg", "png", "gif", "webp", "tif", "tiff", "tga", "dds", "bmp", "ico",
213 "hdr", "exr", "pbm", "pam", "ppm", "pgm", "ff", "farbfeld", "qoi", "svg",
214 ]
215 }
216
217 /// Sets the image cache for the current node.
218 ///
219 /// If the `image_cache` is not explicitly provided, the function will determine the image cache by:
220 ///
221 /// 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.
222 /// 2. If no ancestor node contains an `ImageCacheElement`, the global image cache will be used as a fallback.
223 ///
224 /// This mechanism provides a flexible way to manage image caching, allowing precise control when needed,
225 /// while ensuring a default behavior when no cache is explicitly specified.
226 #[inline]
227 pub fn image_cache<I: ImageCache>(self, image_cache: &Entity<I>) -> Self {
228 Self {
229 image_cache: Some(image_cache.clone().into()),
230 ..self
231 }
232 }
233}
234
235impl Deref for Stateful<Img> {
236 type Target = Img;
237
238 fn deref(&self) -> &Self::Target {
239 &self.element
240 }
241}
242
243impl DerefMut for Stateful<Img> {
244 fn deref_mut(&mut self) -> &mut Self::Target {
245 &mut self.element
246 }
247}
248
249/// The image state between frames
250struct ImgState {
251 frame_index: usize,
252 last_frame_time: Option<Instant>,
253 started_loading: Option<(Instant, Task<()>)>,
254}
255
256/// The image layout state between frames
257pub struct ImgLayoutState {
258 frame_index: usize,
259 replacement: Option<AnyElement>,
260}
261
262impl Element for Img {
263 type RequestLayoutState = ImgLayoutState;
264 type PrepaintState = Option<Hitbox>;
265
266 fn id(&self) -> Option<ElementId> {
267 self.interactivity.element_id.clone()
268 }
269
270 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
271 self.interactivity.source_location()
272 }
273
274 fn request_layout(
275 &mut self,
276 global_id: Option<&GlobalElementId>,
277 inspector_id: Option<&InspectorElementId>,
278 window: &mut Window,
279 cx: &mut App,
280 ) -> (LayoutId, Self::RequestLayoutState) {
281 let mut layout_state = ImgLayoutState {
282 frame_index: 0,
283 replacement: None,
284 };
285
286 window.with_optional_element_state(global_id, |state, window| {
287 let mut state = state.map(|state| {
288 state.unwrap_or(ImgState {
289 frame_index: 0,
290 last_frame_time: None,
291 started_loading: None,
292 })
293 });
294
295 let frame_index = state.as_ref().map(|state| state.frame_index).unwrap_or(0);
296
297 let layout_id = self.interactivity.request_layout(
298 global_id,
299 inspector_id,
300 window,
301 cx,
302 |mut style, window, cx| {
303 let mut replacement_id = None;
304
305 match self.source.use_data(
306 self.image_cache
307 .clone()
308 .or_else(|| window.image_cache_stack.last().cloned()),
309 window,
310 cx,
311 ) {
312 Some(Ok(data)) => {
313 if let Some(state) = &mut state {
314 let frame_count = data.frame_count();
315 if frame_count > 1 {
316 let current_time = Instant::now();
317 if let Some(last_frame_time) = state.last_frame_time {
318 let elapsed = current_time - last_frame_time;
319 let frame_duration =
320 Duration::from(data.delay(state.frame_index));
321
322 if elapsed >= frame_duration {
323 state.frame_index =
324 (state.frame_index + 1) % frame_count;
325 state.last_frame_time =
326 Some(current_time - (elapsed - frame_duration));
327 }
328 } else {
329 state.last_frame_time = Some(current_time);
330 }
331 }
332 state.started_loading = None;
333 }
334
335 let image_size = data.render_size(frame_index);
336 style.aspect_ratio = Some(image_size.width / image_size.height);
337
338 if let Length::Auto = style.size.width {
339 style.size.width = match style.size.height {
340 Length::Definite(DefiniteLength::Absolute(abs_length)) => {
341 let height_px = abs_length.to_pixels(window.rem_size());
342 Length::Definite(
343 px(image_size.width.0 * height_px.0
344 / image_size.height.0)
345 .into(),
346 )
347 }
348 _ => Length::Definite(image_size.width.into()),
349 };
350 }
351
352 if let Length::Auto = style.size.height {
353 style.size.height = match style.size.width {
354 Length::Definite(DefiniteLength::Absolute(abs_length)) => {
355 let width_px = abs_length.to_pixels(window.rem_size());
356 Length::Definite(
357 px(image_size.height.0 * width_px.0
358 / image_size.width.0)
359 .into(),
360 )
361 }
362 _ => Length::Definite(image_size.height.into()),
363 };
364 }
365
366 if global_id.is_some() && data.frame_count() > 1 {
367 window.request_animation_frame();
368 }
369 }
370 Some(_err) => {
371 if let Some(fallback) = self.style.fallback.as_ref() {
372 let mut element = fallback();
373 replacement_id = Some(element.request_layout(window, cx));
374 layout_state.replacement = Some(element);
375 }
376 if let Some(state) = &mut state {
377 state.started_loading = None;
378 }
379 }
380 None => {
381 if let Some(state) = &mut state {
382 if let Some((started_loading, _)) = state.started_loading {
383 if started_loading.elapsed() > LOADING_DELAY
384 && let Some(loading) = self.style.loading.as_ref()
385 {
386 let mut element = loading();
387 replacement_id = Some(element.request_layout(window, cx));
388 layout_state.replacement = Some(element);
389 }
390 } else {
391 let current_view = window.current_view();
392 let task = window.spawn(cx, async move |cx| {
393 cx.background_executor().timer(LOADING_DELAY).await;
394 cx.update(move |_, cx| {
395 cx.notify(current_view);
396 })
397 .ok();
398 });
399 state.started_loading = Some((Instant::now(), task));
400 }
401 }
402 }
403 }
404
405 window.request_layout(style, replacement_id, cx)
406 },
407 );
408
409 layout_state.frame_index = frame_index;
410
411 ((layout_id, layout_state), state)
412 })
413 }
414
415 fn prepaint(
416 &mut self,
417 global_id: Option<&GlobalElementId>,
418 inspector_id: Option<&InspectorElementId>,
419 bounds: Bounds<Pixels>,
420 request_layout: &mut Self::RequestLayoutState,
421 window: &mut Window,
422 cx: &mut App,
423 ) -> Self::PrepaintState {
424 self.interactivity.prepaint(
425 global_id,
426 inspector_id,
427 bounds,
428 bounds.size,
429 window,
430 cx,
431 |_, _, hitbox, window, cx| {
432 if let Some(replacement) = &mut request_layout.replacement {
433 replacement.prepaint(window, cx);
434 }
435
436 hitbox
437 },
438 )
439 }
440
441 fn paint(
442 &mut self,
443 global_id: Option<&GlobalElementId>,
444 inspector_id: Option<&InspectorElementId>,
445 bounds: Bounds<Pixels>,
446 layout_state: &mut Self::RequestLayoutState,
447 hitbox: &mut Self::PrepaintState,
448 window: &mut Window,
449 cx: &mut App,
450 ) {
451 let source = self.source.clone();
452 self.interactivity.paint(
453 global_id,
454 inspector_id,
455 bounds,
456 hitbox.as_ref(),
457 window,
458 cx,
459 |style, window, cx| {
460 if let Some(Ok(data)) = source.use_data(
461 self.image_cache
462 .clone()
463 .or_else(|| window.image_cache_stack.last().cloned()),
464 window,
465 cx,
466 ) {
467 let new_bounds = self
468 .style
469 .object_fit
470 .get_bounds(bounds, data.size(layout_state.frame_index));
471 let corner_radii = style
472 .corner_radii
473 .to_pixels(window.rem_size())
474 .clamp_radii_for_quad_size(new_bounds.size);
475 window
476 .paint_image(
477 new_bounds,
478 corner_radii,
479 data,
480 layout_state.frame_index,
481 self.style.grayscale,
482 )
483 .log_err();
484 } else if let Some(replacement) = &mut layout_state.replacement {
485 replacement.paint(window, cx);
486 }
487 },
488 )
489 }
490}
491
492impl Styled for Img {
493 fn style(&mut self) -> &mut StyleRefinement {
494 &mut self.interactivity.base_style
495 }
496}
497
498impl InteractiveElement for Img {
499 fn interactivity(&mut self) -> &mut Interactivity {
500 &mut self.interactivity
501 }
502}
503
504impl IntoElement for Img {
505 type Element = Self;
506
507 fn into_element(self) -> Self::Element {
508 self
509 }
510}
511
512impl StatefulInteractiveElement for Img {}
513
514impl ImageSource {
515 pub(crate) fn use_data(
516 &self,
517 cache: Option<AnyImageCache>,
518 window: &mut Window,
519 cx: &mut App,
520 ) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
521 match self {
522 ImageSource::Resource(resource) => {
523 if let Some(cache) = cache {
524 cache.load(resource, window, cx)
525 } else {
526 window.use_asset::<ImgResourceLoader>(resource, cx)
527 }
528 }
529 ImageSource::Custom(loading_fn) => loading_fn(window, cx),
530 ImageSource::Render(data) => Some(Ok(data.to_owned())),
531 ImageSource::Image(data) => window.use_asset::<AssetLogger<ImageDecoder>>(data, cx),
532 }
533 }
534
535 pub(crate) fn get_data(
536 &self,
537 cache: Option<AnyImageCache>,
538 window: &mut Window,
539 cx: &mut App,
540 ) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
541 match self {
542 ImageSource::Resource(resource) => {
543 if let Some(cache) = cache {
544 cache.load(resource, window, cx)
545 } else {
546 window.get_asset::<ImgResourceLoader>(resource, cx)
547 }
548 }
549 ImageSource::Custom(loading_fn) => loading_fn(window, cx),
550 ImageSource::Render(data) => Some(Ok(data.to_owned())),
551 ImageSource::Image(data) => window.get_asset::<AssetLogger<ImageDecoder>>(data, cx),
552 }
553 }
554
555 /// Remove this image source from the asset system
556 pub fn remove_asset(&self, cx: &mut App) {
557 match self {
558 ImageSource::Resource(resource) => {
559 cx.remove_asset::<ImgResourceLoader>(resource);
560 }
561 ImageSource::Custom(_) | ImageSource::Render(_) => {}
562 ImageSource::Image(data) => cx.remove_asset::<AssetLogger<ImageDecoder>>(data),
563 }
564 }
565}
566
567#[derive(Clone)]
568enum ImageDecoder {}
569
570impl Asset for ImageDecoder {
571 type Source = Arc<Image>;
572 type Output = Result<Arc<RenderImage>, ImageCacheError>;
573
574 fn load(
575 source: Self::Source,
576 cx: &mut App,
577 ) -> impl Future<Output = Self::Output> + Send + 'static {
578 let renderer = cx.svg_renderer();
579 async move { source.to_image_data(renderer).map_err(Into::into) }
580 }
581}
582
583/// An image loader for the GPUI asset system
584#[derive(Clone)]
585pub enum ImageAssetLoader {}
586
587impl Asset for ImageAssetLoader {
588 type Source = Resource;
589 type Output = Result<Arc<RenderImage>, ImageCacheError>;
590
591 fn load(
592 source: Self::Source,
593 cx: &mut App,
594 ) -> impl Future<Output = Self::Output> + Send + 'static {
595 let client = cx.http_client();
596 // TODO: Can we make SVGs always rescale?
597 // let scale_factor = cx.scale_factor();
598 let svg_renderer = cx.svg_renderer();
599 let asset_source = cx.asset_source().clone();
600 async move {
601 let bytes = match source.clone() {
602 Resource::Path(uri) => fs::read(uri.as_ref())?,
603 Resource::Uri(uri) => {
604 let mut response = client
605 .get(uri.as_ref(), ().into(), true)
606 .await
607 .with_context(|| format!("loading image asset from {uri:?}"))?;
608 let mut body = Vec::new();
609 response.body_mut().read_to_end(&mut body).await?;
610 if !response.status().is_success() {
611 let mut body = String::from_utf8_lossy(&body).into_owned();
612 let first_line = body.lines().next().unwrap_or("").trim_end();
613 body.truncate(first_line.len());
614 return Err(ImageCacheError::BadStatus {
615 uri,
616 status: response.status(),
617 body,
618 });
619 }
620 body
621 }
622 Resource::Embedded(path) => {
623 let data = asset_source.load(&path).ok().flatten();
624 if let Some(data) = data {
625 data.to_vec()
626 } else {
627 return Err(ImageCacheError::Asset(
628 format!("Embedded resource not found: {}", path).into(),
629 ));
630 }
631 }
632 };
633
634 let data = if let Ok(format) = image::guess_format(&bytes) {
635 let data = match format {
636 ImageFormat::Gif => {
637 let decoder = GifDecoder::new(Cursor::new(&bytes))?;
638 let mut frames = SmallVec::new();
639
640 for frame in decoder.into_frames() {
641 let mut frame = frame?;
642 // Convert from RGBA to BGRA.
643 for pixel in frame.buffer_mut().chunks_exact_mut(4) {
644 pixel.swap(0, 2);
645 }
646 frames.push(frame);
647 }
648
649 frames
650 }
651 ImageFormat::WebP => {
652 let mut decoder = WebPDecoder::new(Cursor::new(&bytes))?;
653
654 if decoder.has_animation() {
655 let _ = decoder.set_background_color(Rgba([0, 0, 0, 0]));
656 let mut frames = SmallVec::new();
657
658 for frame in decoder.into_frames() {
659 let mut frame = frame?;
660 // Convert from RGBA to BGRA.
661 for pixel in frame.buffer_mut().chunks_exact_mut(4) {
662 pixel.swap(0, 2);
663 }
664 frames.push(frame);
665 }
666
667 frames
668 } else {
669 let mut data = DynamicImage::from_decoder(decoder)?.into_rgba8();
670
671 // Convert from RGBA to BGRA.
672 for pixel in data.chunks_exact_mut(4) {
673 pixel.swap(0, 2);
674 }
675
676 SmallVec::from_elem(Frame::new(data), 1)
677 }
678 }
679 _ => {
680 let mut data =
681 image::load_from_memory_with_format(&bytes, format)?.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 RenderImage::new(data)
693 } else {
694 let pixmap =
695 // TODO: Can we make svgs always rescale?
696 svg_renderer.render_pixmap(&bytes, SvgSize::ScaleFactor(SMOOTH_SVG_SCALE_FACTOR))?;
697
698 let mut buffer =
699 ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take()).unwrap();
700
701 for pixel in buffer.chunks_exact_mut(4) {
702 swap_rgba_pa_to_bgra(pixel);
703 }
704
705 let mut image = RenderImage::new(SmallVec::from_elem(Frame::new(buffer), 1));
706 image.scale_factor = SMOOTH_SVG_SCALE_FACTOR;
707 image
708 };
709
710 Ok(Arc::new(data))
711 }
712 }
713}
714
715/// An error that can occur when interacting with the image cache.
716#[derive(Debug, Error, Clone)]
717pub enum ImageCacheError {
718 /// Some other kind of error occurred
719 #[error("error: {0}")]
720 Other(#[from] Arc<anyhow::Error>),
721 /// An error that occurred while reading the image from disk.
722 #[error("IO error: {0}")]
723 Io(Arc<std::io::Error>),
724 /// An error that occurred while processing an image.
725 #[error("unexpected http status for {uri}: {status}, body: {body}")]
726 BadStatus {
727 /// The URI of the image.
728 uri: SharedUri,
729 /// The HTTP status code.
730 status: http_client::StatusCode,
731 /// The HTTP response body.
732 body: String,
733 },
734 /// An error that occurred while processing an asset.
735 #[error("asset error: {0}")]
736 Asset(SharedString),
737 /// An error that occurred while processing an image.
738 #[error("image error: {0}")]
739 Image(Arc<ImageError>),
740 /// An error that occurred while processing an SVG.
741 #[error("svg error: {0}")]
742 Usvg(Arc<usvg::Error>),
743}
744
745impl From<anyhow::Error> for ImageCacheError {
746 fn from(value: anyhow::Error) -> Self {
747 Self::Other(Arc::new(value))
748 }
749}
750
751impl From<io::Error> for ImageCacheError {
752 fn from(value: io::Error) -> Self {
753 Self::Io(Arc::new(value))
754 }
755}
756
757impl From<usvg::Error> for ImageCacheError {
758 fn from(value: usvg::Error) -> Self {
759 Self::Usvg(Arc::new(value))
760 }
761}
762
763impl From<image::ImageError> for ImageCacheError {
764 fn from(value: image::ImageError) -> Self {
765 Self::Image(Arc::new(value))
766 }
767}