Checkpoint: start rendering images

Antonio Scandurra created

Change summary

crates/gpui/src/image_cache.rs               |  1 
crates/gpui3/src/app.rs                      | 25 ++++-
crates/gpui3/src/assets.rs                   | 17 ++-
crates/gpui3/src/elements/img.rs             | 63 ++++++--------
crates/gpui3/src/geometry.rs                 |  4 
crates/gpui3/src/gpui3.rs                    |  1 
crates/gpui3/src/image_cache.rs              | 99 ++++++++++++++++++++++
crates/gpui3/src/platform.rs                 | 14 ++
crates/gpui3/src/platform/mac/metal_atlas.rs |  6 
crates/gpui3/src/window.rs                   | 74 ++++++++++++----
crates/storybook2/src/collab_panel.rs        |  4 
11 files changed, 231 insertions(+), 77 deletions(-)

Detailed changes

crates/gpui/src/image_cache.rs 🔗

@@ -84,7 +84,6 @@ impl ImageCache {
                         let format = image::guess_format(&body)?;
                         let image =
                             image::load_from_memory_with_format(&body, format)?.into_bgra8();
-
                         Ok(ImageData::new(image))
                     }
                 }

crates/gpui3/src/app.rs 🔗

@@ -8,9 +8,9 @@ pub use model_context::*;
 use refineable::Refineable;
 
 use crate::{
-    current_platform, run_on_main, spawn_on_main, AssetSource, Context, LayoutId, MainThread,
-    MainThreadOnly, Platform, PlatformDispatcher, RootView, SvgRenderer, TextStyle,
-    TextStyleRefinement, TextSystem, Window, WindowContext, WindowHandle, WindowId,
+    current_platform, image_cache::ImageCache, run_on_main, spawn_on_main, AssetSource, Context,
+    LayoutId, MainThread, MainThreadOnly, Platform, PlatformDispatcher, RootView, SvgRenderer,
+    TextStyle, TextStyleRefinement, TextSystem, Window, WindowContext, WindowHandle, WindowId,
 };
 use anyhow::{anyhow, Result};
 use collections::{HashMap, VecDeque};
@@ -23,24 +23,33 @@ use std::{
     mem,
     sync::{Arc, Weak},
 };
-use util::ResultExt;
+use util::{
+    http::{self, HttpClient},
+    ResultExt,
+};
 
 #[derive(Clone)]
 pub struct App(Arc<Mutex<MainThread<AppContext>>>);
 
 impl App {
     pub fn production(asset_source: Arc<dyn AssetSource>) -> Self {
-        Self::new(current_platform(), asset_source)
+        let http_client = http::client();
+        Self::new(current_platform(), asset_source, http_client)
     }
 
     #[cfg(any(test, feature = "test"))]
     pub fn test() -> Self {
         let platform = Arc::new(super::TestPlatform::new());
         let asset_source = Arc::new(());
-        Self::new(platform, asset_source)
+        let http_client = util::http::FakeHttpClient::with_404_response();
+        Self::new(platform, asset_source, http_client)
     }
 
-    fn new(platform: Arc<dyn Platform>, asset_source: Arc<dyn AssetSource>) -> Self {
+    fn new(
+        platform: Arc<dyn Platform>,
+        asset_source: Arc<dyn AssetSource>,
+        http_client: Arc<dyn HttpClient>,
+    ) -> Self {
         let dispatcher = platform.dispatcher();
         let text_system = Arc::new(TextSystem::new(platform.text_system()));
         let entities = EntityMap::new();
@@ -52,6 +61,7 @@ impl App {
                 dispatcher,
                 text_system,
                 svg_renderer: SvgRenderer::new(asset_source),
+                image_cache: ImageCache::new(http_client),
                 pending_updates: 0,
                 text_style_stack: Vec::new(),
                 state_stacks_by_type: HashMap::default(),
@@ -87,6 +97,7 @@ pub struct AppContext {
     text_system: Arc<TextSystem>,
     pending_updates: usize,
     pub(crate) svg_renderer: SvgRenderer,
+    pub(crate) image_cache: ImageCache,
     pub(crate) text_style_stack: Vec<TextStyleRefinement>,
     pub(crate) state_stacks_by_type: HashMap<TypeId, Vec<Box<dyn Any + Send + Sync>>>,
     pub(crate) unit_entity: Handle<()>,

crates/gpui3/src/assets.rs 🔗

@@ -3,7 +3,9 @@ use anyhow::anyhow;
 use image::{Bgra, ImageBuffer};
 use std::{
     borrow::Cow,
+    cmp::Ordering,
     fmt,
+    hash::{Hash, Hasher},
     sync::atomic::{AtomicUsize, Ordering::SeqCst},
 };
 
@@ -25,18 +27,21 @@ impl AssetSource for () {
     }
 }
 
+#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
+pub struct ImageId(usize);
+
 pub struct ImageData {
-    pub id: usize,
+    pub id: ImageId,
     data: ImageBuffer<Bgra<u8>, Vec<u8>>,
 }
 
 impl ImageData {
-    pub fn from_raw(size: Size<DevicePixels>, bytes: Vec<u8>) -> Self {
+    pub fn new(data: ImageBuffer<Bgra<u8>, Vec<u8>>) -> Self {
         static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
 
         Self {
-            id: NEXT_ID.fetch_add(1, SeqCst),
-            data: ImageBuffer::from_raw(size.width.into(), size.height.into(), bytes).unwrap(),
+            id: ImageId(NEXT_ID.fetch_add(1, SeqCst)),
+            data,
         }
     }
 
@@ -44,10 +49,6 @@ impl ImageData {
         &self.data
     }
 
-    pub fn into_bytes(self) -> Vec<u8> {
-        self.data.into_raw()
-    }
-
     pub fn size(&self) -> Size<DevicePixels> {
         let (width, height) = self.data.dimensions();
         size(width.into(), height.into())

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

@@ -1,11 +1,14 @@
-use crate::{Element, Layout, LayoutId, Result, Style, StyleHelpers, Styled};
+use crate::{
+    Element, Layout, LayoutId, Result, SharedString, Style, StyleHelpers, Styled, ViewContext,
+};
+use futures::FutureExt;
 use refineable::RefinementCascade;
 use std::marker::PhantomData;
-use util::arc_cow::ArcCow;
+use util::ResultExt;
 
 pub struct Img<S> {
     style: RefinementCascade<Style>,
-    uri: Option<ArcCow<'static, str>>,
+    uri: Option<SharedString>,
     state_type: PhantomData<S>,
 }
 
@@ -18,7 +21,7 @@ pub fn img<S>() -> Img<S> {
 }
 
 impl<S> Img<S> {
-    pub fn uri(mut self, uri: impl Into<ArcCow<'static, str>>) -> Self {
+    pub fn uri(mut self, uri: impl Into<SharedString>) -> Self {
         self.uri = Some(uri.into());
         self
     }
@@ -31,7 +34,7 @@ impl<S: 'static> Element for Img<S> {
     fn layout(
         &mut self,
         _: &mut Self::State,
-        cx: &mut crate::ViewContext<Self::State>,
+        cx: &mut ViewContext<Self::State>,
     ) -> anyhow::Result<(LayoutId, Self::FrameState)>
     where
         Self: Sized,
@@ -46,7 +49,7 @@ impl<S: 'static> Element for Img<S> {
         layout: Layout,
         _: &mut Self::State,
         _: &mut Self::FrameState,
-        cx: &mut crate::ViewContext<Self::State>,
+        cx: &mut ViewContext<Self::State>,
     ) -> Result<()> {
         let style = self.computed_style();
         let order = layout.order;
@@ -54,36 +57,24 @@ impl<S: 'static> Element for Img<S> {
 
         style.paint(order, 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,
-        //             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(bounds.size(), 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();
-        //     }
-        // }
+        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)
+            {
+                cx.paint_image(bounds, order, data, false)?;
+            } else {
+                log::warn!("image not loaded yet");
+                // cx.spawn(|this, mut cx| async move {
+                //     if image_future.await.log_err().is_some() {
+                //         this.update(&mut cx, |_, cx| cx.notify()).ok();
+                //     }
+                // })
+                // .detach();
+            }
+        }
         Ok(())
     }
 }

crates/gpui3/src/geometry.rs 🔗

@@ -645,6 +645,10 @@ impl ScaledPixels {
     pub fn floor(&self) -> Self {
         Self(self.0.floor())
     }
+
+    pub fn ceil(&self) -> Self {
+        Self(self.0.ceil())
+    }
 }
 
 impl Eq for ScaledPixels {}

crates/gpui3/src/gpui3.rs 🔗

@@ -5,6 +5,7 @@ mod element;
 mod elements;
 mod executor;
 mod geometry;
+mod image_cache;
 mod platform;
 mod scene;
 mod style;

crates/gpui3/src/image_cache.rs 🔗

@@ -0,0 +1,99 @@
+use crate::{ImageData, ImageId, SharedString};
+use collections::HashMap;
+use futures::{
+    future::{BoxFuture, Shared},
+    AsyncReadExt, FutureExt,
+};
+use image::ImageError;
+use parking_lot::Mutex;
+use std::sync::Arc;
+use thiserror::Error;
+use util::http::{self, HttpClient};
+
+#[derive(PartialEq, Eq, Hash, Clone)]
+pub struct RenderImageParams {
+    pub(crate) image_id: ImageId,
+}
+
+#[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<SharedString, FetchImageFuture>>>,
+}
+
+type FetchImageFuture = 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: impl Into<SharedString>,
+    ) -> Shared<BoxFuture<'static, Result<Arc<ImageData>, Error>>> {
+        let uri = uri.into();
+        let mut images = self.images.lock();
+
+        match images.get(&uri) {
+            Some(future) => future.clone(),
+            None => {
+                let client = self.client.clone();
+                let future = {
+                    let uri = uri.clone();
+                    async move {
+                        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();
+                        Ok(Arc::new(ImageData::new(image)))
+                    }
+                }
+                .boxed()
+                .shared();
+
+                images.insert(uri, future.clone());
+                future
+            }
+        }
+    }
+}

crates/gpui3/src/platform.rs 🔗

@@ -5,6 +5,7 @@ mod mac;
 #[cfg(any(test, feature = "test"))]
 mod test;
 
+use crate::image_cache::RenderImageParams;
 use crate::{
     AnyWindowHandle, Bounds, DevicePixels, Font, FontId, FontMetrics, GlyphId, Pixels, Point,
     RenderGlyphParams, RenderSvgParams, Result, Scene, ShapedLine, SharedString, Size,
@@ -14,6 +15,7 @@ use async_task::Runnable;
 use futures::channel::oneshot;
 use seahash::SeaHasher;
 use serde::{Deserialize, Serialize};
+use std::borrow::Cow;
 use std::ffi::c_void;
 use std::hash::{Hash, Hasher};
 use std::{
@@ -179,6 +181,7 @@ pub trait PlatformTextSystem: Send + Sync {
 pub enum AtlasKey {
     Glyph(RenderGlyphParams),
     Svg(RenderSvgParams),
+    Image(RenderImageParams),
 }
 
 impl AtlasKey {
@@ -186,6 +189,7 @@ impl AtlasKey {
         match self {
             AtlasKey::Glyph(params) => !params.is_emoji,
             AtlasKey::Svg(_) => true,
+            AtlasKey::Image(_) => false,
         }
     }
 }
@@ -202,11 +206,17 @@ impl From<RenderSvgParams> for AtlasKey {
     }
 }
 
+impl From<RenderImageParams> for AtlasKey {
+    fn from(params: RenderImageParams) -> Self {
+        Self::Image(params)
+    }
+}
+
 pub trait PlatformAtlas: Send + Sync {
-    fn get_or_insert_with(
+    fn get_or_insert_with<'a>(
         &self,
         key: &AtlasKey,
-        build: &mut dyn FnMut() -> Result<(Size<DevicePixels>, Vec<u8>)>,
+        build: &mut dyn FnMut() -> Result<(Size<DevicePixels>, Cow<'a, [u8]>)>,
     ) -> Result<AtlasTile>;
 
     fn clear(&self);

crates/gpui3/src/platform/mac/metal_atlas.rs 🔗

@@ -1,3 +1,5 @@
+use std::borrow::Cow;
+
 use crate::{
     AtlasKey, AtlasTextureId, AtlasTile, Bounds, DevicePixels, PlatformAtlas, Point, Size,
 };
@@ -31,10 +33,10 @@ struct MetalAtlasState {
 }
 
 impl PlatformAtlas for MetalAtlas {
-    fn get_or_insert_with(
+    fn get_or_insert_with<'a>(
         &self,
         key: &AtlasKey,
-        build: &mut dyn FnMut() -> Result<(Size<DevicePixels>, Vec<u8>)>,
+        build: &mut dyn FnMut() -> Result<(Size<DevicePixels>, Cow<'a, [u8]>)>,
     ) -> Result<AtlasTile> {
         let mut lock = self.0.lock();
         if let Some(tile) = lock.tiles_by_key.get(key) {

crates/gpui3/src/window.rs 🔗

@@ -1,14 +1,15 @@
 use crate::{
-    px, AnyView, AppContext, AvailableSpace, BorrowAppContext, Bounds, Context, Corners,
-    DevicePixels, Effect, Element, EntityId, FontId, GlyphId, Handle, Hsla, IsZero, LayerId,
-    LayoutId, MainThread, MainThreadOnly, MonochromeSprite, Pixels, PlatformAtlas, PlatformWindow,
-    Point, PolychromeSprite, Reference, RenderGlyphParams, RenderSvgParams, ScaledPixels, Scene,
-    SharedString, Size, Style, TaffyLayoutEngine, WeakHandle, WindowOptions, SUBPIXEL_VARIANTS,
+    image_cache::RenderImageParams, px, AnyView, AppContext, AvailableSpace, BorrowAppContext,
+    Bounds, Context, Corners, DevicePixels, Effect, Element, EntityId, FontId, GlyphId, Handle,
+    Hsla, ImageData, IsZero, LayerId, LayoutId, MainThread, MainThreadOnly, MonochromeSprite,
+    Pixels, PlatformAtlas, PlatformWindow, Point, PolychromeSprite, Reference, RenderGlyphParams,
+    RenderSvgParams, ScaledPixels, Scene, SharedString, Size, Style, TaffyLayoutEngine, WeakHandle,
+    WindowOptions, SUBPIXEL_VARIANTS,
 };
 use anyhow::Result;
 use futures::Future;
 use smallvec::SmallVec;
-use std::{any::TypeId, marker::PhantomData, mem, sync::Arc};
+use std::{any::TypeId, borrow::Cow, marker::PhantomData, mem, sync::Arc};
 use util::ResultExt;
 
 pub struct AnyWindow {}
@@ -234,12 +235,13 @@ impl<'a, 'w> WindowContext<'a, 'w> {
         let raster_bounds = self.text_system().raster_bounds(&params)?;
         if !raster_bounds.is_zero() {
             let layer_id = self.current_layer_id();
-            let tile = self
-                .window
-                .sprite_atlas
-                .get_or_insert_with(&params.clone().into(), &mut || {
-                    self.text_system().rasterize_glyph(&params)
-                })?;
+            let tile =
+                self.window
+                    .sprite_atlas
+                    .get_or_insert_with(&params.clone().into(), &mut || {
+                        let (size, bytes) = self.text_system().rasterize_glyph(&params)?;
+                        Ok((size, Cow::Owned(bytes)))
+                    })?;
             let bounds = Bounds {
                 origin: glyph_origin.map(|px| px.floor()) + raster_bounds.origin.map(Into::into),
                 size: tile.bounds.size.map(Into::into),
@@ -283,12 +285,13 @@ impl<'a, 'w> WindowContext<'a, 'w> {
         let raster_bounds = self.text_system().raster_bounds(&params)?;
         if !raster_bounds.is_zero() {
             let layer_id = self.current_layer_id();
-            let tile = self
-                .window
-                .sprite_atlas
-                .get_or_insert_with(&params.clone().into(), &mut || {
-                    self.text_system().rasterize_glyph(&params)
-                })?;
+            let tile =
+                self.window
+                    .sprite_atlas
+                    .get_or_insert_with(&params.clone().into(), &mut || {
+                        let (size, bytes) = self.text_system().rasterize_glyph(&params)?;
+                        Ok((size, Cow::Owned(bytes)))
+                    })?;
             let bounds = Bounds {
                 origin: glyph_origin.map(|px| px.floor()) + raster_bounds.origin.map(Into::into),
                 size: tile.bounds.size.map(Into::into),
@@ -331,7 +334,7 @@ impl<'a, 'w> WindowContext<'a, 'w> {
                 .sprite_atlas
                 .get_or_insert_with(&params.clone().into(), &mut || {
                     let bytes = self.svg_renderer.render(&params)?;
-                    Ok((params.size, bytes))
+                    Ok((params.size, Cow::Owned(bytes)))
                 })?;
         let content_mask = self.content_mask().scale(scale_factor);
 
@@ -349,6 +352,39 @@ impl<'a, 'w> WindowContext<'a, 'w> {
         Ok(())
     }
 
+    pub fn paint_image(
+        &mut self,
+        bounds: Bounds<Pixels>,
+        order: u32,
+        data: Arc<ImageData>,
+        grayscale: bool,
+    ) -> Result<()> {
+        let scale_factor = self.scale_factor();
+        let bounds = bounds.scale(scale_factor);
+        let params = RenderImageParams { image_id: data.id };
+
+        let layer_id = self.current_layer_id();
+        let tile = self
+            .window
+            .sprite_atlas
+            .get_or_insert_with(&params.clone().into(), &mut || {
+                Ok((data.size(), Cow::Borrowed(data.as_bytes())))
+            })?;
+        let content_mask = self.content_mask().scale(scale_factor);
+
+        self.window.scene.insert(
+            layer_id,
+            PolychromeSprite {
+                order,
+                bounds,
+                content_mask,
+                tile,
+            },
+        );
+
+        Ok(())
+    }
+
     pub(crate) fn draw(&mut self) -> Result<()> {
         let unit_entity = self.unit_entity.clone();
         self.update_entity(&unit_entity, |_, cx| {

crates/storybook2/src/collab_panel.rs 🔗

@@ -1,7 +1,7 @@
 use crate::theme::{theme, Theme};
 use gpui3::{
     div, img, svg, view, AppContext, ArcCow, Context, Element, IntoAnyElement, ParentElement,
-    ScrollState, StyleHelpers, View, ViewContext, WindowContext,
+    ScrollState, SharedString, StyleHelpers, View, ViewContext, WindowContext,
 };
 
 pub struct CollabPanel {
@@ -144,7 +144,7 @@ impl CollabPanel {
 
     fn list_item(
         &self,
-        avatar_uri: impl Into<ArcCow<'static, str>>,
+        avatar_uri: impl Into<SharedString>,
         label: impl IntoAnyElement<Self>,
         theme: &Theme,
     ) -> impl Element<State = Self> {