Detailed changes
@@ -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::*;
@@ -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"] }
@@ -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 } => {
@@ -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)
+ }
+}
@@ -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
@@ -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))
+ }
+}
@@ -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::*;
@@ -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
- }
- }
- }
-}
@@ -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(¶ms.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();
@@ -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()
@@ -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())),
+ ),
)
}
}