Cargo.lock 🔗
@@ -3177,6 +3177,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"derive_more",
+ "futures 0.3.28",
"gpui",
"gpui2_macros",
"log",
Nathan Sobo created
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(-)
@@ -3177,6 +3177,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"derive_more",
+ "futures 0.3.28",
"gpui",
"gpui2_macros",
"log",
@@ -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
}
}
@@ -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
@@ -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;
@@ -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 {}
@@ -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),
@@ -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 {