Checkpoint

Nathan Sobo created

Change summary

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(-)

Detailed changes

Cargo.lock 🔗

@@ -3162,6 +3162,7 @@ dependencies = [
  "sqlez",
  "sum_tree",
  "taffy",
+ "thiserror",
  "time 0.3.27",
  "tiny-skia",
  "usvg",

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 = [] }

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<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(),

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),
             (),
         );

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"))]

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<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
+            }
+        }
+    }
+}

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<V: 'static, S: Into<ArcCow<'static, str>>> IntoElement<V> for S {
     type Element = Text;

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::*;

crates/gpui2/src/arc_cow.rs → 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<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;
 

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};

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<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);