Checkpoint: render SVGs

Antonio Scandurra created

Change summary

crates/gpui3/src/app.rs                         |  18 ++-
crates/gpui3/src/assets.rs                      |   4 
crates/gpui3/src/elements/svg.rs                |  30 +---
crates/gpui3/src/gpui3.rs                       |   4 
crates/gpui3/src/platform.rs                    |  12 +
crates/gpui3/src/platform/mac/metal_renderer.rs |  12 +-
crates/gpui3/src/platform/mac/window.rs         |   4 
crates/gpui3/src/svg_library.rs                 | 102 -------------------
crates/gpui3/src/svg_renderer.rs                |  47 ++++++++
crates/gpui3/src/window.rs                      |  59 +++++++++-
crates/storybook2/src/assets.rs                 |  30 +++++
crates/storybook2/src/storybook2.rs             |  30 +----
crates/util/src/arc_cow.rs                      |  10 +
13 files changed, 187 insertions(+), 175 deletions(-)

Detailed changes

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, Context, LayoutId, MainThread, MainThreadOnly,
-    Platform, PlatformDispatcher, RootView, TextStyle, TextStyleRefinement, TextSystem, Window,
-    WindowContext, WindowHandle, WindowId,
+    current_platform, 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};
@@ -29,16 +29,18 @@ use util::ResultExt;
 pub struct App(Arc<Mutex<MainThread<AppContext>>>);
 
 impl App {
-    pub fn production() -> Self {
-        Self::new(current_platform())
+    pub fn production(asset_source: Arc<dyn AssetSource>) -> Self {
+        Self::new(current_platform(), asset_source)
     }
 
     #[cfg(any(test, feature = "test"))]
     pub fn test() -> Self {
-        Self::new(Arc::new(super::TestPlatform::new()))
+        let platform = Arc::new(super::TestPlatform::new());
+        let asset_source = Arc::new(());
+        Self::new(platform, asset_source)
     }
 
-    fn new(platform: Arc<dyn Platform>) -> Self {
+    fn new(platform: Arc<dyn Platform>, asset_source: Arc<dyn AssetSource>) -> Self {
         let dispatcher = platform.dispatcher();
         let text_system = Arc::new(TextSystem::new(platform.text_system()));
         let entities = EntityMap::new();
@@ -49,6 +51,7 @@ impl App {
                 platform: MainThreadOnly::new(platform, dispatcher.clone()),
                 dispatcher,
                 text_system,
+                svg_renderer: SvgRenderer::new(asset_source),
                 pending_updates: 0,
                 text_style_stack: Vec::new(),
                 state_stacks_by_type: HashMap::default(),
@@ -83,6 +86,7 @@ pub struct AppContext {
     dispatcher: Arc<dyn PlatformDispatcher>,
     text_system: Arc<TextSystem>,
     pending_updates: usize,
+    pub(crate) svg_renderer: SvgRenderer,
     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 🔗

@@ -44,6 +44,10 @@ 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/svg.rs 🔗

@@ -1,9 +1,9 @@
-use crate::{Element, Layout, LayoutId, Result, Style, StyleHelpers, Styled};
+use crate::{Element, Layout, LayoutId, Result, SharedString, Style, StyleHelpers, Styled};
 use refineable::RefinementCascade;
-use std::{borrow::Cow, marker::PhantomData};
+use std::marker::PhantomData;
 
 pub struct Svg<S> {
-    path: Option<Cow<'static, str>>,
+    path: Option<SharedString>,
     style: RefinementCascade<Style>,
     state_type: PhantomData<S>,
 }
@@ -17,7 +17,7 @@ pub fn svg<S>() -> Svg<S> {
 }
 
 impl<S> Svg<S> {
-    pub fn path(mut self, path: impl Into<Cow<'static, str>>) -> Self {
+    pub fn path(mut self, path: impl Into<SharedString>) -> Self {
         self.path = Some(path.into());
         self
     }
@@ -41,28 +41,18 @@ impl<S: 'static> Element for Svg<S> {
 
     fn paint(
         &mut self,
-        _layout: Layout,
+        layout: Layout,
         _: &mut Self::State,
         _: &mut Self::FrameState,
-        _cx: &mut crate::ViewContext<S>,
+        cx: &mut crate::ViewContext<S>,
     ) -> Result<()>
     where
         Self: Sized,
     {
-        // todo!
-        // let fill_color = self.computed_style().fill.and_then(|fill| fill.color());
-        // if let Some((path, fill_color)) = self.path.as_ref().zip(fill_color) {
-        //     if let Some(svg_tree) = cx.asset_cache.svg(path).log_err() {
-        //         let icon = scene::Icon {
-        //             bounds: layout.bounds + parent_origin,
-        //             svg: svg_tree,
-        //             path: path.clone(),
-        //             color: Rgba::from(fill_color).into(),
-        //         };
-
-        //         cx.scene().push_icon(icon);
-        //     }
-        // }
+        let fill_color = self.computed_style().fill.and_then(|fill| fill.color());
+        if let Some((path, fill_color)) = self.path.as_ref().zip(fill_color) {
+            cx.paint_svg(layout.bounds, layout.order, path.clone(), fill_color)?;
+        }
         Ok(())
     }
 }

crates/gpui3/src/gpui3.rs 🔗

@@ -10,7 +10,7 @@ mod scene;
 mod style;
 mod style_helpers;
 mod styled;
-mod svg_library;
+mod svg_renderer;
 mod taffy;
 mod text_system;
 mod util;
@@ -26,7 +26,7 @@ pub use elements::*;
 pub use executor::*;
 pub use geometry::*;
 pub use gpui3_macros::*;
-pub use svg_library::*;
+pub use svg_renderer::*;
 
 pub use platform::*;
 pub use refineable::*;

crates/gpui3/src/platform.rs 🔗

@@ -7,7 +7,7 @@ mod test;
 
 use crate::{
     AnyWindowHandle, Bounds, DevicePixels, Font, FontId, FontMetrics, GlyphId, Pixels, Point,
-    RenderGlyphParams, Result, Scene, ShapedLine, SharedString, Size,
+    RenderGlyphParams, RenderSvgParams, Result, Scene, ShapedLine, SharedString, Size,
 };
 use anyhow::anyhow;
 use async_task::Runnable;
@@ -147,7 +147,7 @@ pub trait PlatformWindow {
     fn is_topmost_for_position(&self, position: Point<Pixels>) -> bool;
     fn draw(&self, scene: Scene);
 
-    fn glyph_atlas(&self) -> Arc<dyn PlatformAtlas>;
+    fn monochrome_sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
 }
 
 pub trait PlatformDispatcher: Send + Sync {
@@ -178,7 +178,7 @@ pub trait PlatformTextSystem: Send + Sync {
 #[derive(PartialEq, Eq, Hash, Clone)]
 pub enum AtlasKey {
     Glyph(RenderGlyphParams),
-    // Svg(RenderSvgParams),
+    Svg(RenderSvgParams),
 }
 
 impl From<RenderGlyphParams> for AtlasKey {
@@ -187,6 +187,12 @@ impl From<RenderGlyphParams> for AtlasKey {
     }
 }
 
+impl From<RenderSvgParams> for AtlasKey {
+    fn from(params: RenderSvgParams) -> Self {
+        Self::Svg(params)
+    }
+}
+
 pub trait PlatformAtlas: Send + Sync {
     fn get_or_insert_with(
         &self,

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

@@ -20,7 +20,7 @@ pub struct MetalRenderer {
     sprites_pipeline_state: metal::RenderPipelineState,
     unit_vertices: metal::Buffer,
     instances: metal::Buffer,
-    glyph_atlas: Arc<MetalAtlas>,
+    monochrome_sprite_atlas: Arc<MetalAtlas>,
 }
 
 impl MetalRenderer {
@@ -99,7 +99,7 @@ impl MetalRenderer {
         );
 
         let command_queue = device.new_command_queue();
-        let glyph_atlas = Arc::new(MetalAtlas::new(
+        let monochrome_sprite_atlas = Arc::new(MetalAtlas::new(
             Size {
                 width: DevicePixels(1024),
                 height: DevicePixels(768),
@@ -115,7 +115,7 @@ impl MetalRenderer {
             sprites_pipeline_state,
             unit_vertices,
             instances,
-            glyph_atlas,
+            monochrome_sprite_atlas,
         }
     }
 
@@ -123,8 +123,8 @@ impl MetalRenderer {
         &*self.layer
     }
 
-    pub fn glyph_atlas(&self) -> &Arc<MetalAtlas> {
-        &self.glyph_atlas
+    pub fn monochrome_sprite_atlas(&self) -> &Arc<MetalAtlas> {
+        &self.monochrome_sprite_atlas
     }
 
     pub fn draw(&mut self, scene: &mut Scene) {
@@ -277,7 +277,7 @@ impl MetalRenderer {
         }
         align_offset(offset);
 
-        let texture = self.glyph_atlas.texture(texture_id);
+        let texture = self.monochrome_sprite_atlas.texture(texture_id);
         let texture_size = size(
             DevicePixels(texture.width() as i32),
             DevicePixels(texture.height() as i32),

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

@@ -886,8 +886,8 @@ impl PlatformWindow for MacWindow {
         }
     }
 
-    fn glyph_atlas(&self) -> Arc<dyn PlatformAtlas> {
-        self.0.lock().renderer.glyph_atlas().clone()
+    fn monochrome_sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
+        self.0.lock().renderer.monochrome_sprite_atlas().clone()
     }
 }
 

crates/gpui3/src/svg_library.rs 🔗

@@ -1,102 +0,0 @@
-use crate::{AssetSource, DevicePixels, ImageData, IsZero, Result, SharedString, Size};
-use anyhow::anyhow;
-use collections::HashMap;
-use parking_lot::{RwLock, RwLockUpgradableReadGuard};
-use std::hash::Hash;
-use std::sync::Arc;
-use usvg::Tree as SvgTree;
-
-#[derive(Clone, PartialEq, Hash, Eq)]
-pub struct SvgRenderParams {
-    path: SharedString,
-    size: Size<DevicePixels>,
-}
-
-pub struct SvgRenderer {
-    asset_source: Arc<dyn AssetSource>,
-    trees_by_path: RwLock<HashMap<SharedString, SvgTree>>,
-    rendered: RwLock<HashMap<SvgRenderParams, Arc<ImageData>>>,
-}
-
-impl SvgRenderer {
-    pub fn render(&self, params: SvgRenderParams) -> Result<Arc<ImageData>> {
-        if params.size.is_zero() {
-            return Err(anyhow!("can't render at a zero size"));
-        }
-
-        let rendered = self.rendered.upgradable_read();
-        if let Some(image_data) = rendered.get(&params) {
-            Ok(image_data.clone())
-        } else {
-            // There's no rendered SVG for the path at the requested size.
-            // Have we already loaded a tree for the path?
-            let trees_by_path = self.trees_by_path.upgradable_read();
-            let tree = if let Some(tree) = trees_by_path.get(&params.path) {
-                tree.clone()
-            } else {
-                // Load the tree
-                let bytes = self.asset_source.load(&params.path)?;
-                let tree = usvg::Tree::from_data(&bytes, &usvg::Options::default())?;
-                let mut trees_by_path = RwLockUpgradableReadGuard::upgrade(trees_by_path);
-                trees_by_path.insert(params.path.clone(), tree.clone());
-                tree
-            };
-
-            // Render the SVG to a pixmap with the specified width and height.
-            // Convert the pixmap's pixels into an image data and cache it in `rendered`.
-            let mut pixmap =
-                tiny_skia::Pixmap::new(params.size.width.into(), params.size.height.into())
-                    .unwrap();
-            resvg::render(
-                &tree,
-                usvg::FitTo::Width(params.size.width.into()),
-                pixmap.as_mut(),
-            );
-            let alpha_mask = pixmap
-                .pixels()
-                .iter()
-                .map(|p| p.alpha())
-                .collect::<Vec<_>>();
-            let mut rendered = RwLockUpgradableReadGuard::upgrade(rendered);
-            let image_data = Arc::new(ImageData::from_raw(params.size, alpha_mask));
-            rendered.insert(params, image_data.clone());
-
-            Ok(image_data)
-        }
-    }
-}
-
-// impl SvgRenderer {
-//     pub fn render_svg(
-//         &mut self,
-//         size: Vector2I,
-//         path: Cow<'static, str>,
-//         svg: usvg::Tree,
-//     ) -> Option<IconSprite> {
-//         let mut pixmap = tiny_skia::Pixmap::new(size.x() as u32, size.y() as u32)?;
-//         resvg::render(&svg, usvg::FitTo::Width(size.x() as u32), pixmap.as_mut());
-
-//         let atlases = &mut self.atlases;
-//         match self.icons.entry(IconDescriptor {
-//             path,
-//             width: size.x(),
-//             height: size.y(),
-//         }) {
-//             Entry::Occupied(entry) => Some(entry.get().clone()),
-//             Entry::Vacant(entry) => {
-//                 let mask = pixmap
-//                     .pixels()
-//                     .iter()
-//                     .map(|a| a.alpha())
-//                     .collect::<Vec<_>>();
-//                 let (alloc_id, atlas_bounds) = atlases.upload(size, &mask)?;
-//                 let icon_sprite = IconSprite {
-//                     atlas_id: alloc_id.atlas_id,
-//                     atlas_origin: atlas_bounds.origin(),
-//                     size,
-//                 };
-//                 Some(entry.insert(icon_sprite).clone())
-//             }
-//         }
-//     }
-// }

crates/gpui3/src/svg_renderer.rs 🔗

@@ -0,0 +1,47 @@
+use crate::{AssetSource, DevicePixels, IsZero, Result, SharedString, Size};
+use anyhow::anyhow;
+use std::hash::Hash;
+use std::sync::Arc;
+
+#[derive(Clone, PartialEq, Hash, Eq)]
+pub struct RenderSvgParams {
+    pub(crate) path: SharedString,
+    pub(crate) size: Size<DevicePixels>,
+}
+
+pub struct SvgRenderer {
+    asset_source: Arc<dyn AssetSource>,
+}
+
+impl SvgRenderer {
+    pub fn new(asset_source: Arc<dyn AssetSource>) -> Self {
+        Self { asset_source }
+    }
+
+    pub fn render(&self, params: &RenderSvgParams) -> Result<Vec<u8>> {
+        if params.size.is_zero() {
+            return Err(anyhow!("can't render at a zero size"));
+        }
+
+        // Load the tree.
+        let bytes = self.asset_source.load(&params.path)?;
+        let tree = usvg::Tree::from_data(&bytes, &usvg::Options::default())?;
+
+        // Render the SVG to a pixmap with the specified width and height.
+        let mut pixmap =
+            tiny_skia::Pixmap::new(params.size.width.into(), params.size.height.into()).unwrap();
+        resvg::render(
+            &tree,
+            usvg::FitTo::Width(params.size.width.into()),
+            pixmap.as_mut(),
+        );
+
+        // Convert the pixmap's pixels into an alpha mask.
+        let alpha_mask = pixmap
+            .pixels()
+            .iter()
+            .map(|p| p.alpha())
+            .collect::<Vec<_>>();
+        Ok(alpha_mask)
+    }
+}

crates/gpui3/src/window.rs 🔗

@@ -1,9 +1,9 @@
 use crate::{
-    px, AnyView, AppContext, AvailableSpace, BorrowAppContext, Bounds, Context, Corners, Effect,
-    Element, EntityId, FontId, GlyphId, Handle, Hsla, IsZero, LayerId, LayoutId, MainThread,
-    MainThreadOnly, MonochromeSprite, Pixels, PlatformAtlas, PlatformWindow, Point, Reference,
-    RenderGlyphParams, ScaledPixels, Scene, Size, Style, TaffyLayoutEngine, WeakHandle,
-    WindowOptions, SUBPIXEL_VARIANTS,
+    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, Reference, RenderGlyphParams, RenderSvgParams, ScaledPixels, Scene, SharedString, Size,
+    Style, TaffyLayoutEngine, WeakHandle, WindowOptions, SUBPIXEL_VARIANTS,
 };
 use anyhow::Result;
 use futures::Future;
@@ -16,7 +16,7 @@ pub struct AnyWindow {}
 pub struct Window {
     handle: AnyWindowHandle,
     platform_window: MainThreadOnly<Box<dyn PlatformWindow>>,
-    glyph_atlas: Arc<dyn PlatformAtlas>,
+    monochrome_sprite_atlas: Arc<dyn PlatformAtlas>,
     rem_size: Pixels,
     content_size: Size<Pixels>,
     layout_engine: TaffyLayoutEngine,
@@ -35,7 +35,7 @@ impl Window {
         cx: &mut MainThread<AppContext>,
     ) -> Self {
         let platform_window = cx.platform().open_window(handle, options);
-        let glyph_atlas = platform_window.glyph_atlas();
+        let monochrome_sprite_atlas = platform_window.monochrome_sprite_atlas();
         let mouse_position = platform_window.mouse_position();
         let content_size = platform_window.content_size();
         let scale_factor = platform_window.scale_factor();
@@ -58,7 +58,7 @@ impl Window {
         Window {
             handle,
             platform_window,
-            glyph_atlas,
+            monochrome_sprite_atlas,
             rem_size: px(16.),
             content_size,
             layout_engine: TaffyLayoutEngine::new(),
@@ -235,7 +235,7 @@ impl<'a, 'w> WindowContext<'a, 'w> {
             let layer_id = self.current_layer_id();
             let tile = self
                 .window
-                .glyph_atlas
+                .monochrome_sprite_atlas
                 .get_or_insert_with(&params.clone().into(), &mut || {
                     self.text_system().rasterize_glyph(&params)
                 })?;
@@ -259,6 +259,47 @@ impl<'a, 'w> WindowContext<'a, 'w> {
         Ok(())
     }
 
+    pub fn paint_svg(
+        &mut self,
+        bounds: Bounds<Pixels>,
+        order: u32,
+        path: SharedString,
+        color: Hsla,
+    ) -> Result<()> {
+        let scale_factor = self.scale_factor();
+        let bounds = bounds.scale(scale_factor);
+        // Render the SVG at twice the size to get a higher quality result.
+        let params = RenderSvgParams {
+            path,
+            size: bounds
+                .size
+                .map(|pixels| DevicePixels::from((pixels.0 * 2.).ceil() as i32)),
+        };
+
+        let layer_id = self.current_layer_id();
+        let tile = self.window.monochrome_sprite_atlas.get_or_insert_with(
+            &params.clone().into(),
+            &mut || {
+                let bytes = self.svg_renderer.render(&params)?;
+                Ok((params.size, bytes))
+            },
+        )?;
+        let content_mask = self.content_mask().scale(scale_factor);
+
+        self.window.scene.insert(
+            layer_id,
+            MonochromeSprite {
+                order,
+                bounds,
+                content_mask,
+                color,
+                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/assets.rs 🔗

@@ -0,0 +1,30 @@
+use std::borrow::Cow;
+
+use anyhow::{anyhow, Result};
+use gpui3::{AssetSource, SharedString};
+use rust_embed::RustEmbed;
+
+#[derive(RustEmbed)]
+#[folder = "../../assets"]
+#[include = "fonts/**/*"]
+#[include = "icons/**/*"]
+#[include = "themes/**/*"]
+#[include = "sounds/**/*"]
+#[include = "*.md"]
+#[exclude = "*.DS_Store"]
+pub struct Assets;
+
+impl AssetSource for Assets {
+    fn load(&self, path: &SharedString) -> Result<Cow<[u8]>> {
+        Self::get(path.as_ref())
+            .map(|f| f.data)
+            .ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path))
+    }
+
+    fn list(&self, path: &SharedString) -> Result<Vec<SharedString>> {
+        Ok(Self::iter()
+            .filter(|p| p.starts_with(path.as_ref()))
+            .map(SharedString::from)
+            .collect())
+    }
+}

crates/storybook2/src/storybook2.rs 🔗

@@ -1,9 +1,13 @@
 #![allow(dead_code, unused_variables)]
 
+use assets::Assets;
 use gpui3::{px, size, Bounds, WindowBounds, WindowOptions};
 use log::LevelFilter;
 use simplelog::SimpleLogger;
+use std::sync::Arc;
+use workspace::workspace;
 
+mod assets;
 mod collab_panel;
 mod theme;
 mod themes;
@@ -19,7 +23,8 @@ fn main() {
 
     SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
 
-    gpui3::App::production().run(|cx| {
+    let asset_source = Arc::new(Assets);
+    gpui3::App::production(asset_source).run(|cx| {
         let window = cx.open_window(
             WindowOptions {
                 bounds: WindowBounds::Fixed(Bounds {
@@ -35,29 +40,6 @@ fn main() {
     });
 }
 
-use rust_embed::RustEmbed;
-use workspace::workspace;
-
-#[derive(RustEmbed)]
-#[folder = "../../assets"]
-#[include = "themes/**/*"]
-#[include = "fonts/**/*"]
-#[include = "icons/**/*"]
-#[exclude = "*.DS_Store"]
-pub struct Assets;
-
-// impl AssetSource for Assets {
-//     fn load(&self, path: &str) -> Result<std::borrow::Cow<[u8]>> {
-//         Self::get(path)
-//             .map(|f| f.data)
-//             .ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path))
-//     }
-
-//     fn list(&self, path: &str) -> Vec<std::borrow::Cow<'static, str>> {
-//         Self::iter().filter(|p| p.starts_with(path)).collect()
-//     }
-// }
-
 // fn load_embedded_fonts(platform: &dyn gpui2::Platform) {
 //     let font_paths = Assets.list("fonts");
 //     let mut embedded_fonts = Vec::new();

crates/util/src/arc_cow.rs 🔗

@@ -1,4 +1,5 @@
 use std::{
+    borrow::Cow,
     fmt::{self, Debug},
     sync::Arc,
 };
@@ -47,6 +48,15 @@ impl From<String> for ArcCow<'_, str> {
     }
 }
 
+impl<'a> From<Cow<'a, str>> for ArcCow<'a, str> {
+    fn from(value: Cow<'a, str>) -> Self {
+        match value {
+            Cow::Borrowed(borrowed) => Self::Borrowed(borrowed),
+            Cow::Owned(owned) => Self::Owned(owned.into()),
+        }
+    }
+}
+
 impl<'a, T: ?Sized + ToOwned> std::borrow::Borrow<T> for ArcCow<'a, T> {
     fn borrow(&self) -> &T {
         match self {