Implement SVG rendering

Antonio Scandurra created

Change summary

Cargo.lock                            | 314 ++++++++++++++++++++++++++++
gpui/Cargo.toml                       |   3 
gpui/src/assets.rs                    |  16 +
gpui/src/elements/svg.rs              | 101 +++++---
gpui/src/platform/mac/renderer.rs     |  29 ++
gpui/src/platform/mac/sprite_cache.rs |  64 +++++
gpui/src/scene.rs                     |  23 ++
zed/assets/icons/file-16.svg          |   1 
zed/src/file_finder.rs                |   3 
9 files changed, 505 insertions(+), 49 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1,5 +1,11 @@
 # This file is automatically @generated by Cargo.
 # It is not intended for manual editing.
+[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
 [[package]]
 name = "adler32"
 version = "1.2.0"
@@ -297,6 +303,12 @@ version = "3.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "63396b8a4b9de3f4fdfb320ab6080762242f66a8ef174c49d8e19b674db4cdbe"
 
+[[package]]
+name = "bytemuck"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bed57e2090563b83ba8f83366628ce535a7584c9afa4c9fc0612a03925c6df58"
+
 [[package]]
 name = "byteorder"
 version = "1.4.2"
@@ -545,6 +557,15 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "data-url"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d33fe99ccedd6e84bc035f1931bb2e6be79739d6242bd895e7311c886c50dc9c"
+dependencies = [
+ "matches",
+]
+
 [[package]]
 name = "deflate"
 version = "0.8.6"
@@ -671,6 +692,24 @@ dependencies = [
  "instant",
 ]
 
+[[package]]
+name = "flate2"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd3aec53de10fe96d7d8c565eb17f2c687bb5518a2ec453b5b1252964526abe0"
+dependencies = [
+ "cfg-if 1.0.0",
+ "crc32fast",
+ "libc",
+ "miniz_oxide 0.4.4",
+]
+
+[[package]]
+name = "float-cmp"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75224bec9bfe1a65e2d34132933f2de7fe79900c96a0174307554244ece8150e"
+
 [[package]]
 name = "float-ord"
 version = "0.2.0"
@@ -707,6 +746,17 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "fontdb"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "428948a0f39fb83fe55991d4423e35a793cdbb0322ebe23853f6024124a330d7"
+dependencies = [
+ "log",
+ "memmap2 0.1.0",
+ "ttf-parser 0.9.0",
+]
+
 [[package]]
 name = "foreign-types"
 version = "0.3.2"
@@ -879,10 +929,13 @@ dependencies = [
  "png",
  "rand 0.8.3",
  "replace_with",
+ "resvg",
  "simplelog",
  "smallvec",
  "smol",
+ "tiny-skia",
  "tree-sitter",
+ "usvg",
 ]
 
 [[package]]
@@ -933,6 +986,12 @@ version = "0.4.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
 
+[[package]]
+name = "jpeg-decoder"
+version = "0.1.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2"
+
 [[package]]
 name = "js-sys"
 version = "0.3.50"
@@ -942,6 +1001,15 @@ dependencies = [
  "wasm-bindgen",
 ]
 
+[[package]]
+name = "kurbo"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e30b1df631d23875f230ed3ddd1a88c231f269a04b2044eb6ca87e763b5f4c42"
+dependencies = [
+ "arrayvec",
+]
+
 [[package]]
 name = "kv-log-macro"
 version = "1.0.7"
@@ -1018,6 +1086,12 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "matches"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"
+
 [[package]]
 name = "maybe-uninit"
 version = "2.0.0"
@@ -1030,6 +1104,24 @@ version = "2.3.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525"
 
+[[package]]
+name = "memmap2"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b70ca2a6103ac8b665dc150b142ef0e4e89df640c9e6cf295d189c3caebe5a"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "memmap2"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "397d1a6d6d0563c0f5462bbdae662cf6c784edf5e828e40c7257f85d82bf56dd"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "metal"
 version = "0.21.0"
@@ -1053,6 +1145,16 @@ dependencies = [
  "adler32",
 ]
 
+[[package]]
+name = "miniz_oxide"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b"
+dependencies = [
+ "adler",
+ "autocfg",
+]
+
 [[package]]
 name = "nb-connect"
 version = "1.0.3"
@@ -1201,6 +1303,12 @@ version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099"
 
+[[package]]
+name = "pico-args"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d70072c20945e1ab871c472a285fc772aefd4f5407723c206242f2c6f94595d6"
+
 [[package]]
 name = "pin-project-lite"
 version = "0.2.4"
@@ -1228,7 +1336,7 @@ dependencies = [
  "bitflags",
  "crc32fast",
  "deflate",
- "miniz_oxide",
+ "miniz_oxide 0.3.7",
 ]
 
 [[package]]
@@ -1336,6 +1444,12 @@ dependencies = [
  "rand_core 0.6.2",
 ]
 
+[[package]]
+name = "rctree"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be9e29cb19c8fe84169fcb07f8f11e66bc9e6e0280efd4715c54818296f8a4a8"
+
 [[package]]
 name = "rdrand"
 version = "0.4.0"
@@ -1414,6 +1528,40 @@ version = "0.1.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e3a8614ee435691de62bcffcf4a66d91b3594bf1428a5722e79103249a095690"
 
+[[package]]
+name = "resvg"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac9efbe9c239253e11e518352c5f015ec0c69e73658eed153670e853e1b78e40"
+dependencies = [
+ "jpeg-decoder",
+ "log",
+ "pico-args",
+ "png",
+ "rgb",
+ "svgfilters",
+ "tiny-skia",
+ "usvg",
+]
+
+[[package]]
+name = "rgb"
+version = "0.8.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fddb3b23626145d1776addfc307e1a1851f60ef6ca64f376bcb889697144cf0"
+dependencies = [
+ "bytemuck",
+]
+
+[[package]]
+name = "roxmltree"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "921904a62e410e37e215c40381b7117f830d9d89ba60ab5236170541dd25646b"
+dependencies = [
+ "xmlparser",
+]
+
 [[package]]
 name = "rust-argon2"
 version = "0.8.3"
@@ -1474,12 +1622,37 @@ dependencies = [
  "semver",
 ]
 
+[[package]]
+name = "rustybuzz"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ab463a295d00f3692e0974a0bfd83c7a9bcd119e27e07c2beecdb1b44a09d10"
+dependencies = [
+ "bitflags",
+ "bytemuck",
+ "smallvec",
+ "ttf-parser 0.9.0",
+ "unicode-bidi-mirroring",
+ "unicode-ccc",
+ "unicode-general-category",
+ "unicode-script",
+]
+
 [[package]]
 name = "ryu"
 version = "1.0.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
 
+[[package]]
+name = "safe_arch"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1ff3d6d9696af502cc3110dacce942840fb06ff4514cad92236ecc455f2ce05"
+dependencies = [
+ "bytemuck",
+]
+
 [[package]]
 name = "same-file"
 version = "1.0.6"
@@ -1579,6 +1752,15 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "simplecss"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "596554e63596d556a0dbd681416342ca61c75f1a45203201e7e77d3fa2fa9014"
+dependencies = [
+ "log",
+]
+
 [[package]]
 name = "simplelog"
 version = "0.9.0"
@@ -1590,6 +1772,12 @@ dependencies = [
  "termcolor",
 ]
 
+[[package]]
+name = "siphasher"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac"
+
 [[package]]
 name = "slab"
 version = "0.4.2"
@@ -1643,6 +1831,26 @@ version = "0.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8fb1df15f412ee2e9dfc1c504260fa695c1c3f10fe9f4a6ee2d2184d7d6450e2"
 
+[[package]]
+name = "svgfilters"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb0dce2fee79ac40c21dafba48565ff7a5fa275e23ffe9ce047a40c9574ba34e"
+dependencies = [
+ "float-cmp",
+ "rgb",
+]
+
+[[package]]
+name = "svgtypes"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c536faaff1a10837cfe373142583f6e27d81e96beba339147e77b67c9f260ff"
+dependencies = [
+ "float-cmp",
+ "siphasher",
+]
+
 [[package]]
 name = "syn"
 version = "1.0.67"
@@ -1702,6 +1910,20 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "tiny-skia"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bf81f2900d2e235220e6f31ec9f63ade6a7f59090c556d74fe949bb3b15e9fe"
+dependencies = [
+ "arrayref",
+ "arrayvec",
+ "bytemuck",
+ "cfg-if 1.0.0",
+ "png",
+ "safe_arch",
+]
+
 [[package]]
 name = "tree-sitter"
 version = "0.17.1"
@@ -1712,6 +1934,57 @@ dependencies = [
  "regex",
 ]
 
+[[package]]
+name = "ttf-parser"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62ddb402ac6c2af6f7a2844243887631c4e94b51585b229fcfddb43958cd55ca"
+
+[[package]]
+name = "ttf-parser"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85e00391c1f3d171490a3f8bd79999b0002ae38d3da0d6a3a306c754b053d71b"
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5"
+dependencies = [
+ "matches",
+]
+
+[[package]]
+name = "unicode-bidi-mirroring"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56d12260fb92d52f9008be7e4bca09f584780eb2266dc8fecc6a192bec561694"
+
+[[package]]
+name = "unicode-ccc"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28ae07c514c335bbd0251147bb1de333e28ebc8f57d792014f919ed212d119f6"
+
+[[package]]
+name = "unicode-general-category"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f9af028e052a610d99e066b33304625dea9613170a2563314490a4e6ec5cf7f"
+
+[[package]]
+name = "unicode-script"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79bf4d5fc96546fdb73f9827097810bbda93b11a6770ff3a54e1f445d4135787"
+
+[[package]]
+name = "unicode-vo"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94"
+
 [[package]]
 name = "unicode-width"
 version = "0.1.8"
@@ -1730,6 +2003,33 @@ version = "0.1.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f14ee04d9415b52b3aeab06258a3f07093182b88ba0f9b8d203f211a7a7d41c7"
 
+[[package]]
+name = "usvg"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ffbeb91d06989028c9c5e44d14d78b0cacdec56a613bb146e7a70007b1b6163"
+dependencies = [
+ "base64",
+ "data-url",
+ "flate2",
+ "fontdb",
+ "kurbo",
+ "log",
+ "memmap2 0.2.2",
+ "pico-args",
+ "rctree",
+ "roxmltree",
+ "rustybuzz",
+ "simplecss",
+ "siphasher",
+ "svgtypes",
+ "ttf-parser 0.12.0",
+ "unicode-bidi",
+ "unicode-script",
+ "unicode-vo",
+ "xmlwriter",
+]
+
 [[package]]
 name = "value-bag"
 version = "1.0.0-alpha.6"
@@ -1920,6 +2220,18 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "xmlparser"
+version = "0.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "114ba2b24d2167ef6d67d7d04c8cc86522b87f490025f39f0303b7db5bf5e3d8"
+
+[[package]]
+name = "xmlwriter"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
+
 [[package]]
 name = "zed"
 version = "0.1.0"

gpui/Cargo.toml 🔗

@@ -17,9 +17,12 @@ pathfinder_color = "0.5"
 pathfinder_geometry = "0.5"
 rand = "0.8.3"
 replace_with = "0.1.7"
+resvg = "0.14"
 smallvec = "1.6.1"
 smol = "1.2"
+tiny-skia = "0.5"
 tree-sitter = "0.17"
+usvg = "0.14"
 
 [build-dependencies]
 bindgen = "0.57"

gpui/src/assets.rs 🔗

@@ -1,5 +1,5 @@
 use anyhow::{anyhow, Result};
-use std::borrow::Cow;
+use std::{borrow::Cow, cell::RefCell, collections::HashMap};
 
 pub trait AssetSource: 'static {
     fn load(&self, path: &str) -> Result<Cow<[u8]>>;
@@ -16,12 +16,26 @@ impl AssetSource for () {
 
 pub struct AssetCache {
     source: Box<dyn AssetSource>,
+    svgs: RefCell<HashMap<String, usvg::Tree>>,
 }
 
 impl AssetCache {
     pub fn new(source: impl AssetSource) -> Self {
         Self {
             source: Box::new(source),
+            svgs: RefCell::new(HashMap::new()),
+        }
+    }
+
+    pub fn svg(&self, path: &str) -> Result<usvg::Tree> {
+        let mut svgs = self.svgs.borrow_mut();
+        if let Some(svg) = svgs.get(path) {
+            Ok(svg.clone())
+        } else {
+            let bytes = self.source.load(path)?;
+            let svg = usvg::Tree::from_data(&bytes, &usvg::Options::default())?;
+            svgs.insert(path.to_string(), svg.clone());
+            Ok(svg)
         }
     }
 }

gpui/src/elements/svg.rs 🔗

@@ -1,73 +1,85 @@
 use crate::{
-    geometry::vector::Vector2F, AfterLayoutContext, Element, Event, EventContext, LayoutContext,
-    PaintContext, SizeConstraint,
+    color::ColorU,
+    geometry::{
+        rect::RectF,
+        vector::{vec2f, Vector2F},
+    },
+    scene, AfterLayoutContext, Element, Event, EventContext, LayoutContext, PaintContext,
+    SizeConstraint,
 };
 
 pub struct Svg {
     path: String,
+    color: ColorU,
 }
 
 impl Svg {
     pub fn new(path: String) -> Self {
-        Self { path }
+        Self {
+            path,
+            color: ColorU::black(),
+        }
+    }
+
+    pub fn with_color(mut self, color: ColorU) -> Self {
+        self.color = color;
+        self
     }
 }
 
 impl Element for Svg {
-    type LayoutState = ();
+    type LayoutState = Option<usvg::Tree>;
     type PaintState = ();
 
     fn layout(
         &mut self,
-        _: SizeConstraint,
-        _: &mut LayoutContext,
+        constraint: SizeConstraint,
+        ctx: &mut LayoutContext,
     ) -> (Vector2F, Self::LayoutState) {
-        // let size;
-        // match ctx.asset_cache.svg(&self.path) {
-        //     Ok(tree) => {
-        //         size = if constraint.max.x().is_infinite() && constraint.max.y().is_infinite() {
-        //             let rect = usvg_rect_to_euclid_rect(&tree.svg_node().view_box.rect);
-        //             rect.size()
-        //         } else {
-        //             let max_size = constraint.max;
-        //             let svg_size = usvg_rect_to_euclid_rect(&tree.svg_node().view_box.rect).size();
-
-        //             if max_size.x().is_infinite()
-        //                 || max_size.x() / max_size.y() > svg_size.x() / svg_size.y()
-        //             {
-        //                 vec2f(svg_size.x() * max_size.y() / svg_size.y(), max_size.y())
-        //             } else {
-        //                 vec2f(max_size.x(), svg_size.y() * max_size.x() / svg_size.x())
-        //             }
-        //         };
-        //         self.tree = Some(tree);
-        //     }
-        //     Err(error) => {
-        //         log::error!("{}", error);
-        //         size = constraint.min;
-        //     }
-        // };
-
-        // size
+        match ctx.asset_cache.svg(&self.path) {
+            Ok(tree) => {
+                let size = if constraint.max.x().is_infinite() && constraint.max.y().is_infinite() {
+                    let rect = from_usvg_rect(tree.svg_node().view_box.rect);
+                    rect.size()
+                } else {
+                    let max_size = constraint.max;
+                    let svg_size = from_usvg_rect(tree.svg_node().view_box.rect).size();
 
-        todo!()
+                    if max_size.x().is_infinite()
+                        || max_size.x() / max_size.y() > svg_size.x() / svg_size.y()
+                    {
+                        vec2f(svg_size.x() * max_size.y() / svg_size.y(), max_size.y())
+                    } else {
+                        vec2f(max_size.x(), svg_size.y() * max_size.x() / svg_size.x())
+                    }
+                };
+                (size, Some(tree))
+            }
+            Err(error) => {
+                log::error!("{}", error);
+                (constraint.min, None)
+            }
+        }
     }
 
     fn after_layout(&mut self, _: Vector2F, _: &mut Self::LayoutState, _: &mut AfterLayoutContext) {
     }
 
-    fn paint(
-        &mut self,
-        _: pathfinder_geometry::rect::RectF,
-        _: &mut Self::LayoutState,
-        _: &mut PaintContext,
-    ) -> Self::PaintState {
+    fn paint(&mut self, bounds: RectF, svg: &mut Self::LayoutState, ctx: &mut PaintContext) {
+        if let Some(svg) = svg.clone() {
+            ctx.scene.push_icon(scene::Icon {
+                bounds,
+                svg,
+                path: self.path.clone(),
+                color: self.color,
+            });
+        }
     }
 
     fn dispatch_event(
         &mut self,
         _: &Event,
-        _: pathfinder_geometry::rect::RectF,
+        _: RectF,
         _: &mut Self::LayoutState,
         _: &mut Self::PaintState,
         _: &mut EventContext,
@@ -75,3 +87,10 @@ impl Element for Svg {
         false
     }
 }
+
+fn from_usvg_rect(rect: usvg::Rect) -> RectF {
+    RectF::new(
+        vec2f(rect.x() as f32, rect.y() as f32),
+        vec2f(rect.width() as f32, rect.height() as f32),
+    )
+}

gpui/src/platform/mac/renderer.rs 🔗

@@ -296,7 +296,7 @@ impl Renderer {
                 drawable_size,
                 command_encoder,
             );
-            self.render_glyph_sprites(scene, layer, offset, drawable_size, command_encoder);
+            self.render_sprites(scene, layer, offset, drawable_size, command_encoder);
         }
 
         command_encoder.end_encoding();
@@ -465,7 +465,7 @@ impl Renderer {
         *offset = next_offset;
     }
 
-    fn render_glyph_sprites(
+    fn render_sprites(
         &mut self,
         scene: &Scene,
         layer: &Layer,
@@ -473,11 +473,12 @@ impl Renderer {
         drawable_size: Vector2F,
         command_encoder: &metal::RenderCommandEncoderRef,
     ) {
-        if layer.glyphs().is_empty() {
+        if layer.glyphs().is_empty() && layer.icons().is_empty() {
             return;
         }
 
         let mut sprites_by_atlas = HashMap::new();
+
         for glyph in layer.glyphs() {
             if let Some(sprite) = self.sprite_cache.render_glyph(
                 glyph.font_id,
@@ -501,6 +502,28 @@ impl Renderer {
             }
         }
 
+        for icon in layer.icons() {
+            let sprite = self.sprite_cache.render_icon(
+                icon.bounds.size(),
+                icon.path.clone(),
+                icon.svg.clone(),
+                scene.scale_factor(),
+            );
+
+            // Snap sprite to pixel grid.
+            let origin = (icon.bounds.origin() * scene.scale_factor()).floor();
+            sprites_by_atlas
+                .entry(sprite.atlas_id)
+                .or_insert_with(Vec::new)
+                .push(shaders::GPUISprite {
+                    origin: origin.to_float2(),
+                    size: sprite.size.to_float2(),
+                    atlas_origin: sprite.atlas_origin.to_float2(),
+                    color: icon.color.to_uchar4(),
+                    compute_winding: 0,
+                });
+        }
+
         command_encoder.set_render_pipeline_state(&self.sprite_pipeline_state);
         command_encoder.set_vertex_buffer(
             shaders::GPUISpriteVertexInputIndex_GPUISpriteVertexInputIndexVertices as u64,

gpui/src/platform/mac/sprite_cache.rs 🔗

@@ -27,12 +27,27 @@ pub struct GlyphSprite {
     pub size: Vector2I,
 }
 
+#[derive(Hash, Eq, PartialEq)]
+struct IconDescriptor {
+    path: String,
+    width: i32,
+    height: i32,
+}
+
+#[derive(Clone)]
+pub struct IconSprite {
+    pub atlas_id: usize,
+    pub atlas_origin: Vector2I,
+    pub size: Vector2I,
+}
+
 pub struct SpriteCache {
     device: metal::Device,
     atlas_size: Vector2I,
     fonts: Arc<dyn platform::FontSystem>,
     atlases: Vec<Atlas>,
     glyphs: HashMap<GlyphDescriptor, Option<GlyphSprite>>,
+    icons: HashMap<IconDescriptor, IconSprite>,
 }
 
 impl SpriteCache {
@@ -48,6 +63,7 @@ impl SpriteCache {
             fonts,
             atlases,
             glyphs: Default::default(),
+            icons: Default::default(),
         }
     }
 
@@ -119,6 +135,54 @@ impl SpriteCache {
             .clone()
     }
 
+    pub fn render_icon(
+        &mut self,
+        size: Vector2F,
+        path: String,
+        svg: usvg::Tree,
+        scale_factor: f32,
+    ) -> IconSprite {
+        let atlases = &mut self.atlases;
+        let atlas_size = self.atlas_size;
+        let device = &self.device;
+        let size = (size * scale_factor).round().to_i32();
+        assert!(size.x() < atlas_size.x());
+        assert!(size.y() < atlas_size.y());
+        self.icons
+            .entry(IconDescriptor {
+                path,
+                width: size.x(),
+                height: size.y(),
+            })
+            .or_insert_with(|| {
+                let mut pixmap = tiny_skia::Pixmap::new(size.x() as u32, size.y() as u32).unwrap();
+                resvg::render(&svg, usvg::FitTo::Width(size.x() as u32), pixmap.as_mut());
+                let mask = pixmap
+                    .pixels()
+                    .iter()
+                    .map(|a| a.alpha())
+                    .collect::<Vec<_>>();
+
+                let atlas_bounds = atlases
+                    .last_mut()
+                    .unwrap()
+                    .try_insert(size, &mask)
+                    .unwrap_or_else(|| {
+                        let mut atlas = Atlas::new(device, atlas_size);
+                        let bounds = atlas.try_insert(size, &mask).unwrap();
+                        atlases.push(atlas);
+                        bounds
+                    });
+
+                IconSprite {
+                    atlas_id: atlases.len() - 1,
+                    atlas_origin: atlas_bounds.origin(),
+                    size,
+                }
+            })
+            .clone()
+    }
+
     pub fn atlas_texture(&self, atlas_id: usize) -> Option<&metal::TextureRef> {
         self.atlases.get(atlas_id).map(|a| a.texture.as_ref())
     }

gpui/src/scene.rs 🔗

@@ -10,12 +10,13 @@ pub struct Scene {
     active_layer_stack: Vec<usize>,
 }
 
-#[derive(Default, Debug)]
+#[derive(Default)]
 pub struct Layer {
     clip_bounds: Option<RectF>,
     quads: Vec<Quad>,
     shadows: Vec<Shadow>,
     glyphs: Vec<Glyph>,
+    icons: Vec<Icon>,
     paths: Vec<Path>,
 }
 
@@ -44,6 +45,13 @@ pub struct Glyph {
     pub color: ColorU,
 }
 
+pub struct Icon {
+    pub bounds: RectF,
+    pub svg: usvg::Tree,
+    pub path: String,
+    pub color: ColorU,
+}
+
 #[derive(Clone, Copy, Default, Debug)]
 pub struct Border {
     pub width: f32,
@@ -107,6 +115,10 @@ impl Scene {
         self.active_layer().push_glyph(glyph)
     }
 
+    pub fn push_icon(&mut self, icon: Icon) {
+        self.active_layer().push_icon(icon)
+    }
+
     pub fn push_path(&mut self, path: Path) {
         self.active_layer().push_path(path);
     }
@@ -123,6 +135,7 @@ impl Layer {
             quads: Vec::new(),
             shadows: Vec::new(),
             glyphs: Vec::new(),
+            icons: Vec::new(),
             paths: Vec::new(),
         }
     }
@@ -155,6 +168,14 @@ impl Layer {
         self.glyphs.as_slice()
     }
 
+    pub fn push_icon(&mut self, icon: Icon) {
+        self.icons.push(icon);
+    }
+
+    pub fn icons(&self) -> &[Icon] {
+        self.icons.as_slice()
+    }
+
     fn push_path(&mut self, path: Path) {
         if !path.bounds.is_empty() {
             self.paths.push(path);

zed/assets/icons/file-16.svg 🔗

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M3.75 1.5a.25.25 0 00-.25.25v11.5c0 .138.112.25.25.25h8.5a.25.25 0 00.25-.25V6H9.75A1.75 1.75 0 018 4.25V1.5H3.75zm5.75.56v2.19c0 .138.112.25.25.25h2.19L9.5 2.06zM2 1.75C2 .784 2.784 0 3.75 0h5.086c.464 0 .909.184 1.237.513l3.414 3.414c.329.328.513.773.513 1.237v8.086A1.75 1.75 0 0112.25 15h-8.5A1.75 1.75 0 012 13.25V1.75z"/></svg>

zed/src/file_finder.rs 🔗

@@ -175,8 +175,7 @@ impl FileFinder {
                             LineBox::new(
                                 settings.ui_font_family,
                                 settings.ui_font_size,
-                                Empty::new().boxed(),
-                                // Svg::new("icons/file-16.svg".into()).boxed(),
+                                Svg::new("icons/file-16.svg".into()).boxed(),
                             )
                             .boxed(),
                         )