Cargo.lock 🔗
@@ -3162,6 +3162,7 @@ dependencies = [
"sqlez",
"sum_tree",
"taffy",
+ "thiserror",
"time 0.3.27",
"tiny-skia",
"usvg",
Nathan Sobo created
Cargo.lock | 1
crates/gpui/Cargo.toml | 1
crates/gpui/src/app.rs | 11 ++
crates/gpui/src/app/test_app_context.rs | 1
crates/gpui/src/gpui.rs | 1
crates/gpui/src/image_cache.rs | 111 +++++++++++++++++++++++++++
crates/gpui2/src/elements/text.rs | 2
crates/gpui2/src/gpui2.rs | 3
crates/util/src/arc_cow.rs | 10 ++
crates/util/src/http.rs | 2
crates/util/src/util.rs | 17 +++
11 files changed, 151 insertions(+), 9 deletions(-)
@@ -3162,6 +3162,7 @@ dependencies = [
"sqlez",
"sum_tree",
"taffy",
+ "thiserror",
"time 0.3.27",
"tiny-skia",
"usvg",
@@ -49,6 +49,7 @@ serde_json.workspace = true
smallvec.workspace = true
smol.workspace = true
taffy = { git = "https://github.com/DioxusLabs/taffy", rev = "4fb530bdd71609bb1d3f76c6a8bde1ba82805d5e" }
+thiserror.workspace = true
time.workspace = true
tiny-skia = "0.5"
usvg = { version = "0.14", features = [] }
@@ -10,6 +10,7 @@ mod window_input_handler;
use crate::{
elements::{AnyElement, AnyRootElement, RootElement},
executor::{self, Task},
+ image_cache::ImageCache,
json,
keymap_matcher::{self, Binding, KeymapContext, KeymapMatcher, Keystroke, MatchResult},
platform::{
@@ -50,7 +51,10 @@ use std::{
};
#[cfg(any(test, feature = "test-support"))]
pub use test_app_context::{ContextHandle, TestAppContext};
-use util::ResultExt;
+use util::{
+ http::{self, HttpClient},
+ ResultExt,
+};
use uuid::Uuid;
pub use window::MeasureParams;
use window_input_handler::WindowInputHandler;
@@ -154,12 +158,14 @@ impl App {
let platform = platform::current::platform();
let foreground = Rc::new(executor::Foreground::platform(platform.dispatcher())?);
let foreground_platform = platform::current::foreground_platform(foreground.clone());
+ let http_client = http::client();
let app = Self(Rc::new(RefCell::new(AppContext::new(
foreground,
Arc::new(executor::Background::new()),
platform.clone(),
foreground_platform.clone(),
Arc::new(FontCache::new(platform.fonts())),
+ http_client,
Default::default(),
asset_source,
))));
@@ -456,6 +462,7 @@ pub struct AppContext {
pub asset_cache: Arc<AssetCache>,
font_system: Arc<dyn FontSystem>,
pub font_cache: Arc<FontCache>,
+ pub image_cache: Arc<ImageCache>,
action_deserializers: HashMap<&'static str, (TypeId, DeserializeActionCallback)>,
capture_actions: HashMap<TypeId, HashMap<TypeId, Vec<Box<ActionCallback>>>>,
// Entity Types -> { Action Types -> Action Handlers }
@@ -499,6 +506,7 @@ impl AppContext {
platform: Arc<dyn platform::Platform>,
foreground_platform: Rc<dyn platform::ForegroundPlatform>,
font_cache: Arc<FontCache>,
+ http_client: Arc<dyn HttpClient>,
ref_counts: RefCounts,
asset_source: impl AssetSource,
) -> Self {
@@ -517,6 +525,7 @@ impl AppContext {
platform,
foreground_platform,
font_cache,
+ image_cache: Arc::new(ImageCache::new(http_client)),
asset_cache: Arc::new(AssetCache::new(asset_source)),
action_deserializers: Default::default(),
capture_actions: Default::default(),
@@ -57,6 +57,7 @@ impl TestAppContext {
platform,
foreground_platform.clone(),
font_cache,
+ util::http::FakeHttpClient::with_404_response(),
RefCounts::new(leak_detector),
(),
);
@@ -1,4 +1,5 @@
mod app;
+mod image_cache;
pub use app::*;
mod assets;
#[cfg(any(test, feature = "test-support"))]
@@ -0,0 +1,111 @@
+use std::sync::Arc;
+
+use crate::ImageData;
+use collections::HashMap;
+use futures::{
+ future::{BoxFuture, Shared},
+ AsyncReadExt, FutureExt,
+};
+use image::ImageError;
+use parking_lot::Mutex;
+use thiserror::Error;
+use util::{
+ arc_cow::ArcCow,
+ defer,
+ http::{self, HttpClient},
+};
+
+#[derive(Debug, Error, Clone)]
+pub 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 struct ImageCache {
+ client: Arc<dyn HttpClient>,
+ images: Arc<
+ Mutex<
+ HashMap<
+ ArcCow<'static, str>,
+ Shared<BoxFuture<'static, Result<Arc<ImageData>, Error>>>,
+ >,
+ >,
+ >,
+}
+
+impl ImageCache {
+ pub fn new(client: Arc<dyn HttpClient>) -> Self {
+ ImageCache {
+ client,
+ images: Default::default(),
+ }
+ }
+
+ pub fn get(
+ &self,
+ uri: ArcCow<'static, str>,
+ ) -> Shared<BoxFuture<'static, Result<Arc<ImageData>, Error>>> {
+ match self.images.lock().get(uri.as_ref()) {
+ Some(future) => future.clone(),
+ None => {
+ let client = self.client.clone();
+ let images = self.images.clone();
+ let future = {
+ let uri = uri.clone();
+ async move {
+ // If we error, remove the cached future. Otherwise we cancel before returning.
+ let remove_cached_future = defer({
+ let uri = uri.clone();
+ move || {
+ images.lock().remove(uri.as_ref());
+ }
+ });
+
+ 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();
+
+ remove_cached_future.cancel();
+ Ok(ImageData::new(image))
+ }
+ }
+ .boxed()
+ .shared();
+ self.images.lock().insert(uri.clone(), future.clone());
+ future
+ }
+ }
+ }
+}
@@ -2,12 +2,12 @@ use crate::{
element::{Element, IntoElement, Layout},
layout_context::LayoutContext,
paint_context::PaintContext,
- ArcCow,
};
use anyhow::Result;
use gpui::{geometry::Size, text_layout::LineLayout, LayoutId};
use parking_lot::Mutex;
use std::sync::Arc;
+use util::arc_cow::ArcCow;
impl<V: 'static, S: Into<ArcCow<'static, str>>> IntoElement<V> for S {
type Element = Text;
@@ -1,5 +1,4 @@
pub mod adapter;
-mod arc_cow;
pub mod color;
pub mod element;
pub mod elements;
@@ -9,7 +8,6 @@ pub mod paint_context;
pub mod style;
pub mod view;
-pub use arc_cow::ArcCow;
pub use color::*;
pub use element::{AnyElement, Element, IntoElement, Layout, ParentElement};
pub use geometry::{
@@ -21,4 +19,5 @@ pub use gpui2_macros::{Element, *};
pub use interactive::*;
pub use layout_context::LayoutContext;
pub use platform::{Platform, WindowBounds, WindowOptions};
+pub use util::arc_cow::ArcCow;
pub use view::*;
@@ -1,5 +1,6 @@
use std::sync::Arc;
+#[derive(PartialEq, Eq, Hash)]
pub enum ArcCow<'a, T: ?Sized> {
Borrowed(&'a T),
Owned(Arc<T>),
@@ -32,6 +33,15 @@ impl From<String> for ArcCow<'_, str> {
}
}
+impl<'a, T: ?Sized + ToOwned> std::borrow::Borrow<T> for ArcCow<'a, T> {
+ fn borrow(&self) -> &T {
+ match self {
+ ArcCow::Borrowed(borrowed) => borrowed,
+ ArcCow::Owned(owned) => owned.as_ref(),
+ }
+ }
+}
+
impl<T: ?Sized> std::ops::Deref for ArcCow<'_, T> {
type Target = T;
@@ -2,7 +2,7 @@ pub use anyhow::{anyhow, Result};
use futures::future::BoxFuture;
use isahc::config::{Configurable, RedirectPolicy};
pub use isahc::{
- http::{Method, Uri},
+ http::{Method, StatusCode, Uri},
Error,
};
pub use isahc::{AsyncBody, Request, Response};
@@ -1,3 +1,4 @@
+pub mod arc_cow;
pub mod channel;
pub mod fs;
pub mod github;
@@ -246,9 +247,16 @@ where
}
}
-struct Defer<F: FnOnce()>(Option<F>);
+pub struct Deferred<F: FnOnce()>(Option<F>);
-impl<F: FnOnce()> Drop for Defer<F> {
+impl<F: FnOnce()> Deferred<F> {
+ /// Drop without running the deferred function.
+ pub fn cancel(mut self) {
+ self.0.take();
+ }
+}
+
+impl<F: FnOnce()> Drop for Deferred<F> {
fn drop(&mut self) {
if let Some(f) = self.0.take() {
f()
@@ -256,8 +264,9 @@ impl<F: FnOnce()> Drop for Defer<F> {
}
}
-pub fn defer<F: FnOnce()>(f: F) -> impl Drop {
- Defer(Some(f))
+/// Run the given function when the returned value is dropped (unless it's cancelled).
+pub fn defer<F: FnOnce()>(f: F) -> Deferred<F> {
+ Deferred(Some(f))
}
pub struct RandomCharIter<T: Rng>(T);