diff --git a/Cargo.lock b/Cargo.lock index 1906aeb6ab97cd62f8606f350ea8e32bbaff2a66..cf14fcf989e0d75e805ae8db43f5d1b6eb4442a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3162,6 +3162,7 @@ dependencies = [ "sqlez", "sum_tree", "taffy", + "thiserror", "time 0.3.27", "tiny-skia", "usvg", diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index a77c9e2237bdc372f24c27fdec3dd991d51daef2..705feb351d170efdd845537a24bd9b6c219ef08e 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -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 = [] } diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index dda02fa4d6d3dc569e73ee09a333602d42993078..68b42f6e6e4ecd2fb870aaf87c93a20915323acc 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -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, font_system: Arc, pub font_cache: Arc, + pub image_cache: Arc, action_deserializers: HashMap<&'static str, (TypeId, DeserializeActionCallback)>, capture_actions: HashMap>>>, // Entity Types -> { Action Types -> Action Handlers } @@ -499,6 +506,7 @@ impl AppContext { platform: Arc, foreground_platform: Rc, font_cache: Arc, + http_client: Arc, 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(), diff --git a/crates/gpui/src/app/test_app_context.rs b/crates/gpui/src/app/test_app_context.rs index 6d593c2e7247d6d6005b32320b2b4dd235b4cac8..0dc1d1eba437fb67beddbde47bd10bc497ed928a 100644 --- a/crates/gpui/src/app/test_app_context.rs +++ b/crates/gpui/src/app/test_app_context.rs @@ -57,6 +57,7 @@ impl TestAppContext { platform, foreground_platform.clone(), font_cache, + util::http::FakeHttpClient::with_404_response(), RefCounts::new(leak_detector), (), ); diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index 74a50e9e64cae2e4f27c9adbdb7ada000e284f9d..0139f6e4e002e04b196063ee17553f733a164d7a 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -1,4 +1,5 @@ mod app; +mod image_cache; pub use app::*; mod assets; #[cfg(any(test, feature = "test-support"))] diff --git a/crates/gpui/src/image_cache.rs b/crates/gpui/src/image_cache.rs new file mode 100644 index 0000000000000000000000000000000000000000..77195c493faa17075e71e1b5625c25e51bd0ba2d --- /dev/null +++ b/crates/gpui/src/image_cache.rs @@ -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), + #[error("unexpected http status: {status}, body: {body}")] + BadStatus { + status: http::StatusCode, + body: String, + }, + #[error("image error: {0}")] + Image(Arc), +} + +impl From for Error { + fn from(error: std::io::Error) -> Self { + Error::Io(Arc::new(error)) + } +} + +impl From for Error { + fn from(error: ImageError) -> Self { + Error::Image(Arc::new(error)) + } +} + +pub struct ImageCache { + client: Arc, + images: Arc< + Mutex< + HashMap< + ArcCow<'static, str>, + Shared, Error>>>, + >, + >, + >, +} + +impl ImageCache { + pub fn new(client: Arc) -> Self { + ImageCache { + client, + images: Default::default(), + } + } + + pub fn get( + &self, + uri: ArcCow<'static, str>, + ) -> Shared, 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 + } + } + } +} diff --git a/crates/gpui2/src/elements/text.rs b/crates/gpui2/src/elements/text.rs index 545ca508730f46ce1ea5073b24a47dc2a440568f..4ef05e0e8e15069c5107ec3ec2b97ee770baebb7 100644 --- a/crates/gpui2/src/elements/text.rs +++ b/crates/gpui2/src/elements/text.rs @@ -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>> IntoElement for S { type Element = Text; diff --git a/crates/gpui2/src/gpui2.rs b/crates/gpui2/src/gpui2.rs index e9cddb65c872d1e955f1f53fd922d0d00eff0517..c92d22ba61772e06a65e0c67a8a117495a1ece08 100644 --- a/crates/gpui2/src/gpui2.rs +++ b/crates/gpui2/src/gpui2.rs @@ -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::*; diff --git a/crates/gpui2/src/arc_cow.rs b/crates/util/src/arc_cow.rs similarity index 80% rename from crates/gpui2/src/arc_cow.rs rename to crates/util/src/arc_cow.rs index 26c9870ee086db5dbf9a6868763401f603d82cce..c1176931667e1d69d3541b25fefe9aec0d55b0a6 100644 --- a/crates/gpui2/src/arc_cow.rs +++ b/crates/util/src/arc_cow.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +#[derive(PartialEq, Eq, Hash)] pub enum ArcCow<'a, T: ?Sized> { Borrowed(&'a T), Owned(Arc), @@ -32,6 +33,15 @@ impl From for ArcCow<'_, str> { } } +impl<'a, T: ?Sized + ToOwned> std::borrow::Borrow for ArcCow<'a, T> { + fn borrow(&self) -> &T { + match self { + ArcCow::Borrowed(borrowed) => borrowed, + ArcCow::Owned(owned) => owned.as_ref(), + } + } +} + impl std::ops::Deref for ArcCow<'_, T> { type Target = T; diff --git a/crates/util/src/http.rs b/crates/util/src/http.rs index e29768a53e891e165dd859fd27be5325f35cdfa5..329d84996d908259ea6fe2a40d0ec536d9292f97 100644 --- a/crates/util/src/http.rs +++ b/crates/util/src/http.rs @@ -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}; diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index c8beb86aeff44a75482531d22a6e8b0c07155fa6..858f6074339d9ee9991f9bce3aaa2fdba97bba2f 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -1,3 +1,4 @@ +pub mod arc_cow; pub mod channel; pub mod fs; pub mod github; @@ -246,9 +247,16 @@ where } } -struct Defer(Option); +pub struct Deferred(Option); -impl Drop for Defer { +impl Deferred { + /// Drop without running the deferred function. + pub fn cancel(mut self) { + self.0.take(); + } +} + +impl Drop for Deferred { fn drop(&mut self) { if let Some(f) = self.0.take() { f() @@ -256,8 +264,9 @@ impl Drop for Defer { } } -pub fn defer(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: F) -> Deferred { + Deferred(Some(f)) } pub struct RandomCharIter(T);