mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-24 06:12:25 +03:00
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
@ -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 } => {
|
||||
|
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)]
|
||||
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,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::<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();
|
||||
|
@ -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())),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user