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:
parent
ed5bfcdddc
commit
f9becbd3d1
11 changed files with 392 additions and 237 deletions
|
@ -10,5 +10,6 @@ pub type HashMap<K, V> = std::collections::HashMap<K, V>;
|
||||||
#[cfg(not(feature = "test-support"))]
|
#[cfg(not(feature = "test-support"))]
|
||||||
pub type HashSet<T> = std::collections::HashSet<T>;
|
pub type HashSet<T> = std::collections::HashSet<T>;
|
||||||
|
|
||||||
|
pub use rustc_hash::FxHasher;
|
||||||
pub use rustc_hash::{FxHashMap, FxHashSet};
|
pub use rustc_hash::{FxHashMap, FxHashSet};
|
||||||
pub use std::collections::*;
|
pub use std::collections::*;
|
||||||
|
|
|
@ -72,6 +72,9 @@ util.workspace = true
|
||||||
uuid = { version = "1.1.2", features = ["v4", "v5"] }
|
uuid = { version = "1.1.2", features = ["v4", "v5"] }
|
||||||
waker-fn = "1.1.0"
|
waker-fn = "1.1.0"
|
||||||
|
|
||||||
|
[profile.dev.package]
|
||||||
|
resvg = { opt-level = 3 }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
backtrace = "0.3"
|
backtrace = "0.3"
|
||||||
collections = { workspace = true, features = ["test-support"] }
|
collections = { workspace = true, features = ["test-support"] }
|
||||||
|
|
|
@ -28,8 +28,8 @@ use util::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
current_platform, image_cache::ImageCache, init_app_menus, Action, ActionRegistry, Any,
|
current_platform, init_app_menus, Action, ActionRegistry, Any, AnyView, AnyWindowHandle,
|
||||||
AnyView, AnyWindowHandle, AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context,
|
AppMetadata, AssetCache, AssetSource, BackgroundExecutor, ClipboardItem, Context,
|
||||||
DispatchPhase, DisplayId, Entity, EventEmitter, ForegroundExecutor, Global, KeyBinding, Keymap,
|
DispatchPhase, DisplayId, Entity, EventEmitter, ForegroundExecutor, Global, KeyBinding, Keymap,
|
||||||
Keystroke, LayoutId, Menu, PathPromptOptions, Pixels, Platform, PlatformDisplay, Point,
|
Keystroke, LayoutId, Menu, PathPromptOptions, Pixels, Platform, PlatformDisplay, Point,
|
||||||
PromptBuilder, PromptHandle, PromptLevel, Render, RenderablePromptHandle, SharedString,
|
PromptBuilder, PromptHandle, PromptLevel, Render, RenderablePromptHandle, SharedString,
|
||||||
|
@ -217,9 +217,11 @@ pub struct AppContext {
|
||||||
pub(crate) active_drag: Option<AnyDrag>,
|
pub(crate) active_drag: Option<AnyDrag>,
|
||||||
pub(crate) background_executor: BackgroundExecutor,
|
pub(crate) background_executor: BackgroundExecutor,
|
||||||
pub(crate) foreground_executor: ForegroundExecutor,
|
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>,
|
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) globals_by_type: FxHashMap<TypeId, Box<dyn Any>>,
|
||||||
pub(crate) entities: EntityMap,
|
pub(crate) entities: EntityMap,
|
||||||
pub(crate) new_view_observers: SubscriberSet<TypeId, NewViewListener>,
|
pub(crate) new_view_observers: SubscriberSet<TypeId, NewViewListener>,
|
||||||
|
@ -279,8 +281,10 @@ impl AppContext {
|
||||||
background_executor: executor,
|
background_executor: executor,
|
||||||
foreground_executor,
|
foreground_executor,
|
||||||
svg_renderer: SvgRenderer::new(asset_source.clone()),
|
svg_renderer: SvgRenderer::new(asset_source.clone()),
|
||||||
|
asset_cache: AssetCache::new(),
|
||||||
|
loading_assets: Default::default(),
|
||||||
asset_source,
|
asset_source,
|
||||||
image_cache: ImageCache::new(http_client),
|
http_client,
|
||||||
globals_by_type: FxHashMap::default(),
|
globals_by_type: FxHashMap::default(),
|
||||||
entities,
|
entities,
|
||||||
new_view_observers: SubscriberSet::new(),
|
new_view_observers: SubscriberSet::new(),
|
||||||
|
@ -635,6 +639,16 @@ impl AppContext {
|
||||||
self.platform.local_timezone()
|
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) {
|
pub(crate) fn push_effect(&mut self, effect: Effect) {
|
||||||
match &effect {
|
match &effect {
|
||||||
Effect::Notify { emitter } => {
|
Effect::Notify { emitter } => {
|
||||||
|
|
87
crates/gpui/src/asset_cache.rs
Normal file
87
crates/gpui/src/asset_cache.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -34,6 +34,11 @@ impl AssetSource for () {
|
||||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||||
pub struct ImageId(usize);
|
pub struct ImageId(usize);
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Hash, Clone)]
|
||||||
|
pub(crate) struct RenderImageParams {
|
||||||
|
pub(crate) image_id: ImageId,
|
||||||
|
}
|
||||||
|
|
||||||
/// A cached and processed image.
|
/// A cached and processed image.
|
||||||
pub struct ImageData {
|
pub struct ImageData {
|
||||||
/// The ID associated with this image
|
/// The ID associated with this image
|
||||||
|
|
|
@ -1,15 +1,19 @@
|
||||||
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
point, px, size, AbsoluteLength, Bounds, DefiniteLength, DevicePixels, Element, ElementContext,
|
point, px, size, AbsoluteLength, Asset, Bounds, DefiniteLength, DevicePixels, Element,
|
||||||
Hitbox, ImageData, InteractiveElement, Interactivity, IntoElement, LayoutId, Length, Pixels,
|
ElementContext, Hitbox, ImageData, InteractiveElement, Interactivity, IntoElement, LayoutId,
|
||||||
SharedUri, Size, StyleRefinement, Styled, UriOrPath,
|
Length, Pixels, SharedUri, Size, StyleRefinement, Styled, SvgSize, UriOrPath, WindowContext,
|
||||||
};
|
};
|
||||||
use futures::FutureExt;
|
use futures::{AsyncReadExt, Future};
|
||||||
|
use image::{ImageBuffer, ImageError};
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
use media::core_video::CVImageBuffer;
|
use media::core_video::CVImageBuffer;
|
||||||
use util::ResultExt;
|
|
||||||
|
use thiserror::Error;
|
||||||
|
use util::{http, ResultExt};
|
||||||
|
|
||||||
/// A source of image content.
|
/// A source of image content.
|
||||||
#[derive(Clone, Debug)]
|
#[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.
|
/// An image element.
|
||||||
pub struct Img {
|
pub struct Img {
|
||||||
interactivity: Interactivity,
|
interactivity: Interactivity,
|
||||||
|
@ -201,6 +167,15 @@ impl ObjectFit {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Img {
|
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.
|
/// Set the image to be displayed in grayscale.
|
||||||
pub fn grayscale(mut self, grayscale: bool) -> Self {
|
pub fn grayscale(mut self, grayscale: bool) -> Self {
|
||||||
self.grayscale = grayscale;
|
self.grayscale = grayscale;
|
||||||
|
@ -235,6 +210,7 @@ impl Element for Img {
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cx.request_layout(&style, [])
|
cx.request_layout(&style, [])
|
||||||
});
|
});
|
||||||
(layout_id, ())
|
(layout_id, ())
|
||||||
|
@ -262,28 +238,20 @@ impl Element for Img {
|
||||||
.paint(bounds, hitbox.as_ref(), cx, |style, cx| {
|
.paint(bounds, hitbox.as_ref(), cx, |style, cx| {
|
||||||
let corner_radii = style.corner_radii.to_pixels(bounds.size, cx.rem_size());
|
let corner_radii = style.corner_radii.to_pixels(bounds.size, cx.rem_size());
|
||||||
|
|
||||||
match source.data(cx) {
|
if let Some(data) = source.data(cx) {
|
||||||
Some(data) => {
|
cx.paint_image(bounds, corner_radii, data.clone(), self.grayscale)
|
||||||
let bounds = self.object_fit.get_bounds(bounds, data.size());
|
.log_err();
|
||||||
cx.paint_image(bounds, corner_radii, data, self.grayscale)
|
}
|
||||||
.log_err();
|
|
||||||
}
|
match source {
|
||||||
#[cfg(not(target_os = "macos"))]
|
|
||||||
None => {
|
|
||||||
// No renderable image loaded yet. Do nothing.
|
|
||||||
}
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
None => match source {
|
ImageSource::Surface(surface) => {
|
||||||
ImageSource::Surface(surface) => {
|
let size = size(surface.width().into(), surface.height().into());
|
||||||
let size = size(surface.width().into(), surface.height().into());
|
let new_bounds = self.object_fit.get_bounds(bounds, size);
|
||||||
let new_bounds = self.object_fit.get_bounds(bounds, size);
|
// TODO: Add support for corner_radii and grayscale.
|
||||||
// TODO: Add support for corner_radii and grayscale.
|
cx.paint_surface(new_bounds, surface);
|
||||||
cx.paint_surface(new_bounds, surface);
|
}
|
||||||
}
|
_ => {}
|
||||||
_ => {
|
|
||||||
// No renderable image loaded yet. Do nothing.
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -308,3 +276,115 @@ impl InteractiveElement for Img {
|
||||||
&mut self.interactivity
|
&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 app;
|
||||||
|
|
||||||
mod arena;
|
mod arena;
|
||||||
|
mod asset_cache;
|
||||||
mod assets;
|
mod assets;
|
||||||
mod bounds_tree;
|
mod bounds_tree;
|
||||||
mod color;
|
mod color;
|
||||||
|
@ -76,7 +77,6 @@ mod element;
|
||||||
mod elements;
|
mod elements;
|
||||||
mod executor;
|
mod executor;
|
||||||
mod geometry;
|
mod geometry;
|
||||||
mod image_cache;
|
|
||||||
mod input;
|
mod input;
|
||||||
mod interactive;
|
mod interactive;
|
||||||
mod key_dispatch;
|
mod key_dispatch;
|
||||||
|
@ -117,6 +117,7 @@ pub use action::*;
|
||||||
pub use anyhow::Result;
|
pub use anyhow::Result;
|
||||||
pub use app::*;
|
pub use app::*;
|
||||||
pub(crate) use arena::*;
|
pub(crate) use arena::*;
|
||||||
|
pub use asset_cache::*;
|
||||||
pub use assets::*;
|
pub use assets::*;
|
||||||
pub use color::*;
|
pub use color::*;
|
||||||
pub use ctor::ctor;
|
pub use ctor::ctor;
|
||||||
|
@ -125,7 +126,6 @@ pub use elements::*;
|
||||||
pub use executor::*;
|
pub use executor::*;
|
||||||
pub use geometry::*;
|
pub use geometry::*;
|
||||||
pub use gpui_macros::{register_action, test, IntoElement, Render};
|
pub use gpui_macros::{register_action, test, IntoElement, Render};
|
||||||
pub use image_cache::*;
|
|
||||||
pub use input::*;
|
pub use input::*;
|
||||||
pub use interactive::*;
|
pub use interactive::*;
|
||||||
use key_dispatch::*;
|
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 crate::{AssetSource, DevicePixels, IsZero, Result, SharedString, Size};
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
|
use resvg::tiny_skia::Pixmap;
|
||||||
use std::{
|
use std::{
|
||||||
hash::Hash,
|
hash::Hash,
|
||||||
sync::{Arc, OnceLock},
|
sync::{Arc, OnceLock},
|
||||||
|
@ -11,10 +12,16 @@ pub(crate) struct RenderSvgParams {
|
||||||
pub(crate) size: Size<DevicePixels>,
|
pub(crate) size: Size<DevicePixels>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub(crate) struct SvgRenderer {
|
pub(crate) struct SvgRenderer {
|
||||||
asset_source: Arc<dyn AssetSource>,
|
asset_source: Arc<dyn AssetSource>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum SvgSize {
|
||||||
|
Size(Size<DevicePixels>),
|
||||||
|
ScaleFactor(f32),
|
||||||
|
}
|
||||||
|
|
||||||
impl SvgRenderer {
|
impl SvgRenderer {
|
||||||
pub fn new(asset_source: Arc<dyn AssetSource>) -> Self {
|
pub fn new(asset_source: Arc<dyn AssetSource>) -> Self {
|
||||||
Self { asset_source }
|
Self { asset_source }
|
||||||
|
@ -27,20 +34,8 @@ impl SvgRenderer {
|
||||||
|
|
||||||
// Load the tree.
|
// Load the tree.
|
||||||
let bytes = self.asset_source.load(¶ms.path)?;
|
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 pixmap = self.render_pixmap(&bytes, SvgSize::Size(params.size))?;
|
||||||
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(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Convert the pixmap's pixels into an alpha mask.
|
// Convert the pixmap's pixels into an alpha mask.
|
||||||
let alpha_mask = pixmap
|
let alpha_mask = pixmap
|
||||||
|
@ -50,10 +45,37 @@ impl SvgRenderer {
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
Ok(alpha_mask)
|
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.
|
/// 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();
|
static FONTDB: OnceLock<resvg::usvg::fontdb::Database> = OnceLock::new();
|
||||||
FONTDB.get_or_init(|| {
|
FONTDB.get_or_init(|| {
|
||||||
let mut fontdb = resvg::usvg::fontdb::Database::new();
|
let mut fontdb = resvg::usvg::fontdb::Database::new();
|
||||||
|
|
|
@ -24,20 +24,21 @@ use std::{
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use collections::FxHashMap;
|
use collections::FxHashMap;
|
||||||
use derive_more::{Deref, DerefMut};
|
use derive_more::{Deref, DerefMut};
|
||||||
|
use futures::{future::Shared, FutureExt};
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
use media::core_video::CVImageBuffer;
|
use media::core_video::CVImageBuffer;
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
prelude::*, size, AnyElement, AnyTooltip, AppContext, AvailableSpace, Bounds, BoxShadow,
|
hash, prelude::*, size, AnyElement, AnyTooltip, AppContext, Asset, AvailableSpace, Bounds,
|
||||||
ContentMask, Corners, CursorStyle, DevicePixels, DispatchNodeId, DispatchPhase, DispatchTree,
|
BoxShadow, ContentMask, Corners, CursorStyle, DevicePixels, DispatchNodeId, DispatchPhase,
|
||||||
DrawPhase, ElementId, ElementStateBox, EntityId, FocusHandle, FocusId, FontId, GlobalElementId,
|
DispatchTree, DrawPhase, ElementId, ElementStateBox, EntityId, FocusHandle, FocusId, FontId,
|
||||||
GlyphId, Hsla, ImageData, InputHandler, IsZero, KeyContext, KeyEvent, LayoutId,
|
GlobalElementId, GlyphId, Hsla, ImageData, InputHandler, IsZero, KeyContext, KeyEvent,
|
||||||
LineLayoutIndex, ModifiersChangedEvent, MonochromeSprite, MouseEvent, PaintQuad, Path, Pixels,
|
LayoutId, LineLayoutIndex, ModifiersChangedEvent, MonochromeSprite, MouseEvent, PaintQuad,
|
||||||
PlatformInputHandler, Point, PolychromeSprite, Quad, RenderGlyphParams, RenderImageParams,
|
Path, Pixels, PlatformInputHandler, Point, PolychromeSprite, Quad, RenderGlyphParams,
|
||||||
RenderSvgParams, Scene, Shadow, SharedString, Size, StrikethroughStyle, Style,
|
RenderImageParams, RenderSvgParams, Scene, Shadow, SharedString, Size, StrikethroughStyle,
|
||||||
TextStyleRefinement, TransformationMatrix, Underline, UnderlineStyle, Window, WindowContext,
|
Style, Task, TextStyleRefinement, TransformationMatrix, Underline, UnderlineStyle, Window,
|
||||||
SUBPIXEL_VARIANTS,
|
WindowContext, SUBPIXEL_VARIANTS,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) type AnyMouseListener =
|
pub(crate) type AnyMouseListener =
|
||||||
|
@ -665,6 +666,83 @@ impl<'a> ElementContext<'a> {
|
||||||
result
|
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.
|
/// Obtain the current element offset.
|
||||||
pub fn element_offset(&self) -> Point<Pixels> {
|
pub fn element_offset(&self) -> Point<Pixels> {
|
||||||
self.window()
|
self.window()
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use gpui::{
|
use gpui::{
|
||||||
canvas, div, fill, img, opaque_grey, point, size, AnyElement, AppContext, Bounds, Context,
|
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,
|
ParentElement, Render, Styled, Task, View, ViewContext, VisualContext, WeakView, WindowContext,
|
||||||
};
|
};
|
||||||
use persistence::IMAGE_VIEWER;
|
use persistence::IMAGE_VIEWER;
|
||||||
|
@ -36,8 +36,7 @@ impl project::Item for ImageItem {
|
||||||
.and_then(OsStr::to_str)
|
.and_then(OsStr::to_str)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let format = gpui::ImageFormat::from_extension(ext);
|
if Img::extensions().contains(&ext) {
|
||||||
if format.is_some() {
|
|
||||||
Some(cx.spawn(|mut cx| async move {
|
Some(cx.spawn(|mut cx| async move {
|
||||||
let abs_path = project
|
let abs_path = project
|
||||||
.read_with(&cx, |project, cx| project.absolute_path(&path, cx))?
|
.read_with(&cx, |project, cx| project.absolute_path(&path, cx))?
|
||||||
|
@ -156,8 +155,6 @@ impl FocusableView for ImageView {
|
||||||
|
|
||||||
impl Render for ImageView {
|
impl Render for ImageView {
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
let im = img(self.path.clone()).into_any();
|
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.track_focus(&self.focus_handle)
|
.track_focus(&self.focus_handle)
|
||||||
.size_full()
|
.size_full()
|
||||||
|
@ -210,10 +207,12 @@ impl Render for ImageView {
|
||||||
.left_0(),
|
.left_0(),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex().h_full().justify_around().child(
|
||||||
.h_full()
|
h_flex()
|
||||||
.justify_around()
|
.w_full()
|
||||||
.child(h_flex().w_full().justify_around().child(im)),
|
.justify_around()
|
||||||
|
.child(img(self.path.clone())),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue