img.rs

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