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