img.rs

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