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