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    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}