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}