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