This builds on #9353 by adding an image viewer to Zed. Closes #5251. Release Notes: - Added support for rendering image files ([#5251](https://github.com/zed-industries/zed/issues/5251)). <img width="1840" alt="image" src="https://github.com/zed-industries/zed/assets/836375/3bccfa8e-aa5c-421f-9dfa-671caa274c3c"> --------- Co-authored-by: Mikayla Maki <mikayla@zed.dev>
135 lines
4.4 KiB
Rust
135 lines
4.4 KiB
Rust
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_bgra8();
|
|
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_bgra8();
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}
|