From f9becbd3d1fe53da97fb1f01b6531b588b4e5829 Mon Sep 17 00:00:00 2001 From: Matthias Grandl <50196894+MatthiasGrandl@users.noreply.github.com> Date: Sat, 30 Mar 2024 01:09:49 +0100 Subject: [PATCH] gpui: Add SVG rendering to `img` element and generic asset cache (#9931) 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 --- 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 | 50 ++++-- crates/gpui/src/window/element_cx.rs | 96 ++++++++++- crates/image_viewer/src/image_viewer.rs | 17 +- 11 files changed, 392 insertions(+), 237 deletions(-) create mode 100644 crates/gpui/src/asset_cache.rs delete mode 100644 crates/gpui/src/image_cache.rs diff --git a/crates/collections/src/collections.rs b/crates/collections/src/collections.rs index be41aaebd6..25f6135c1f 100644 --- a/crates/collections/src/collections.rs +++ b/crates/collections/src/collections.rs @@ -10,5 +10,6 @@ pub type HashMap = std::collections::HashMap; #[cfg(not(feature = "test-support"))] pub type HashSet = std::collections::HashSet; +pub use rustc_hash::FxHasher; pub use rustc_hash::{FxHashMap, FxHashSet}; pub use std::collections::*; diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index d27f9a353d..2cf569d35c 100644 --- a/crates/gpui/Cargo.toml +++ b/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"] } diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 7602398895..894b51e856 100644 --- a/crates/gpui/src/app.rs +++ b/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, pub(crate) background_executor: BackgroundExecutor, pub(crate) foreground_executor: ForegroundExecutor, - pub(crate) svg_renderer: SvgRenderer, + pub(crate) loading_assets: FxHashMap<(TypeId, u64), Box>, + pub(crate) asset_cache: AssetCache, asset_source: Arc, - pub(crate) image_cache: ImageCache, + pub(crate) svg_renderer: SvgRenderer, + http_client: Arc, pub(crate) globals_by_type: FxHashMap>, pub(crate) entities: EntityMap, pub(crate) new_view_observers: SubscriberSet, @@ -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 { + 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 } => { diff --git a/crates/gpui/src/asset_cache.rs b/crates/gpui/src/asset_cache.rs new file mode 100644 index 0000000000..070aff32f6 --- /dev/null +++ b/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), +} + +impl From for UriOrPath { + fn from(value: SharedUri) -> Self { + Self::Uri(value) + } +} + +impl From> for UriOrPath { + fn from(value: Arc) -> 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 + Send + 'static; +} + +/// Use a quick, non-cryptographically secure hash function to get an identifier from data +pub fn 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>>>, +} + +impl AssetCache { + pub(crate) fn new() -> Self { + Self { + assets: Default::default(), + } + } + + /// Get the asset from the cache, if it exists. + pub fn get(&self, source: &A::Source) -> Option { + self.assets + .lock() + .get(&(TypeId::of::(), hash(&source))) + .and_then(|task| task.downcast_ref::()) + .cloned() + } + + /// Insert the asset into the cache. + pub fn insert(&mut self, source: A::Source, output: A::Output) { + self.assets + .lock() + .insert((TypeId::of::(), hash(&source)), Box::new(output)); + } + + /// Remove an entry from the asset cache + pub fn remove(&mut self, source: &A::Source) -> Option { + self.assets + .lock() + .remove(&(TypeId::of::(), hash(&source))) + .and_then(|any| any.downcast::().ok()) + .map(|boxed| *boxed) + } +} diff --git a/crates/gpui/src/assets.rs b/crates/gpui/src/assets.rs index 37f67ed1bb..b6c021ba38 100644 --- a/crates/gpui/src/assets.rs +++ b/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 diff --git a/crates/gpui/src/elements/img.rs b/crates/gpui/src/elements/img.rs index 953e45d0a0..cb41010cf6 100644 --- a/crates/gpui/src/elements/img.rs +++ b/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 for ImageSource { } } -impl ImageSource { - fn data(&self, cx: &mut ElementContext) -> Option> { - 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> { + 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::(&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, ImageCacheError>; + + fn load( + source: Self::Source, + cx: &mut WindowContext, + ) -> impl Future + 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), + /// 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), + /// An error that occurred while processing an SVG. + #[error("svg error: {0}")] + Usvg(Arc), +} + +impl From for ImageCacheError { + fn from(error: std::io::Error) -> Self { + Self::Io(Arc::new(error)) + } +} + +impl From for ImageCacheError { + fn from(error: ImageError) -> Self { + Self::Image(Arc::new(error)) + } +} + +impl From for ImageCacheError { + fn from(error: resvg::usvg::Error) -> Self { + Self::Usvg(Arc::new(error)) + } +} diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index adde92670c..1821c24401 100644 --- a/crates/gpui/src/gpui.rs +++ b/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::*; diff --git a/crates/gpui/src/image_cache.rs b/crates/gpui/src/image_cache.rs deleted file mode 100644 index eb6aa42671..0000000000 --- a/crates/gpui/src/image_cache.rs +++ /dev/null @@ -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), - #[error("unexpected http status: {status}, body: {body}")] - BadStatus { - status: http::StatusCode, - body: String, - }, - #[error("image error: {0}")] - Image(Arc), -} - -impl From for Error { - fn from(error: std::io::Error) -> Self { - Error::Io(Arc::new(error)) - } -} - -impl From for Error { - fn from(error: ImageError) -> Self { - Error::Image(Arc::new(error)) - } -} - -pub(crate) struct ImageCache { - client: Arc, - images: Arc>>, -} - -#[derive(Debug, PartialEq, Eq, Hash, Clone)] -pub(crate) enum UriOrPath { - Uri(SharedUri), - Path(Arc), -} - -impl From for UriOrPath { - fn from(value: SharedUri) -> Self { - Self::Uri(value) - } -} - -impl From> for UriOrPath { - fn from(value: Arc) -> Self { - Self::Path(value) - } -} - -type FetchImageTask = Shared, Error>>>; - -impl ImageCache { - pub fn new(client: Arc) -> Self { - ImageCache { - client, - images: Default::default(), - } - } - - pub fn get(&self, uri_or_path: impl Into, 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 - } - } - } -} diff --git a/crates/gpui/src/svg_renderer.rs b/crates/gpui/src/svg_renderer.rs index c1aa8881de..af4f7651ae 100644 --- a/crates/gpui/src/svg_renderer.rs +++ b/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, } +#[derive(Clone)] pub(crate) struct SvgRenderer { asset_source: Arc, } +pub enum SvgSize { + Size(Size), + ScaleFactor(f32), +} + impl SvgRenderer { pub fn new(asset_source: Arc) -> Self { Self { asset_source } @@ -27,20 +34,8 @@ impl SvgRenderer { // Load the tree. let bytes = self.asset_source.load(¶ms.path)?; - let tree = - resvg::usvg::Tree::from_data(&bytes, &resvg::usvg::Options::default(), svg_fontdb())?; - // 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(); - - 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(), - ); + let pixmap = self.render_pixmap(&bytes, SvgSize::Size(params.size))?; // Convert the pixmap's pixels into an alpha mask. let alpha_mask = pixmap @@ -50,10 +45,37 @@ impl SvgRenderer { .collect::>(); Ok(alpha_mask) } + + pub fn render_pixmap(&self, bytes: &[u8], size: SvgSize) -> Result { + 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(size.width.into(), size.height.into()).unwrap(); + + let ratio = size.width.0 as f32 / tree.size().width(); + + resvg::render( + &tree, + resvg::tiny_skia::Transform::from_scale(ratio, ratio), + &mut pixmap.as_mut(), + ); + + 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 = OnceLock::new(); FONTDB.get_or_init(|| { let mut fontdb = resvg::usvg::fontdb::Database::new(); diff --git a/crates/gpui/src/window/element_cx.rs b/crates/gpui/src/window/element_cx.rs index 3f56d39fc1..2648c045b1 100644 --- a/crates/gpui/src/window/element_cx.rs +++ b/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( + &mut self, + source: &A::Source, + ) -> Option { + self.asset_cache.remove::(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( + &mut self, + source: &A::Source, + ) -> Option { + self.asset_cache.get::(source).or_else(|| { + if let Some(asset) = self.use_asset::(source) { + self.asset_cache + .insert::(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(&mut self, source: &A::Source) -> Option { + let asset_id = (TypeId::of::(), hash(source)); + let mut is_first = false; + let task = self + .loading_assets + .remove(&asset_id) + .map(|boxed_task| *boxed_task.downcast::>>().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 { self.window() diff --git a/crates/image_viewer/src/image_viewer.rs b/crates/image_viewer/src/image_viewer.rs index d9a30c9e77..a866e272b6 100644 --- a/crates/image_viewer/src/image_viewer.rs +++ b/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) -> 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())), + ), ) } }