Add support for fetching/rendering images

This commit is contained in:
Nathan Sobo 2023-09-06 17:13:38 -06:00
parent 6d4dd0e7a4
commit 99ad60460a
7 changed files with 161 additions and 32 deletions

1
Cargo.lock generated
View file

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

View file

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

View file

@ -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

View file

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

View file

@ -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 {}

View file

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

View file

@ -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 {