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 <mikayla@zed.dev>
This commit is contained in:
Matthias Grandl 2024-03-30 01:09:49 +01:00 committed by GitHub
parent ed5bfcdddc
commit f9becbd3d1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 392 additions and 237 deletions

View File

@ -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::*;

View File

@ -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"] }

View File

@ -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 } => {

View File

@ -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)
}
}

View File

@ -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

View File

@ -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))
}
}

View File

@ -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::*;

View File

@ -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
}
}
}
}

View File

@ -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,20 +34,8 @@ impl SvgRenderer {
// Load the tree.
let bytes = self.asset_source.load(&params.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::<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(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<resvg::usvg::fontdb::Database> = OnceLock::new();
FONTDB.get_or_init(|| {
let mut fontdb = resvg::usvg::fontdb::Database::new();

View File

@ -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()

View File

@ -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())),
),
)
}
}