Add support for fetching/rendering images

Nathan Sobo created

Change summary

Cargo.lock                           |   1 
crates/gpui/src/image_cache.rs       |  32 ++------
crates/gpui2/Cargo.toml              |   1 
crates/gpui2/src/elements.rs         |   2 
crates/gpui2/src/elements/img.rs     | 105 ++++++++++++++++++++++++++++++
crates/storybook/src/collab_panel.rs |  39 ++++++++--
crates/util/src/arc_cow.rs           |  13 +++
7 files changed, 161 insertions(+), 32 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -3177,6 +3177,7 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "derive_more",
+ "futures 0.3.28",
  "gpui",
  "gpui2_macros",
  "log",

crates/gpui/src/image_cache.rs 🔗

@@ -11,7 +11,6 @@ use parking_lot::Mutex;
 use thiserror::Error;
 use util::{
     arc_cow::ArcCow,
-    defer,
     http::{self, HttpClient},
 };
 
@@ -44,16 +43,11 @@ impl From<ImageError> for Error {
 
 pub struct ImageCache {
     client: Arc<dyn HttpClient>,
-    images: Arc<
-        Mutex<
-            HashMap<
-                ArcCow<'static, str>,
-                Shared<BoxFuture<'static, Result<Arc<ImageData>, Error>>>,
-            >,
-        >,
-    >,
+    images: Arc<Mutex<HashMap<ArcCow<'static, str>, FetchImageFuture>>>,
 }
 
+type FetchImageFuture = Shared<BoxFuture<'static, Result<Arc<ImageData>, Error>>>;
+
 impl ImageCache {
     pub fn new(client: Arc<dyn HttpClient>) -> Self {
         ImageCache {
@@ -64,24 +58,18 @@ impl ImageCache {
 
     pub fn get(
         &self,
-        uri: ArcCow<'static, str>,
+        uri: impl Into<ArcCow<'static, str>>,
     ) -> Shared<BoxFuture<'static, Result<Arc<ImageData>, Error>>> {
-        match self.images.lock().get(uri.as_ref()) {
+        let uri = uri.into();
+        let mut images = self.images.lock();
+
+        match images.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?;
@@ -97,13 +85,13 @@ impl ImageCache {
                         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());
+
+                images.insert(uri, future.clone());
                 future
             }
         }

crates/gpui2/Cargo.toml 🔗

@@ -16,6 +16,7 @@ anyhow.workspace = true
 derive_more.workspace = true
 gpui = { path = "../gpui" }
 log.workspace = true
+futures.workspace = true
 gpui2_macros = { path = "../gpui2_macros" }
 parking_lot.workspace = true
 refineable.workspace = true

crates/gpui2/src/elements.rs 🔗

@@ -1,8 +1,10 @@
 pub mod div;
 pub mod hoverable;
+mod img;
 pub mod pressable;
 pub mod svg;
 pub mod text;
 
 pub use div::div;
+pub use img::img;
 pub use svg::svg;

crates/gpui2/src/elements/img.rs 🔗

@@ -0,0 +1,105 @@
+use crate as gpui2;
+use crate::style::{StyleHelpers, Styleable};
+use crate::{style::Style, Element};
+use futures::FutureExt;
+use gpui::scene;
+use gpui2_macros::IntoElement;
+use refineable::RefinementCascade;
+use util::arc_cow::ArcCow;
+use util::ResultExt;
+
+#[derive(IntoElement)]
+pub struct Img {
+    style: RefinementCascade<Style>,
+    uri: Option<ArcCow<'static, str>>,
+}
+
+pub fn img() -> Img {
+    Img {
+        style: RefinementCascade::default(),
+        uri: None,
+    }
+}
+
+impl Img {
+    pub fn uri(mut self, uri: impl Into<ArcCow<'static, str>>) -> Self {
+        self.uri = Some(uri.into());
+        self
+    }
+}
+
+impl<V: 'static> Element<V> for Img {
+    type PaintState = ();
+
+    fn layout(
+        &mut self,
+        _: &mut V,
+        cx: &mut crate::LayoutContext<V>,
+    ) -> anyhow::Result<(gpui::LayoutId, Self::PaintState)>
+    where
+        Self: Sized,
+    {
+        let style = self.computed_style();
+        let layout_id = cx.add_layout_node(style, [])?;
+        Ok((layout_id, ()))
+    }
+
+    fn paint(
+        &mut self,
+        _: &mut V,
+        layout: &gpui::Layout,
+        _: &mut Self::PaintState,
+        cx: &mut crate::paint_context::PaintContext<V>,
+    ) where
+        Self: Sized,
+    {
+        let style = self.computed_style();
+
+        style.paint_background(layout.bounds, cx);
+
+        if let Some(uri) = &self.uri {
+            let image_future = cx.image_cache.get(uri.clone());
+            if let Some(data) = image_future
+                .clone()
+                .now_or_never()
+                .and_then(ResultExt::log_err)
+            {
+                let rem_size = cx.rem_size();
+                cx.scene.push_image(scene::Image {
+                    bounds: layout.bounds,
+                    border: gpui::Border {
+                        color: style.border_color.unwrap_or_default().into(),
+                        top: style.border_widths.top.to_pixels(rem_size),
+                        right: style.border_widths.right.to_pixels(rem_size),
+                        bottom: style.border_widths.bottom.to_pixels(rem_size),
+                        left: style.border_widths.left.to_pixels(rem_size),
+                    },
+                    corner_radii: style.corner_radii.to_gpui(rem_size),
+                    grayscale: false,
+                    data,
+                })
+            } else {
+                cx.spawn(|this, mut cx| async move {
+                    if image_future.await.log_err().is_some() {
+                        this.update(&mut cx, |_, cx| cx.notify()).ok();
+                    }
+                })
+                .detach();
+            }
+        }
+    }
+}
+
+impl Styleable for Img {
+    type Style = Style;
+
+    fn style_cascade(&mut self) -> &mut RefinementCascade<Self::Style> {
+        &mut self.style
+    }
+
+    fn declared_style(&mut self) -> &mut <Self::Style as refineable::Refineable>::Refinement {
+        self.style.base()
+    }
+}
+
+impl StyleHelpers for Img {}

crates/storybook/src/collab_panel.rs 🔗

@@ -1,6 +1,6 @@
 use crate::theme::{theme, Theme};
 use gpui2::{
-    elements::{div, svg},
+    elements::{div, img, svg},
     style::{StyleHelpers, Styleable},
     ArcCow, Element, IntoElement, ParentElement, ViewContext,
 };
@@ -48,7 +48,11 @@ impl<V: 'static> CollabPanelElement<V> {
                             // List Section Header
                             .child(self.list_section_header("#CRDB", true, theme))
                             // List Item Large
-                            .child(self.list_item("maxbrunsfeld", theme)),
+                            .child(self.list_item(
+                                "http://github.com/maxbrunsfeld.png?s=50",
+                                "maxbrunsfeld",
+                                theme,
+                            )),
                     )
                     .child(
                         div()
@@ -63,9 +67,21 @@ impl<V: 'static> CollabPanelElement<V> {
                             .flex()
                             .flex_col()
                             .child(self.list_section_header("CONTACTS", true, theme))
-                            .child(self.list_item("as-cii", theme))
-                            .child(self.list_item("nathansobo", theme))
-                            .child(self.list_item("maxbrunsfeld", theme)),
+                            .child(self.list_item(
+                                "http://github.com/as-cii.png?s=50",
+                                "as-cii",
+                                theme,
+                            ))
+                            .child(self.list_item(
+                                "http://github.com/nathansobo.png?s=50",
+                                "nathansobo",
+                                theme,
+                            ))
+                            .child(self.list_item(
+                                "http://github.com/maxbrunsfeld.png?s=50",
+                                "maxbrunsfeld",
+                                theme,
+                            )),
                     ),
             )
             .child(
@@ -106,7 +122,12 @@ impl<V: 'static> CollabPanelElement<V> {
             )
     }
 
-    fn list_item(&self, label: impl Into<ArcCow<'static, str>>, theme: &Theme) -> impl Element<V> {
+    fn list_item(
+        &self,
+        avatar_uri: impl Into<ArcCow<'static, str>>,
+        label: impl Into<ArcCow<'static, str>>,
+        theme: &Theme,
+    ) -> impl Element<V> {
         div()
             .h_7()
             .px_2()
@@ -123,9 +144,9 @@ impl<V: 'static> CollabPanelElement<V> {
                     .gap_1()
                     .text_sm()
                     .child(
-                        div()
-                            .w_3p5()
-                            .h_3p5()
+                        img()
+                            .uri(avatar_uri)
+                            .size_3p5()
                             .fill(theme.middle.positive.default.foreground),
                     )
                     .child(label),

crates/util/src/arc_cow.rs 🔗

@@ -1,11 +1,22 @@
 use std::sync::Arc;
 
-#[derive(PartialEq, Eq, Hash)]
+#[derive(PartialEq, Eq)]
 pub enum ArcCow<'a, T: ?Sized> {
     Borrowed(&'a T),
     Owned(Arc<T>),
 }
 
+use std::hash::{Hash, Hasher};
+
+impl<'a, T: ?Sized + Hash> Hash for ArcCow<'a, T> {
+    fn hash<H: Hasher>(&self, state: &mut H) {
+        match self {
+            Self::Borrowed(borrowed) => Hash::hash(borrowed, state),
+            Self::Owned(owned) => Hash::hash(&**owned, state),
+        }
+    }
+}
+
 impl<'a, T: ?Sized> Clone for ArcCow<'a, T> {
     fn clone(&self) -> Self {
         match self {