gpui: Add SVG rendering to `img` element and generic asset cache (#9931)

Matthias Grandl and Mikayla created

This is a follow up to #9436 . It has a cleaner API and generalized the
image_cache to be a generic asset cache, that all GPUI elements can make
use off. The changes have been discussed with @mikayla-maki on Discord.

---------

Co-authored-by: Mikayla <mikayla@zed.dev>

Change summary

crates/collections/src/collections.rs   |   1 
crates/gpui/Cargo.toml                  |   3 
crates/gpui/src/app.rs                  |  24 ++
crates/gpui/src/asset_cache.rs          |  87 +++++++++++
crates/gpui/src/assets.rs               |   5 
crates/gpui/src/elements/img.rs         | 208 ++++++++++++++++++--------
crates/gpui/src/gpui.rs                 |   4 
crates/gpui/src/image_cache.rs          | 134 -----------------
crates/gpui/src/svg_renderer.rs         |  44 ++++-
crates/gpui/src/window/element_cx.rs    |  96 +++++++++++-
crates/image_viewer/src/image_viewer.rs |  17 +-
11 files changed, 389 insertions(+), 234 deletions(-)

Detailed changes

crates/collections/src/collections.rs 🔗

@@ -10,5 +10,6 @@ pub type HashMap<K, V> = std::collections::HashMap<K, V>;
 #[cfg(not(feature = "test-support"))]
 pub type HashSet<T> = std::collections::HashSet<T>;
 
+pub use rustc_hash::FxHasher;
 pub use rustc_hash::{FxHashMap, FxHashSet};
 pub use std::collections::*;

crates/gpui/Cargo.toml 🔗

@@ -72,6 +72,9 @@ util.workspace = true
 uuid = { version = "1.1.2", features = ["v4", "v5"] }
 waker-fn = "1.1.0"
 
+[profile.dev.package]
+resvg = { opt-level = 3 }
+
 [dev-dependencies]
 backtrace = "0.3"
 collections = { workspace = true, features = ["test-support"] }

crates/gpui/src/app.rs 🔗

@@ -28,8 +28,8 @@ use util::{
 };
 
 use crate::{
-    current_platform, image_cache::ImageCache, init_app_menus, Action, ActionRegistry, Any,
-    AnyView, AnyWindowHandle, AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context,
+    current_platform, init_app_menus, Action, ActionRegistry, Any, AnyView, AnyWindowHandle,
+    AppMetadata, AssetCache, AssetSource, BackgroundExecutor, ClipboardItem, Context,
     DispatchPhase, DisplayId, Entity, EventEmitter, ForegroundExecutor, Global, KeyBinding, Keymap,
     Keystroke, LayoutId, Menu, PathPromptOptions, Pixels, Platform, PlatformDisplay, Point,
     PromptBuilder, PromptHandle, PromptLevel, Render, RenderablePromptHandle, SharedString,
@@ -217,9 +217,11 @@ pub struct AppContext {
     pub(crate) active_drag: Option<AnyDrag>,
     pub(crate) background_executor: BackgroundExecutor,
     pub(crate) foreground_executor: ForegroundExecutor,
-    pub(crate) svg_renderer: SvgRenderer,
+    pub(crate) loading_assets: FxHashMap<(TypeId, u64), Box<dyn Any>>,
+    pub(crate) asset_cache: AssetCache,
     asset_source: Arc<dyn AssetSource>,
-    pub(crate) image_cache: ImageCache,
+    pub(crate) svg_renderer: SvgRenderer,
+    http_client: Arc<dyn HttpClient>,
     pub(crate) globals_by_type: FxHashMap<TypeId, Box<dyn Any>>,
     pub(crate) entities: EntityMap,
     pub(crate) new_view_observers: SubscriberSet<TypeId, NewViewListener>,
@@ -279,8 +281,10 @@ impl AppContext {
                 background_executor: executor,
                 foreground_executor,
                 svg_renderer: SvgRenderer::new(asset_source.clone()),
+                asset_cache: AssetCache::new(),
+                loading_assets: Default::default(),
                 asset_source,
-                image_cache: ImageCache::new(http_client),
+                http_client,
                 globals_by_type: FxHashMap::default(),
                 entities,
                 new_view_observers: SubscriberSet::new(),
@@ -635,6 +639,16 @@ impl AppContext {
         self.platform.local_timezone()
     }
 
+    /// Returns the http client assigned to GPUI
+    pub fn http_client(&self) -> Arc<dyn HttpClient> {
+        self.http_client.clone()
+    }
+
+    /// Returns the SVG renderer GPUI uses
+    pub(crate) fn svg_renderer(&self) -> SvgRenderer {
+        self.svg_renderer.clone()
+    }
+
     pub(crate) fn push_effect(&mut self, effect: Effect) {
         match &effect {
             Effect::Notify { emitter } => {

crates/gpui/src/asset_cache.rs 🔗

@@ -0,0 +1,87 @@
+use crate::{SharedUri, WindowContext};
+use collections::FxHashMap;
+use futures::Future;
+use parking_lot::Mutex;
+use std::any::TypeId;
+use std::hash::{Hash, Hasher};
+use std::sync::Arc;
+use std::{any::Any, path::PathBuf};
+
+#[derive(Debug, PartialEq, Eq, Hash, Clone)]
+pub(crate) enum UriOrPath {
+    Uri(SharedUri),
+    Path(Arc<PathBuf>),
+}
+
+impl From<SharedUri> for UriOrPath {
+    fn from(value: SharedUri) -> Self {
+        Self::Uri(value)
+    }
+}
+
+impl From<Arc<PathBuf>> for UriOrPath {
+    fn from(value: Arc<PathBuf>) -> Self {
+        Self::Path(value)
+    }
+}
+
+/// A trait for asynchronous asset loading.
+pub trait Asset {
+    /// The source of the asset.
+    type Source: Clone + Hash + Send;
+
+    /// The loaded asset
+    type Output: Clone + Send;
+
+    /// Load the asset asynchronously
+    fn load(
+        source: Self::Source,
+        cx: &mut WindowContext,
+    ) -> impl Future<Output = Self::Output> + Send + 'static;
+}
+
+/// Use a quick, non-cryptographically secure hash function to get an identifier from data
+pub fn hash<T: Hash>(data: &T) -> u64 {
+    let mut hasher = collections::FxHasher::default();
+    data.hash(&mut hasher);
+    hasher.finish()
+}
+
+/// A cache for assets.
+#[derive(Clone)]
+pub struct AssetCache {
+    assets: Arc<Mutex<FxHashMap<(TypeId, u64), Box<dyn Any + Send>>>>,
+}
+
+impl AssetCache {
+    pub(crate) fn new() -> Self {
+        Self {
+            assets: Default::default(),
+        }
+    }
+
+    /// Get the asset from the cache, if it exists.
+    pub fn get<A: Asset + 'static>(&self, source: &A::Source) -> Option<A::Output> {
+        self.assets
+            .lock()
+            .get(&(TypeId::of::<A>(), hash(&source)))
+            .and_then(|task| task.downcast_ref::<A::Output>())
+            .cloned()
+    }
+
+    /// Insert the asset into the cache.
+    pub fn insert<A: Asset + 'static>(&mut self, source: A::Source, output: A::Output) {
+        self.assets
+            .lock()
+            .insert((TypeId::of::<A>(), hash(&source)), Box::new(output));
+    }
+
+    /// Remove an entry from the asset cache
+    pub fn remove<A: Asset + 'static>(&mut self, source: &A::Source) -> Option<A::Output> {
+        self.assets
+            .lock()
+            .remove(&(TypeId::of::<A>(), hash(&source)))
+            .and_then(|any| any.downcast::<A::Output>().ok())
+            .map(|boxed| *boxed)
+    }
+}

crates/gpui/src/assets.rs 🔗

@@ -34,6 +34,11 @@ impl AssetSource for () {
 #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
 pub struct ImageId(usize);
 
+#[derive(PartialEq, Eq, Hash, Clone)]
+pub(crate) struct RenderImageParams {
+    pub(crate) image_id: ImageId,
+}
+
 /// A cached and processed image.
 pub struct ImageData {
     /// The ID associated with this image

crates/gpui/src/elements/img.rs 🔗

@@ -1,15 +1,19 @@
+use std::fs;
 use std::path::PathBuf;
 use std::sync::Arc;
 
 use crate::{
-    point, px, size, AbsoluteLength, Bounds, DefiniteLength, DevicePixels, Element, ElementContext,
-    Hitbox, ImageData, InteractiveElement, Interactivity, IntoElement, LayoutId, Length, Pixels,
-    SharedUri, Size, StyleRefinement, Styled, UriOrPath,
+    point, px, size, AbsoluteLength, Asset, Bounds, DefiniteLength, DevicePixels, Element,
+    ElementContext, Hitbox, ImageData, InteractiveElement, Interactivity, IntoElement, LayoutId,
+    Length, Pixels, SharedUri, Size, StyleRefinement, Styled, SvgSize, UriOrPath, WindowContext,
 };
-use futures::FutureExt;
+use futures::{AsyncReadExt, Future};
+use image::{ImageBuffer, ImageError};
 #[cfg(target_os = "macos")]
 use media::core_video::CVImageBuffer;
-use util::ResultExt;
+
+use thiserror::Error;
+use util::{http, ResultExt};
 
 /// A source of image content.
 #[derive(Clone, Debug)]
@@ -69,44 +73,6 @@ impl From<CVImageBuffer> for ImageSource {
     }
 }
 
-impl ImageSource {
-    fn data(&self, cx: &mut ElementContext) -> Option<Arc<ImageData>> {
-        match self {
-            ImageSource::Uri(_) | ImageSource::File(_) => {
-                let uri_or_path: UriOrPath = match self {
-                    ImageSource::Uri(uri) => uri.clone().into(),
-                    ImageSource::File(path) => path.clone().into(),
-                    _ => unreachable!(),
-                };
-
-                let image_future = cx.image_cache.get(uri_or_path.clone(), cx);
-                if let Some(data) = image_future
-                    .clone()
-                    .now_or_never()
-                    .and_then(|result| result.ok())
-                {
-                    return Some(data);
-                } else {
-                    cx.spawn(|mut cx| async move {
-                        if image_future.await.ok().is_some() {
-                            cx.on_next_frame(|cx| cx.refresh());
-                        }
-                    })
-                    .detach();
-
-                    return None;
-                }
-            }
-
-            ImageSource::Data(data) => {
-                return Some(data.clone());
-            }
-            #[cfg(target_os = "macos")]
-            ImageSource::Surface(_) => None,
-        }
-    }
-}
-
 /// An image element.
 pub struct Img {
     interactivity: Interactivity,
@@ -201,6 +167,15 @@ impl ObjectFit {
 }
 
 impl Img {
+    /// A list of all format extensions currently supported by this img element
+    pub fn extensions() -> &'static [&'static str] {
+        // This is the list in [image::ImageFormat::from_extension] + `svg`
+        &[
+            "avif", "jpg", "jpeg", "png", "gif", "webp", "tif", "tiff", "tga", "dds", "bmp", "ico",
+            "hdr", "exr", "pbm", "pam", "ppm", "pgm", "ff", "farbfeld", "qoi", "svg",
+        ]
+    }
+
     /// Set the image to be displayed in grayscale.
     pub fn grayscale(mut self, grayscale: bool) -> Self {
         self.grayscale = grayscale;
@@ -235,6 +210,7 @@ impl Element for Img {
                     _ => {}
                 }
             }
+
             cx.request_layout(&style, [])
         });
         (layout_id, ())
@@ -262,28 +238,20 @@ impl Element for Img {
             .paint(bounds, hitbox.as_ref(), cx, |style, cx| {
                 let corner_radii = style.corner_radii.to_pixels(bounds.size, cx.rem_size());
 
-                match source.data(cx) {
-                    Some(data) => {
-                        let bounds = self.object_fit.get_bounds(bounds, data.size());
-                        cx.paint_image(bounds, corner_radii, data, self.grayscale)
-                            .log_err();
-                    }
-                    #[cfg(not(target_os = "macos"))]
-                    None => {
-                        // No renderable image loaded yet. Do nothing.
-                    }
+                if let Some(data) = source.data(cx) {
+                    cx.paint_image(bounds, corner_radii, data.clone(), self.grayscale)
+                        .log_err();
+                }
+
+                match source {
                     #[cfg(target_os = "macos")]
-                    None => match source {
-                        ImageSource::Surface(surface) => {
-                            let size = size(surface.width().into(), surface.height().into());
-                            let new_bounds = self.object_fit.get_bounds(bounds, size);
-                            // TODO: Add support for corner_radii and grayscale.
-                            cx.paint_surface(new_bounds, surface);
-                        }
-                        _ => {
-                            // No renderable image loaded yet. Do nothing.
-                        }
-                    },
+                    ImageSource::Surface(surface) => {
+                        let size = size(surface.width().into(), surface.height().into());
+                        let new_bounds = self.object_fit.get_bounds(bounds, size);
+                        // TODO: Add support for corner_radii and grayscale.
+                        cx.paint_surface(new_bounds, surface);
+                    }
+                    _ => {}
                 }
             })
     }
@@ -308,3 +276,115 @@ impl InteractiveElement for Img {
         &mut self.interactivity
     }
 }
+
+impl ImageSource {
+    fn data(&self, cx: &mut ElementContext) -> Option<Arc<ImageData>> {
+        match self {
+            ImageSource::Uri(_) | ImageSource::File(_) => {
+                let uri_or_path: UriOrPath = match self {
+                    ImageSource::Uri(uri) => uri.clone().into(),
+                    ImageSource::File(path) => path.clone().into(),
+                    _ => unreachable!(),
+                };
+
+                cx.use_cached_asset::<Image>(&uri_or_path)?.log_err()
+            }
+
+            ImageSource::Data(data) => Some(data.to_owned()),
+            #[cfg(target_os = "macos")]
+            ImageSource::Surface(_) => None,
+        }
+    }
+}
+
+#[derive(Clone)]
+enum Image {}
+
+impl Asset for Image {
+    type Source = UriOrPath;
+    type Output = Result<Arc<ImageData>, ImageCacheError>;
+
+    fn load(
+        source: Self::Source,
+        cx: &mut WindowContext,
+    ) -> impl Future<Output = Self::Output> + Send + 'static {
+        let client = cx.http_client();
+        let scale_factor = cx.scale_factor();
+        let svg_renderer = cx.svg_renderer();
+        async move {
+            let bytes = match source.clone() {
+                UriOrPath::Path(uri) => fs::read(uri.as_ref())?,
+                UriOrPath::Uri(uri) => {
+                    let mut response = client.get(uri.as_ref(), ().into(), true).await?;
+                    let mut body = Vec::new();
+                    response.body_mut().read_to_end(&mut body).await?;
+                    if !response.status().is_success() {
+                        return Err(ImageCacheError::BadStatus {
+                            status: response.status(),
+                            body: String::from_utf8_lossy(&body).into_owned(),
+                        });
+                    }
+                    body
+                }
+            };
+
+            let data = if let Ok(format) = image::guess_format(&bytes) {
+                let data = image::load_from_memory_with_format(&bytes, format)?.into_rgba8();
+                ImageData::new(data)
+            } else {
+                let pixmap =
+                    svg_renderer.render_pixmap(&bytes, SvgSize::ScaleFactor(scale_factor))?;
+
+                let buffer =
+                    ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take()).unwrap();
+
+                ImageData::new(buffer)
+            };
+
+            Ok(Arc::new(data))
+        }
+    }
+}
+
+/// An error that can occur when interacting with the image cache.
+#[derive(Debug, Error, Clone)]
+pub enum ImageCacheError {
+    /// An error that occurred while fetching an image from a remote source.
+    #[error("http error: {0}")]
+    Client(#[from] http::Error),
+    /// An error that occurred while reading the image from disk.
+    #[error("IO error: {0}")]
+    Io(Arc<std::io::Error>),
+    /// An error that occurred while processing an image.
+    #[error("unexpected http status: {status}, body: {body}")]
+    BadStatus {
+        /// The HTTP status code.
+        status: http::StatusCode,
+        /// The HTTP response body.
+        body: String,
+    },
+    /// An error that occurred while processing an image.
+    #[error("image error: {0}")]
+    Image(Arc<ImageError>),
+    /// An error that occurred while processing an SVG.
+    #[error("svg error: {0}")]
+    Usvg(Arc<resvg::usvg::Error>),
+}
+
+impl From<std::io::Error> for ImageCacheError {
+    fn from(error: std::io::Error) -> Self {
+        Self::Io(Arc::new(error))
+    }
+}
+
+impl From<ImageError> for ImageCacheError {
+    fn from(error: ImageError) -> Self {
+        Self::Image(Arc::new(error))
+    }
+}
+
+impl From<resvg::usvg::Error> for ImageCacheError {
+    fn from(error: resvg::usvg::Error) -> Self {
+        Self::Usvg(Arc::new(error))
+    }
+}

crates/gpui/src/gpui.rs 🔗

@@ -69,6 +69,7 @@ mod action;
 mod app;
 
 mod arena;
+mod asset_cache;
 mod assets;
 mod bounds_tree;
 mod color;
@@ -76,7 +77,6 @@ mod element;
 mod elements;
 mod executor;
 mod geometry;
-mod image_cache;
 mod input;
 mod interactive;
 mod key_dispatch;
@@ -117,6 +117,7 @@ pub use action::*;
 pub use anyhow::Result;
 pub use app::*;
 pub(crate) use arena::*;
+pub use asset_cache::*;
 pub use assets::*;
 pub use color::*;
 pub use ctor::ctor;
@@ -125,7 +126,6 @@ pub use elements::*;
 pub use executor::*;
 pub use geometry::*;
 pub use gpui_macros::{register_action, test, IntoElement, Render};
-pub use image_cache::*;
 pub use input::*;
 pub use interactive::*;
 use key_dispatch::*;

crates/gpui/src/image_cache.rs 🔗

@@ -1,134 +0,0 @@
-use crate::{AppContext, ImageData, ImageId, SharedUri, Task};
-use collections::HashMap;
-use futures::{future::Shared, AsyncReadExt, FutureExt, TryFutureExt};
-use image::ImageError;
-use parking_lot::Mutex;
-use std::path::PathBuf;
-use std::sync::Arc;
-use thiserror::Error;
-use util::http::{self, HttpClient};
-
-pub use image::ImageFormat;
-
-#[derive(PartialEq, Eq, Hash, Clone)]
-pub(crate) struct RenderImageParams {
-    pub(crate) image_id: ImageId,
-}
-
-#[derive(Debug, Error, Clone)]
-pub(crate) enum Error {
-    #[error("http error: {0}")]
-    Client(#[from] http::Error),
-    #[error("IO error: {0}")]
-    Io(Arc<std::io::Error>),
-    #[error("unexpected http status: {status}, body: {body}")]
-    BadStatus {
-        status: http::StatusCode,
-        body: String,
-    },
-    #[error("image error: {0}")]
-    Image(Arc<ImageError>),
-}
-
-impl From<std::io::Error> for Error {
-    fn from(error: std::io::Error) -> Self {
-        Error::Io(Arc::new(error))
-    }
-}
-
-impl From<ImageError> for Error {
-    fn from(error: ImageError) -> Self {
-        Error::Image(Arc::new(error))
-    }
-}
-
-pub(crate) struct ImageCache {
-    client: Arc<dyn HttpClient>,
-    images: Arc<Mutex<HashMap<UriOrPath, FetchImageTask>>>,
-}
-
-#[derive(Debug, PartialEq, Eq, Hash, Clone)]
-pub(crate) enum UriOrPath {
-    Uri(SharedUri),
-    Path(Arc<PathBuf>),
-}
-
-impl From<SharedUri> for UriOrPath {
-    fn from(value: SharedUri) -> Self {
-        Self::Uri(value)
-    }
-}
-
-impl From<Arc<PathBuf>> for UriOrPath {
-    fn from(value: Arc<PathBuf>) -> Self {
-        Self::Path(value)
-    }
-}
-
-type FetchImageTask = Shared<Task<Result<Arc<ImageData>, Error>>>;
-
-impl ImageCache {
-    pub fn new(client: Arc<dyn HttpClient>) -> Self {
-        ImageCache {
-            client,
-            images: Default::default(),
-        }
-    }
-
-    pub fn get(&self, uri_or_path: impl Into<UriOrPath>, cx: &AppContext) -> FetchImageTask {
-        let uri_or_path = uri_or_path.into();
-        let mut images = self.images.lock();
-
-        match images.get(&uri_or_path) {
-            Some(future) => future.clone(),
-            None => {
-                let client = self.client.clone();
-                let future = cx
-                    .background_executor()
-                    .spawn(
-                        {
-                            let uri_or_path = uri_or_path.clone();
-                            async move {
-                                match uri_or_path {
-                                    UriOrPath::Path(uri) => {
-                                        let image = image::open(uri.as_ref())?.into_rgba8();
-                                        Ok(Arc::new(ImageData::new(image)))
-                                    }
-                                    UriOrPath::Uri(uri) => {
-                                        let mut response =
-                                            client.get(uri.as_ref(), ().into(), true).await?;
-                                        let mut body = Vec::new();
-                                        response.body_mut().read_to_end(&mut body).await?;
-
-                                        if !response.status().is_success() {
-                                            return Err(Error::BadStatus {
-                                                status: response.status(),
-                                                body: String::from_utf8_lossy(&body).into_owned(),
-                                            });
-                                        }
-
-                                        let format = image::guess_format(&body)?;
-                                        let image =
-                                            image::load_from_memory_with_format(&body, format)?
-                                                .into_rgba8();
-                                        Ok(Arc::new(ImageData::new(image)))
-                                    }
-                                }
-                            }
-                        }
-                        .map_err({
-                            let uri_or_path = uri_or_path.clone();
-                            move |error| {
-                                log::log!(log::Level::Error, "{:?} {:?}", &uri_or_path, &error);
-                                error
-                            }
-                        }),
-                    )
-                    .shared();
-
-                images.insert(uri_or_path, future.clone());
-                future
-            }
-        }
-    }
-}

crates/gpui/src/svg_renderer.rs 🔗

@@ -1,5 +1,6 @@
 use crate::{AssetSource, DevicePixels, IsZero, Result, SharedString, Size};
 use anyhow::anyhow;
+use resvg::tiny_skia::Pixmap;
 use std::{
     hash::Hash,
     sync::{Arc, OnceLock},
@@ -11,10 +12,16 @@ pub(crate) struct RenderSvgParams {
     pub(crate) size: Size<DevicePixels>,
 }
 
+#[derive(Clone)]
 pub(crate) struct SvgRenderer {
     asset_source: Arc<dyn AssetSource>,
 }
 
+pub enum SvgSize {
+    Size(Size<DevicePixels>),
+    ScaleFactor(f32),
+}
+
 impl SvgRenderer {
     pub fn new(asset_source: Arc<dyn AssetSource>) -> Self {
         Self { asset_source }
@@ -27,33 +34,48 @@ impl SvgRenderer {
 
         // Load the tree.
         let bytes = self.asset_source.load(&params.path)?;
+
+        let pixmap = self.render_pixmap(&bytes, SvgSize::Size(params.size))?;
+
+        // Convert the pixmap's pixels into an alpha mask.
+        let alpha_mask = pixmap
+            .pixels()
+            .iter()
+            .map(|p| p.alpha())
+            .collect::<Vec<_>>();
+        Ok(alpha_mask)
+    }
+
+    pub fn render_pixmap(&self, bytes: &[u8], size: SvgSize) -> Result<Pixmap, resvg::usvg::Error> {
         let tree =
             resvg::usvg::Tree::from_data(&bytes, &resvg::usvg::Options::default(), svg_fontdb())?;
 
+        let size = match size {
+            SvgSize::Size(size) => size,
+            SvgSize::ScaleFactor(scale) => crate::size(
+                DevicePixels((tree.size().width() * scale) as i32),
+                DevicePixels((tree.size().height() * scale) as i32),
+            ),
+        };
+
         // Render the SVG to a pixmap with the specified width and height.
         let mut pixmap =
-            resvg::tiny_skia::Pixmap::new(params.size.width.into(), params.size.height.into())
-                .unwrap();
+            resvg::tiny_skia::Pixmap::new(size.width.into(), size.height.into()).unwrap();
+
+        let ratio = size.width.0 as f32 / tree.size().width();
 
-        let ratio = params.size.width.0 as f32 / tree.size().width();
         resvg::render(
             &tree,
             resvg::tiny_skia::Transform::from_scale(ratio, ratio),
             &mut pixmap.as_mut(),
         );
 
-        // Convert the pixmap's pixels into an alpha mask.
-        let alpha_mask = pixmap
-            .pixels()
-            .iter()
-            .map(|p| p.alpha())
-            .collect::<Vec<_>>();
-        Ok(alpha_mask)
+        Ok(pixmap)
     }
 }
 
 /// Returns the global font database used for SVG rendering.
-fn svg_fontdb() -> &'static resvg::usvg::fontdb::Database {
+pub(crate) fn svg_fontdb() -> &'static resvg::usvg::fontdb::Database {
     static FONTDB: OnceLock<resvg::usvg::fontdb::Database> = OnceLock::new();
     FONTDB.get_or_init(|| {
         let mut fontdb = resvg::usvg::fontdb::Database::new();

crates/gpui/src/window/element_cx.rs 🔗

@@ -24,20 +24,21 @@ use std::{
 use anyhow::Result;
 use collections::FxHashMap;
 use derive_more::{Deref, DerefMut};
+use futures::{future::Shared, FutureExt};
 #[cfg(target_os = "macos")]
 use media::core_video::CVImageBuffer;
 use smallvec::SmallVec;
 
 use crate::{
-    prelude::*, size, AnyElement, AnyTooltip, AppContext, AvailableSpace, Bounds, BoxShadow,
-    ContentMask, Corners, CursorStyle, DevicePixels, DispatchNodeId, DispatchPhase, DispatchTree,
-    DrawPhase, ElementId, ElementStateBox, EntityId, FocusHandle, FocusId, FontId, GlobalElementId,
-    GlyphId, Hsla, ImageData, InputHandler, IsZero, KeyContext, KeyEvent, LayoutId,
-    LineLayoutIndex, ModifiersChangedEvent, MonochromeSprite, MouseEvent, PaintQuad, Path, Pixels,
-    PlatformInputHandler, Point, PolychromeSprite, Quad, RenderGlyphParams, RenderImageParams,
-    RenderSvgParams, Scene, Shadow, SharedString, Size, StrikethroughStyle, Style,
-    TextStyleRefinement, TransformationMatrix, Underline, UnderlineStyle, Window, WindowContext,
-    SUBPIXEL_VARIANTS,
+    hash, prelude::*, size, AnyElement, AnyTooltip, AppContext, Asset, AvailableSpace, Bounds,
+    BoxShadow, ContentMask, Corners, CursorStyle, DevicePixels, DispatchNodeId, DispatchPhase,
+    DispatchTree, DrawPhase, ElementId, ElementStateBox, EntityId, FocusHandle, FocusId, FontId,
+    GlobalElementId, GlyphId, Hsla, ImageData, InputHandler, IsZero, KeyContext, KeyEvent,
+    LayoutId, LineLayoutIndex, ModifiersChangedEvent, MonochromeSprite, MouseEvent, PaintQuad,
+    Path, Pixels, PlatformInputHandler, Point, PolychromeSprite, Quad, RenderGlyphParams,
+    RenderImageParams, RenderSvgParams, Scene, Shadow, SharedString, Size, StrikethroughStyle,
+    Style, Task, TextStyleRefinement, TransformationMatrix, Underline, UnderlineStyle, Window,
+    WindowContext, SUBPIXEL_VARIANTS,
 };
 
 pub(crate) type AnyMouseListener =
@@ -665,6 +666,83 @@ impl<'a> ElementContext<'a> {
         result
     }
 
+    /// Remove an asset from GPUI's cache
+    pub fn remove_cached_asset<A: Asset + 'static>(
+        &mut self,
+        source: &A::Source,
+    ) -> Option<A::Output> {
+        self.asset_cache.remove::<A>(source)
+    }
+
+    /// Asynchronously load an asset, if the asset hasn't finished loading this will return None.
+    /// Your view will be re-drawn once the asset has finished loading.
+    ///
+    /// Note that the multiple calls to this method will only result in one `Asset::load` call.
+    /// The results of that call will be cached, and returned on subsequent uses of this API.
+    ///
+    /// Use [Self::remove_cached_asset] to reload your asset.
+    pub fn use_cached_asset<A: Asset + 'static>(
+        &mut self,
+        source: &A::Source,
+    ) -> Option<A::Output> {
+        self.asset_cache.get::<A>(source).or_else(|| {
+            if let Some(asset) = self.use_asset::<A>(source) {
+                self.asset_cache
+                    .insert::<A>(source.to_owned(), asset.clone());
+                Some(asset)
+            } else {
+                None
+            }
+        })
+    }
+
+    /// Asynchronously load an asset, if the asset hasn't finished loading this will return None.
+    /// Your view will be re-drawn once the asset has finished loading.
+    ///
+    /// Note that the multiple calls to this method will only result in one `Asset::load` call at a
+    /// time.
+    ///
+    /// This asset will not be cached by default, see [Self::use_cached_asset]
+    pub fn use_asset<A: Asset + 'static>(&mut self, source: &A::Source) -> Option<A::Output> {
+        let asset_id = (TypeId::of::<A>(), hash(source));
+        let mut is_first = false;
+        let task = self
+            .loading_assets
+            .remove(&asset_id)
+            .map(|boxed_task| *boxed_task.downcast::<Shared<Task<A::Output>>>().unwrap())
+            .unwrap_or_else(|| {
+                is_first = true;
+                let future = A::load(source.clone(), self);
+                let task = self.background_executor().spawn(future).shared();
+                task
+            });
+
+        task.clone().now_or_never().or_else(|| {
+            if is_first {
+                let parent_id = self.parent_view_id();
+                self.spawn({
+                    let task = task.clone();
+                    |mut cx| async move {
+                        task.await;
+
+                        cx.on_next_frame(move |cx| {
+                            if let Some(parent_id) = parent_id {
+                                cx.notify(parent_id)
+                            } else {
+                                cx.refresh()
+                            }
+                        });
+                    }
+                })
+                .detach();
+            }
+
+            self.loading_assets.insert(asset_id, Box::new(task));
+
+            None
+        })
+    }
+
     /// Obtain the current element offset.
     pub fn element_offset(&self) -> Point<Pixels> {
         self.window()

crates/image_viewer/src/image_viewer.rs 🔗

@@ -1,6 +1,6 @@
 use gpui::{
     canvas, div, fill, img, opaque_grey, point, size, AnyElement, AppContext, Bounds, Context,
-    Element, EventEmitter, FocusHandle, FocusableView, InteractiveElement, IntoElement, Model,
+    EventEmitter, FocusHandle, FocusableView, Img, InteractiveElement, IntoElement, Model,
     ParentElement, Render, Styled, Task, View, ViewContext, VisualContext, WeakView, WindowContext,
 };
 use persistence::IMAGE_VIEWER;
@@ -36,8 +36,7 @@ impl project::Item for ImageItem {
             .and_then(OsStr::to_str)
             .unwrap_or_default();
 
-        let format = gpui::ImageFormat::from_extension(ext);
-        if format.is_some() {
+        if Img::extensions().contains(&ext) {
             Some(cx.spawn(|mut cx| async move {
                 let abs_path = project
                     .read_with(&cx, |project, cx| project.absolute_path(&path, cx))?
@@ -156,8 +155,6 @@ impl FocusableView for ImageView {
 
 impl Render for ImageView {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
-        let im = img(self.path.clone()).into_any();
-
         div()
             .track_focus(&self.focus_handle)
             .size_full()
@@ -210,10 +207,12 @@ impl Render for ImageView {
                 .left_0(),
             )
             .child(
-                v_flex()
-                    .h_full()
-                    .justify_around()
-                    .child(h_flex().w_full().justify_around().child(im)),
+                v_flex().h_full().justify_around().child(
+                    h_flex()
+                        .w_full()
+                        .justify_around()
+                        .child(img(self.path.clone())),
+                ),
             )
     }
 }