Render a close tab button on tab hover

Nathan Sobo and Max Brunsfeld created

Co-Authored-By: Max Brunsfeld <maxbrunsfeld@gmail.com>

Change summary

gpui/src/elements/mouse_event_handler.rs |   4 
gpui/src/elements/svg.rs                 |   8 
gpui/src/platform/mac/sprite_cache.rs    |   6 
gpui/src/scene.rs                        |   4 
zed/assets/.gitkeep                      |   0 
zed/assets/icons/x.svg                   |   1 
zed/src/file_finder.rs                   |   2 
zed/src/workspace/pane.rs                | 140 ++++++++++++++-----------
8 files changed, 94 insertions(+), 71 deletions(-)

Detailed changes

gpui/src/elements/mouse_event_handler.rs 🔗

@@ -12,8 +12,8 @@ pub struct MouseEventHandler {
 
 #[derive(Clone, Copy, Debug, Default)]
 pub struct MouseState {
-    hovered: bool,
-    clicked: bool,
+    pub hovered: bool,
+    pub clicked: bool,
 }
 
 impl MouseEventHandler {

gpui/src/elements/svg.rs 🔗

@@ -1,3 +1,5 @@
+use std::borrow::Cow;
+
 use serde_json::json;
 
 use crate::{
@@ -11,14 +13,14 @@ use crate::{
 };
 
 pub struct Svg {
-    path: String,
+    path: Cow<'static, str>,
     color: ColorU,
 }
 
 impl Svg {
-    pub fn new(path: String) -> Self {
+    pub fn new(path: impl Into<Cow<'static, str>>) -> Self {
         Self {
-            path,
+            path: path.into(),
             color: ColorU::black(),
         }
     }

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

@@ -9,7 +9,7 @@ use crate::{
 use etagere::BucketedAtlasAllocator;
 use metal::{MTLPixelFormat, TextureDescriptor};
 use ordered_float::OrderedFloat;
-use std::{collections::HashMap, sync::Arc};
+use std::{borrow::Cow, collections::HashMap, sync::Arc};
 
 #[derive(Hash, Eq, PartialEq)]
 struct GlyphDescriptor {
@@ -29,7 +29,7 @@ pub struct GlyphSprite {
 
 #[derive(Hash, Eq, PartialEq)]
 struct IconDescriptor {
-    path: String,
+    path: Cow<'static, str>,
     width: i32,
     height: i32,
 }
@@ -138,7 +138,7 @@ impl SpriteCache {
     pub fn render_icon(
         &mut self,
         size: Vector2F,
-        path: String,
+        path: Cow<'static, str>,
         svg: usvg::Tree,
         scale_factor: f32,
     ) -> IconSprite {

gpui/src/scene.rs 🔗

@@ -1,3 +1,5 @@
+use std::borrow::Cow;
+
 use serde_json::json;
 
 use crate::{
@@ -51,7 +53,7 @@ pub struct Glyph {
 pub struct Icon {
     pub bounds: RectF,
     pub svg: usvg::Tree,
-    pub path: String,
+    pub path: Cow<'static, str>,
     pub color: ColorU,
 }
 

zed/assets/icons/x.svg 🔗

@@ -0,0 +1,3 @@
+<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">

zed/src/file_finder.rs 🔗

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

zed/src/workspace/pane.rs 🔗

@@ -183,54 +183,61 @@ impl Pane {
         let mut row = Flex::row();
         let last_item_ix = self.items.len() - 1;
         for (ix, item) in self.items.iter().enumerate() {
-            let title = item.title(ctx);
-
-            let mut border = Border::new(1.0, border_color);
-            border.left = ix > 0;
-            border.right = ix == last_item_ix;
-            border.bottom = ix != self.active_item;
-
-            let padding = 6.;
-            let mut container = Container::new(
-                Stack::new()
-                    .with_child(
-                        Align::new(
-                            Label::new(title, settings.ui_font_family, settings.ui_font_size)
-                                .boxed(),
-                        )
-                        .boxed(),
-                    )
-                    .with_child(
-                        LineBox::new(
-                            settings.ui_font_family,
-                            settings.ui_font_size,
-                            Align::new(Self::render_modified_icon(item.is_dirty(ctx)))
-                                .right()
-                                .boxed(),
-                        )
-                        .boxed(),
-                    )
-                    .boxed(),
-            )
-            .with_vertical_padding(padding)
-            .with_horizontal_padding(10.)
-            .with_border(border);
-
-            if ix == self.active_item {
-                container = container
-                    .with_background_color(ColorU::white())
-                    .with_padding_bottom(padding + border.width);
-            } else {
-                container = container.with_background_color(ColorU::from_u32(0xeaeaebff));
-            }
-
             enum Tab {}
 
             row.add_child(
                 Expanded::new(
                     1.0,
                     MouseEventHandler::new::<Tab, _>(item.id(), ctx, |mouse_state| {
-                        log::info!("mouse event handler {:?}", mouse_state);
+                        let title = item.title(ctx);
+
+                        let mut border = Border::new(1.0, border_color);
+                        border.left = ix > 0;
+                        border.right = ix == last_item_ix;
+                        border.bottom = ix != self.active_item;
+
+                        let padding = 6.;
+                        let mut container = Container::new(
+                            Stack::new()
+                                .with_child(
+                                    Align::new(
+                                        Label::new(
+                                            title,
+                                            settings.ui_font_family,
+                                            settings.ui_font_size,
+                                        )
+                                        .boxed(),
+                                    )
+                                    .boxed(),
+                                )
+                                .with_child(
+                                    LineBox::new(
+                                        settings.ui_font_family,
+                                        settings.ui_font_size,
+                                        Align::new(Self::render_tab_icon(
+                                            mouse_state.hovered,
+                                            item.is_dirty(ctx),
+                                        ))
+                                        .right()
+                                        .boxed(),
+                                    )
+                                    .boxed(),
+                                )
+                                .boxed(),
+                        )
+                        .with_vertical_padding(padding)
+                        .with_horizontal_padding(10.)
+                        .with_border(border);
+
+                        if ix == self.active_item {
+                            container = container
+                                .with_background_color(ColorU::white())
+                                .with_padding_bottom(padding + border.width);
+                        } else {
+                            container =
+                                container.with_background_color(ColorU::from_u32(0xeaeaebff));
+                        }
+
                         ConstrainedBox::new(
                             EventHandler::new(container.boxed())
                                 .on_mouse_down(move |ctx| {
@@ -290,25 +297,36 @@ impl Pane {
         row.named("tabs")
     }
 
-    fn render_modified_icon(is_modified: bool) -> ElementBox {
-        let diameter = 8.;
-        ConstrainedBox::new(
-            Canvas::new(move |bounds, ctx| {
-                if is_modified {
-                    let square = RectF::new(bounds.origin(), vec2f(diameter, diameter));
-                    ctx.scene.push_quad(Quad {
-                        bounds: square,
-                        background: Some(ColorF::new(0.639, 0.839, 1.0, 1.0).to_u8()),
-                        border: Default::default(),
-                        corner_radius: diameter / 2.,
-                    });
-                }
-            })
-            .boxed(),
-        )
-        .with_width(diameter)
-        .with_height(diameter)
-        .named("tab-right-icon")
+    fn render_tab_icon(tab_hovered: bool, is_modified: bool) -> ElementBox {
+        let modified_color = ColorU::from_u32(0x556de8ff);
+        if tab_hovered {
+            let mut icon = Svg::new("icons/x.svg");
+            if is_modified {
+                icon = icon.with_color(modified_color);
+            }
+            ConstrainedBox::new(icon.boxed())
+                .with_width(10.)
+                .named("close-tab-icon")
+        } else {
+            let diameter = 8.;
+            ConstrainedBox::new(
+                Canvas::new(move |bounds, ctx| {
+                    if is_modified {
+                        let square = RectF::new(bounds.origin(), vec2f(diameter, diameter));
+                        ctx.scene.push_quad(Quad {
+                            bounds: square,
+                            background: Some(modified_color),
+                            border: Default::default(),
+                            corner_radius: diameter / 2.,
+                        });
+                    }
+                })
+                .boxed(),
+            )
+            .with_width(diameter)
+            .with_height(diameter)
+            .named("unsaved-tab-icon")
+        }
     }
 }