img.rs

  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                                    let current_time = Instant::now();
319                                    if let Some(last_frame_time) = state.last_frame_time {
320                                        let elapsed = current_time - last_frame_time;
321                                        let frame_duration =
322                                            Duration::from(data.delay(state.frame_index));
323
324                                        if elapsed >= frame_duration {
325                                            state.frame_index =
326                                                (state.frame_index + 1) % frame_count;
327                                            state.last_frame_time =
328                                                Some(current_time - (elapsed - frame_duration));
329                                        }
330                                    } else {
331                                        state.last_frame_time = Some(current_time);
332                                    }
333                                }
334                                state.started_loading = None;
335                            }
336
337                            let image_size = data.render_size(frame_index);
338                            style.aspect_ratio = Some(image_size.width / image_size.height);
339
340                            if let Length::Auto = style.size.width {
341                                style.size.width = match style.size.height {
342                                    Length::Definite(DefiniteLength::Absolute(abs_length)) => {
343                                        let height_px = abs_length.to_pixels(window.rem_size());
344                                        Length::Definite(
345                                            px(image_size.width.0 * height_px.0
346                                                / image_size.height.0)
347                                            .into(),
348                                        )
349                                    }
350                                    _ => Length::Definite(image_size.width.into()),
351                                };
352                            }
353
354                            if let Length::Auto = style.size.height {
355                                style.size.height = match style.size.width {
356                                    Length::Definite(DefiniteLength::Absolute(abs_length)) => {
357                                        let width_px = abs_length.to_pixels(window.rem_size());
358                                        Length::Definite(
359                                            px(image_size.height.0 * width_px.0
360                                                / image_size.width.0)
361                                            .into(),
362                                        )
363                                    }
364                                    _ => Length::Definite(image_size.height.into()),
365                                };
366                            }
367
368                            if global_id.is_some() && data.frame_count() > 1 {
369                                window.request_animation_frame();
370                            }
371                        }
372                        Some(_err) => {
373                            if let Some(fallback) = self.style.fallback.as_ref() {
374                                let mut element = fallback();
375                                replacement_id = Some(element.request_layout(window, cx));
376                                layout_state.replacement = Some(element);
377                            }
378                            if let Some(state) = &mut state {
379                                state.started_loading = None;
380                            }
381                        }
382                        None => {
383                            if let Some(state) = &mut state {
384                                if let Some((started_loading, _)) = state.started_loading {
385                                    if started_loading.elapsed() > LOADING_DELAY
386                                        && let Some(loading) = self.style.loading.as_ref()
387                                    {
388                                        let mut element = loading();
389                                        replacement_id = Some(element.request_layout(window, cx));
390                                        layout_state.replacement = Some(element);
391                                    }
392                                } else {
393                                    let current_view = window.current_view();
394                                    let task = window.spawn(cx, async move |cx| {
395                                        cx.background_executor().timer(LOADING_DELAY).await;
396                                        cx.update(move |_, cx| {
397                                            cx.notify(current_view);
398                                        })
399                                        .ok();
400                                    });
401                                    state.started_loading = Some((Instant::now(), task));
402                                }
403                            }
404                        }
405                    }
406
407                    window.request_layout(style, replacement_id, cx)
408                },
409            );
410
411            layout_state.frame_index = frame_index;
412
413            ((layout_id, layout_state), state)
414        })
415    }
416
417    fn prepaint(
418        &mut self,
419        global_id: Option<&GlobalElementId>,
420        inspector_id: Option<&InspectorElementId>,
421        bounds: Bounds<Pixels>,
422        request_layout: &mut Self::RequestLayoutState,
423        window: &mut Window,
424        cx: &mut App,
425    ) -> Self::PrepaintState {
426        self.interactivity.prepaint(
427            global_id,
428            inspector_id,
429            bounds,
430            bounds.size,
431            window,
432            cx,
433            |_, _, hitbox, window, cx| {
434                if let Some(replacement) = &mut request_layout.replacement {
435                    replacement.prepaint(window, cx);
436                }
437
438                hitbox
439            },
440        )
441    }
442
443    fn paint(
444        &mut self,
445        global_id: Option<&GlobalElementId>,
446        inspector_id: Option<&InspectorElementId>,
447        bounds: Bounds<Pixels>,
448        layout_state: &mut Self::RequestLayoutState,
449        hitbox: &mut Self::PrepaintState,
450        window: &mut Window,
451        cx: &mut App,
452    ) {
453        let source = self.source.clone();
454        self.interactivity.paint(
455            global_id,
456            inspector_id,
457            bounds,
458            hitbox.as_ref(),
459            window,
460            cx,
461            |style, window, cx| {
462                if let Some(Ok(data)) = source.use_data(
463                    self.image_cache
464                        .clone()
465                        .or_else(|| window.image_cache_stack.last().cloned()),
466                    window,
467                    cx,
468                ) {
469                    let new_bounds = self
470                        .style
471                        .object_fit
472                        .get_bounds(bounds, data.size(layout_state.frame_index));
473                    let corner_radii = style
474                        .corner_radii
475                        .to_pixels(window.rem_size())
476                        .clamp_radii_for_quad_size(new_bounds.size);
477                    window
478                        .paint_image(
479                            new_bounds,
480                            corner_radii,
481                            data,
482                            layout_state.frame_index,
483                            self.style.grayscale,
484                        )
485                        .log_err();
486                } else if let Some(replacement) = &mut layout_state.replacement {
487                    replacement.paint(window, cx);
488                }
489            },
490        )
491    }
492}
493
494impl Styled for Img {
495    fn style(&mut self) -> &mut StyleRefinement {
496        &mut self.interactivity.base_style
497    }
498}
499
500impl InteractiveElement for Img {
501    fn interactivity(&mut self) -> &mut Interactivity {
502        &mut self.interactivity
503    }
504}
505
506impl IntoElement for Img {
507    type Element = Self;
508
509    fn into_element(self) -> Self::Element {
510        self
511    }
512}
513
514impl StatefulInteractiveElement for Img {}
515
516impl ImageSource {
517    pub(crate) fn use_data(
518        &self,
519        cache: Option<AnyImageCache>,
520        window: &mut Window,
521        cx: &mut App,
522    ) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
523        match self {
524            ImageSource::Resource(resource) => {
525                if let Some(cache) = cache {
526                    cache.load(resource, window, cx)
527                } else {
528                    window.use_asset::<ImgResourceLoader>(resource, cx)
529                }
530            }
531            ImageSource::Custom(loading_fn) => loading_fn(window, cx),
532            ImageSource::Render(data) => Some(Ok(data.to_owned())),
533            ImageSource::Image(data) => window.use_asset::<AssetLogger<ImageDecoder>>(data, cx),
534        }
535    }
536
537    pub(crate) fn get_data(
538        &self,
539        cache: Option<AnyImageCache>,
540        window: &mut Window,
541        cx: &mut App,
542    ) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
543        match self {
544            ImageSource::Resource(resource) => {
545                if let Some(cache) = cache {
546                    cache.load(resource, window, cx)
547                } else {
548                    window.get_asset::<ImgResourceLoader>(resource, cx)
549                }
550            }
551            ImageSource::Custom(loading_fn) => loading_fn(window, cx),
552            ImageSource::Render(data) => Some(Ok(data.to_owned())),
553            ImageSource::Image(data) => window.get_asset::<AssetLogger<ImageDecoder>>(data, cx),
554        }
555    }
556
557    /// Remove this image source from the asset system
558    pub fn remove_asset(&self, cx: &mut App) {
559        match self {
560            ImageSource::Resource(resource) => {
561                cx.remove_asset::<ImgResourceLoader>(resource);
562            }
563            ImageSource::Custom(_) | ImageSource::Render(_) => {}
564            ImageSource::Image(data) => cx.remove_asset::<AssetLogger<ImageDecoder>>(data),
565        }
566    }
567}
568
569#[derive(Clone)]
570enum ImageDecoder {}
571
572impl Asset for ImageDecoder {
573    type Source = Arc<Image>;
574    type Output = Result<Arc<RenderImage>, ImageCacheError>;
575
576    fn load(
577        source: Self::Source,
578        cx: &mut App,
579    ) -> impl Future<Output = Self::Output> + Send + 'static {
580        let renderer = cx.svg_renderer();
581        async move { source.to_image_data(renderer).map_err(Into::into) }
582    }
583}
584
585/// An image loader for the GPUI asset system
586#[derive(Clone)]
587pub enum ImageAssetLoader {}
588
589impl Asset for ImageAssetLoader {
590    type Source = Resource;
591    type Output = Result<Arc<RenderImage>, ImageCacheError>;
592
593    fn load(
594        source: Self::Source,
595        cx: &mut App,
596    ) -> impl Future<Output = Self::Output> + Send + 'static {
597        let client = cx.http_client();
598        // TODO: Can we make SVGs always rescale?
599        // let scale_factor = cx.scale_factor();
600        let svg_renderer = cx.svg_renderer();
601        let asset_source = cx.asset_source().clone();
602        async move {
603            let bytes = match source.clone() {
604                Resource::Path(uri) => fs::read(uri.as_ref())?,
605                Resource::Uri(uri) => {
606                    use anyhow::Context as _;
607                    use futures::AsyncReadExt as _;
608
609                    let mut response = client
610                        .get(uri.as_ref(), ().into(), true)
611                        .await
612                        .with_context(|| format!("loading image asset from {uri:?}"))?;
613                    let mut body = Vec::new();
614                    response.body_mut().read_to_end(&mut body).await?;
615                    if !response.status().is_success() {
616                        let mut body = String::from_utf8_lossy(&body).into_owned();
617                        let first_line = body.lines().next().unwrap_or("").trim_end();
618                        body.truncate(first_line.len());
619                        return Err(ImageCacheError::BadStatus {
620                            uri,
621                            status: response.status(),
622                            body,
623                        });
624                    }
625                    body
626                }
627                Resource::Embedded(path) => {
628                    let data = asset_source.load(&path).ok().flatten();
629                    if let Some(data) = data {
630                        data.to_vec()
631                    } else {
632                        return Err(ImageCacheError::Asset(
633                            format!("Embedded resource not found: {}", path).into(),
634                        ));
635                    }
636                }
637            };
638
639            if let Ok(format) = image::guess_format(&bytes) {
640                let data = match format {
641                    ImageFormat::Gif => {
642                        let decoder = GifDecoder::new(Cursor::new(&bytes))?;
643                        let mut frames = SmallVec::new();
644
645                        for frame in decoder.into_frames() {
646                            let mut frame = frame?;
647                            // Convert from RGBA to BGRA.
648                            for pixel in frame.buffer_mut().chunks_exact_mut(4) {
649                                pixel.swap(0, 2);
650                            }
651                            frames.push(frame);
652                        }
653
654                        frames
655                    }
656                    ImageFormat::WebP => {
657                        let mut decoder = WebPDecoder::new(Cursor::new(&bytes))?;
658
659                        if decoder.has_animation() {
660                            let _ = decoder.set_background_color(Rgba([0, 0, 0, 0]));
661                            let mut frames = SmallVec::new();
662
663                            for frame in decoder.into_frames() {
664                                let mut frame = frame?;
665                                // Convert from RGBA to BGRA.
666                                for pixel in frame.buffer_mut().chunks_exact_mut(4) {
667                                    pixel.swap(0, 2);
668                                }
669                                frames.push(frame);
670                            }
671
672                            frames
673                        } else {
674                            let mut data = DynamicImage::from_decoder(decoder)?.into_rgba8();
675
676                            // Convert from RGBA to BGRA.
677                            for pixel in data.chunks_exact_mut(4) {
678                                pixel.swap(0, 2);
679                            }
680
681                            SmallVec::from_elem(Frame::new(data), 1)
682                        }
683                    }
684                    _ => {
685                        let mut data =
686                            image::load_from_memory_with_format(&bytes, format)?.into_rgba8();
687
688                        // Convert from RGBA to BGRA.
689                        for pixel in data.chunks_exact_mut(4) {
690                            pixel.swap(0, 2);
691                        }
692
693                        SmallVec::from_elem(Frame::new(data), 1)
694                    }
695                };
696
697                Ok(Arc::new(RenderImage::new(data)))
698            } else {
699                svg_renderer
700                    .render_single_frame(&bytes, 1.0, true)
701                    .map_err(Into::into)
702            }
703        }
704    }
705}
706
707/// An error that can occur when interacting with the image cache.
708#[derive(Debug, Error, Clone)]
709pub enum ImageCacheError {
710    /// Some other kind of error occurred
711    #[error("error: {0}")]
712    Other(#[from] Arc<anyhow::Error>),
713    /// An error that occurred while reading the image from disk.
714    #[error("IO error: {0}")]
715    Io(Arc<std::io::Error>),
716    /// An error that occurred while processing an image.
717    #[error("unexpected http status for {uri}: {status}, body: {body}")]
718    BadStatus {
719        /// The URI of the image.
720        uri: SharedUri,
721        /// The HTTP status code.
722        status: http_client::StatusCode,
723        /// The HTTP response body.
724        body: String,
725    },
726    /// An error that occurred while processing an asset.
727    #[error("asset error: {0}")]
728    Asset(SharedString),
729    /// An error that occurred while processing an image.
730    #[error("image error: {0}")]
731    Image(Arc<ImageError>),
732    /// An error that occurred while processing an SVG.
733    #[error("svg error: {0}")]
734    Usvg(Arc<usvg::Error>),
735}
736
737impl From<anyhow::Error> for ImageCacheError {
738    fn from(value: anyhow::Error) -> Self {
739        Self::Other(Arc::new(value))
740    }
741}
742
743impl From<io::Error> for ImageCacheError {
744    fn from(value: io::Error) -> Self {
745        Self::Io(Arc::new(value))
746    }
747}
748
749impl From<usvg::Error> for ImageCacheError {
750    fn from(value: usvg::Error) -> Self {
751        Self::Usvg(Arc::new(value))
752    }
753}
754
755impl From<image::ImageError> for ImageCacheError {
756    fn from(value: image::ImageError) -> Self {
757        Self::Image(Arc::new(value))
758    }
759}