Merge pull request #28 from zed-industries/close-tabs

Nathan Sobo created

Add tab close buttons

Change summary

gpui/src/app.rs                             | 189 +++++++++++++-----
gpui/src/elements/mod.rs                    |   2 
gpui/src/elements/mouse_event_handler.rs    | 134 +++++++++++++
gpui/src/elements/svg.rs                    |   8 
gpui/src/platform/event.rs                  |   5 
gpui/src/platform/mac/event.rs              |   6 
gpui/src/platform/mac/renderer.rs           |  24 +-
gpui/src/platform/mac/shaders/shaders.h     |   3 
gpui/src/platform/mac/shaders/shaders.metal |   4 
gpui/src/platform/mac/sprite_cache.rs       |  10 
gpui/src/platform/mac/window.rs             |   4 
gpui/src/presenter.rs                       |  49 +++
gpui/src/scene.rs                           |   4 
zed/assets/.gitkeep                         |   0 
zed/assets/icons/x.svg                      |   3 
zed/src/editor/buffer_element.rs            |   1 
zed/src/file_finder.rs                      |   2 
zed/src/workspace/pane.rs                   | 237 +++++++++++++---------
18 files changed, 501 insertions(+), 184 deletions(-)

Detailed changes

gpui/src/app.rs 🔗

@@ -9,7 +9,7 @@ use crate::{
 };
 use anyhow::{anyhow, Result};
 use keymap::MatchResult;
-use parking_lot::Mutex;
+use parking_lot::{Mutex, RwLock};
 use pathfinder_geometry::{rect::RectF, vector::vec2f};
 use platform::Event;
 use postage::{sink::Sink as _, stream::Stream as _};
@@ -216,7 +216,7 @@ impl App {
     }
 
     pub fn font_cache(&self) -> Arc<FontCache> {
-        self.0.borrow().font_cache.clone()
+        self.0.borrow().ctx.font_cache.clone()
     }
 
     fn update<T, F: FnOnce(&mut MutableAppContext) -> T>(&mut self, callback: F) -> T {
@@ -326,7 +326,7 @@ impl TestAppContext {
     }
 
     pub fn font_cache(&self) -> Arc<FontCache> {
-        self.0.borrow().font_cache.clone()
+        self.0.borrow().ctx.font_cache.clone()
     }
 
     pub fn platform(&self) -> Rc<dyn platform::Platform> {
@@ -370,7 +370,6 @@ type GlobalActionCallback = dyn FnMut(&dyn Any, &mut MutableAppContext);
 pub struct MutableAppContext {
     weak_self: Option<rc::Weak<RefCell<Self>>>,
     platform: Rc<dyn platform::Platform>,
-    font_cache: Arc<FontCache>,
     assets: Arc<AssetCache>,
     ctx: AppContext,
     actions: HashMap<TypeId, HashMap<String, Vec<Box<ActionCallback>>>>,
@@ -404,14 +403,15 @@ impl MutableAppContext {
         Self {
             weak_self: None,
             platform,
-            font_cache: Arc::new(FontCache::new(fonts)),
             assets: Arc::new(AssetCache::new(asset_source)),
             ctx: AppContext {
-                models: HashMap::new(),
-                windows: HashMap::new(),
+                models: Default::default(),
+                windows: Default::default(),
+                values: Default::default(),
                 ref_counts: Arc::new(Mutex::new(RefCounts::default())),
                 background: Arc::new(executor::Background::new()),
                 thread_pool: scoped_pool::Pool::new(num_cpus::get(), "app"),
+                font_cache: Arc::new(FontCache::new(fonts)),
             },
             actions: HashMap::new(),
             global_actions: HashMap::new(),
@@ -443,7 +443,7 @@ impl MutableAppContext {
     }
 
     pub fn font_cache(&self) -> &Arc<FontCache> {
-        &self.font_cache
+        &self.ctx.font_cache
     }
 
     pub fn foreground_executor(&self) -> &Rc<executor::Foreground> {
@@ -584,6 +584,11 @@ impl MutableAppContext {
         );
     }
 
+    pub(crate) fn notify_view(&mut self, window_id: usize, view_id: usize) {
+        self.pending_effects
+            .push_back(Effect::ViewNotification { window_id, view_id });
+    }
+
     pub fn dispatch_action<T: 'static + Any>(
         &mut self,
         window_id: usize,
@@ -594,7 +599,7 @@ impl MutableAppContext {
         self.dispatch_action_any(window_id, &responder_chain, name, Box::new(arg).as_ref());
     }
 
-    fn dispatch_action_any(
+    pub(crate) fn dispatch_action_any(
         &mut self,
         window_id: usize,
         path: &[usize],
@@ -763,7 +768,7 @@ impl MutableAppContext {
         let text_layout_cache = TextLayoutCache::new(self.platform.fonts());
         let presenter = Rc::new(RefCell::new(Presenter::new(
             window_id,
-            self.font_cache.clone(),
+            self.ctx.font_cache.clone(),
             text_layout_cache,
             self.assets.clone(),
             self,
@@ -787,15 +792,7 @@ impl MutableAppContext {
                         }
                     }
 
-                    let actions = presenter.borrow_mut().dispatch_event(event, ctx.as_ref());
-                    for action in actions {
-                        ctx.dispatch_action_any(
-                            window_id,
-                            &action.path,
-                            action.name,
-                            action.arg.as_ref(),
-                        );
-                    }
+                    presenter.borrow_mut().dispatch_event(event, ctx);
                 })
             }));
         }
@@ -865,8 +862,9 @@ impl MutableAppContext {
 
     fn remove_dropped_entities(&mut self) {
         loop {
-            let (dropped_models, dropped_views) = self.ctx.ref_counts.lock().take_dropped();
-            if dropped_models.is_empty() && dropped_views.is_empty() {
+            let (dropped_models, dropped_views, dropped_values) =
+                self.ctx.ref_counts.lock().take_dropped();
+            if dropped_models.is_empty() && dropped_views.is_empty() && dropped_values.is_empty() {
                 break;
             }
 
@@ -890,6 +888,11 @@ impl MutableAppContext {
                     window.views.remove(&view_id);
                 }
             }
+
+            let mut values = self.ctx.values.write();
+            for key in dropped_values {
+                values.remove(&key);
+            }
         }
     }
 
@@ -910,11 +913,14 @@ impl MutableAppContext {
                         self.focus(window_id, view_id);
                     }
                 }
+
+                if self.pending_effects.is_empty() {
+                    self.remove_dropped_entities();
+                    self.update_windows();
+                }
             }
 
             self.flushing_effects = false;
-            self.remove_dropped_entities();
-            self.update_windows();
         }
     }
 
@@ -1308,9 +1314,11 @@ impl AsRef<AppContext> for MutableAppContext {
 pub struct AppContext {
     models: HashMap<usize, Box<dyn AnyModel>>,
     windows: HashMap<usize, Window>,
+    values: RwLock<HashMap<(TypeId, usize), Box<dyn Any>>>,
     background: Arc<executor::Background>,
     ref_counts: Arc<Mutex<RefCounts>>,
     thread_pool: scoped_pool::Pool,
+    font_cache: Arc<FontCache>,
 }
 
 impl AppContext {
@@ -1350,9 +1358,20 @@ impl AppContext {
         &self.background
     }
 
+    pub fn font_cache(&self) -> &FontCache {
+        &self.font_cache
+    }
+
     pub fn thread_pool(&self) -> &scoped_pool::Pool {
         &self.thread_pool
     }
+
+    pub fn value<Tag: 'static, T: 'static + Default>(&self, id: usize) -> ValueHandle<T> {
+        let key = (TypeId::of::<Tag>(), id);
+        let mut values = self.values.write();
+        values.entry(key).or_insert_with(|| Box::new(T::default()));
+        ValueHandle::new(TypeId::of::<Tag>(), id, &self.ref_counts)
+    }
 }
 
 impl ReadModel for AppContext {
@@ -1823,12 +1842,7 @@ impl<'a, T: View> ViewContext<'a, T> {
     }
 
     pub fn notify(&mut self) {
-        self.app
-            .pending_effects
-            .push_back(Effect::ViewNotification {
-                window_id: self.window_id,
-                view_id: self.view_id,
-            });
+        self.app.notify_view(self.window_id, self.view_id);
     }
 
     pub fn propagate_action(&mut self) {
@@ -1958,7 +1972,7 @@ pub struct ModelHandle<T> {
 
 impl<T: Entity> ModelHandle<T> {
     fn new(model_id: usize, ref_counts: &Arc<Mutex<RefCounts>>) -> Self {
-        ref_counts.lock().inc(model_id);
+        ref_counts.lock().inc_entity(model_id);
         Self {
             model_id,
             model_type: PhantomData,
@@ -2031,7 +2045,7 @@ impl<T: Entity> ModelHandle<T> {
 impl<T> Clone for ModelHandle<T> {
     fn clone(&self) -> Self {
         if let Some(ref_counts) = self.ref_counts.upgrade() {
-            ref_counts.lock().inc(self.model_id);
+            ref_counts.lock().inc_entity(self.model_id);
         }
 
         Self {
@@ -2122,7 +2136,7 @@ pub struct ViewHandle<T> {
 
 impl<T: View> ViewHandle<T> {
     fn new(window_id: usize, view_id: usize, ref_counts: &Arc<Mutex<RefCounts>>) -> Self {
-        ref_counts.lock().inc(view_id);
+        ref_counts.lock().inc_entity(view_id);
         Self {
             window_id,
             view_id,
@@ -2205,7 +2219,7 @@ impl<T: View> ViewHandle<T> {
 impl<T> Clone for ViewHandle<T> {
     fn clone(&self) -> Self {
         if let Some(ref_counts) = self.ref_counts.upgrade() {
-            ref_counts.lock().inc(self.view_id);
+            ref_counts.lock().inc_entity(self.view_id);
         }
 
         Self {
@@ -2282,7 +2296,7 @@ impl AnyViewHandle {
 impl<T: View> From<&ViewHandle<T>> for AnyViewHandle {
     fn from(handle: &ViewHandle<T>) -> Self {
         if let Some(ref_counts) = handle.ref_counts.upgrade() {
-            ref_counts.lock().inc(handle.view_id);
+            ref_counts.lock().inc_entity(handle.view_id);
         }
         AnyViewHandle {
             window_id: handle.window_id,
@@ -2342,48 +2356,113 @@ impl<T> Clone for WeakViewHandle<T> {
     }
 }
 
+pub struct ValueHandle<T> {
+    value_type: PhantomData<T>,
+    tag_type_id: TypeId,
+    id: usize,
+    ref_counts: Weak<Mutex<RefCounts>>,
+}
+
+impl<T: 'static> ValueHandle<T> {
+    fn new(tag_type_id: TypeId, id: usize, ref_counts: &Arc<Mutex<RefCounts>>) -> Self {
+        ref_counts.lock().inc_value(tag_type_id, id);
+        Self {
+            value_type: PhantomData,
+            tag_type_id,
+            id,
+            ref_counts: Arc::downgrade(ref_counts),
+        }
+    }
+
+    pub fn read<R>(&self, ctx: &AppContext, f: impl FnOnce(&T) -> R) -> R {
+        f(ctx
+            .values
+            .read()
+            .get(&(self.tag_type_id, self.id))
+            .unwrap()
+            .downcast_ref()
+            .unwrap())
+    }
+
+    pub fn update<R>(&self, ctx: &AppContext, f: impl FnOnce(&mut T) -> R) -> R {
+        f(ctx
+            .values
+            .write()
+            .get_mut(&(self.tag_type_id, self.id))
+            .unwrap()
+            .downcast_mut()
+            .unwrap())
+    }
+}
+
+impl<T> Drop for ValueHandle<T> {
+    fn drop(&mut self) {
+        if let Some(ref_counts) = self.ref_counts.upgrade() {
+            ref_counts.lock().dec_value(self.tag_type_id, self.id);
+        }
+    }
+}
+
 #[derive(Default)]
 struct RefCounts {
-    counts: HashMap<usize, usize>,
+    entity_counts: HashMap<usize, usize>,
+    value_counts: HashMap<(TypeId, usize), usize>,
     dropped_models: HashSet<usize>,
     dropped_views: HashSet<(usize, usize)>,
+    dropped_values: HashSet<(TypeId, usize)>,
 }
 
 impl RefCounts {
-    fn inc(&mut self, model_id: usize) {
-        *self.counts.entry(model_id).or_insert(0) += 1;
+    fn inc_entity(&mut self, model_id: usize) {
+        *self.entity_counts.entry(model_id).or_insert(0) += 1;
+    }
+
+    fn inc_value(&mut self, tag_type_id: TypeId, id: usize) {
+        *self.value_counts.entry((tag_type_id, id)).or_insert(0) += 1;
     }
 
     fn dec_model(&mut self, model_id: usize) {
-        if let Some(count) = self.counts.get_mut(&model_id) {
-            *count -= 1;
-            if *count == 0 {
-                self.counts.remove(&model_id);
-                self.dropped_models.insert(model_id);
-            }
-        } else {
-            panic!("Expected ref count to be positive")
+        let count = self.entity_counts.get_mut(&model_id).unwrap();
+        *count -= 1;
+        if *count == 0 {
+            self.entity_counts.remove(&model_id);
+            self.dropped_models.insert(model_id);
         }
     }
 
     fn dec_view(&mut self, window_id: usize, view_id: usize) {
-        if let Some(count) = self.counts.get_mut(&view_id) {
-            *count -= 1;
-            if *count == 0 {
-                self.counts.remove(&view_id);
-                self.dropped_views.insert((window_id, view_id));
-            }
-        } else {
-            panic!("Expected ref count to be positive")
+        let count = self.entity_counts.get_mut(&view_id).unwrap();
+        *count -= 1;
+        if *count == 0 {
+            self.entity_counts.remove(&view_id);
+            self.dropped_views.insert((window_id, view_id));
         }
     }
 
-    fn take_dropped(&mut self) -> (HashSet<usize>, HashSet<(usize, usize)>) {
+    fn dec_value(&mut self, tag_type_id: TypeId, id: usize) {
+        let key = (tag_type_id, id);
+        let count = self.value_counts.get_mut(&key).unwrap();
+        *count -= 1;
+        if *count == 0 {
+            self.value_counts.remove(&key);
+            self.dropped_values.insert(key);
+        }
+    }
+
+    fn take_dropped(
+        &mut self,
+    ) -> (
+        HashSet<usize>,
+        HashSet<(usize, usize)>,
+        HashSet<(TypeId, usize)>,
+    ) {
         let mut dropped_models = HashSet::new();
         let mut dropped_views = HashSet::new();
+        let mut dropped_values = HashSet::new();
         std::mem::swap(&mut self.dropped_models, &mut dropped_models);
         std::mem::swap(&mut self.dropped_views, &mut dropped_views);
-        (dropped_models, dropped_views)
+        std::mem::swap(&mut self.dropped_values, &mut dropped_values);
+        (dropped_models, dropped_views, dropped_values)
     }
 }
 

gpui/src/elements/mod.rs 🔗

@@ -7,6 +7,7 @@ mod event_handler;
 mod flex;
 mod label;
 mod line_box;
+mod mouse_event_handler;
 mod new;
 mod stack;
 mod svg;
@@ -22,6 +23,7 @@ pub use event_handler::*;
 pub use flex::*;
 pub use label::*;
 pub use line_box::*;
+pub use mouse_event_handler::*;
 pub use new::*;
 pub use stack::*;
 pub use svg::*;

gpui/src/elements/mouse_event_handler.rs 🔗

@@ -0,0 +1,134 @@
+use crate::{
+    geometry::{rect::RectF, vector::Vector2F},
+    AfterLayoutContext, AppContext, DebugContext, Element, ElementBox, Event, EventContext,
+    LayoutContext, PaintContext, SizeConstraint, ValueHandle,
+};
+use serde_json::json;
+
+pub struct MouseEventHandler {
+    state: ValueHandle<MouseState>,
+    child: ElementBox,
+    click_handler: Option<Box<dyn FnMut(&mut EventContext)>>,
+}
+
+#[derive(Clone, Copy, Debug, Default)]
+pub struct MouseState {
+    pub hovered: bool,
+    pub clicked: bool,
+}
+
+impl MouseEventHandler {
+    pub fn new<Tag, F>(id: usize, ctx: &AppContext, render_child: F) -> Self
+    where
+        Tag: 'static,
+        F: FnOnce(MouseState) -> ElementBox,
+    {
+        let state_handle = ctx.value::<Tag, _>(id);
+        let state = state_handle.read(ctx, |state| *state);
+        let child = render_child(state);
+        Self {
+            state: state_handle,
+            child,
+            click_handler: None,
+        }
+    }
+
+    pub fn on_click(mut self, handler: impl FnMut(&mut EventContext) + 'static) -> Self {
+        self.click_handler = Some(Box::new(handler));
+        self
+    }
+}
+
+impl Element for MouseEventHandler {
+    type LayoutState = ();
+    type PaintState = ();
+
+    fn layout(
+        &mut self,
+        constraint: SizeConstraint,
+        ctx: &mut LayoutContext,
+    ) -> (Vector2F, Self::LayoutState) {
+        (self.child.layout(constraint, ctx), ())
+    }
+
+    fn after_layout(
+        &mut self,
+        _: Vector2F,
+        _: &mut Self::LayoutState,
+        ctx: &mut AfterLayoutContext,
+    ) {
+        self.child.after_layout(ctx);
+    }
+
+    fn paint(
+        &mut self,
+        bounds: RectF,
+        _: &mut Self::LayoutState,
+        ctx: &mut PaintContext,
+    ) -> Self::PaintState {
+        self.child.paint(bounds.origin(), ctx);
+    }
+
+    fn dispatch_event(
+        &mut self,
+        event: &Event,
+        bounds: RectF,
+        _: &mut Self::LayoutState,
+        _: &mut Self::PaintState,
+        ctx: &mut EventContext,
+    ) -> bool {
+        let click_handler = self.click_handler.as_mut();
+
+        let handled_in_child = self.child.dispatch_event(event, ctx);
+
+        self.state.update(ctx.app, |state| match event {
+            Event::MouseMoved { position } => {
+                let mouse_in = bounds.contains_point(*position);
+                if state.hovered != mouse_in {
+                    state.hovered = mouse_in;
+                    ctx.notify();
+                    true
+                } else {
+                    handled_in_child
+                }
+            }
+            Event::LeftMouseDown { position, .. } => {
+                if !handled_in_child && bounds.contains_point(*position) {
+                    state.clicked = true;
+                    ctx.notify();
+                    true
+                } else {
+                    handled_in_child
+                }
+            }
+            Event::LeftMouseUp { position, .. } => {
+                if !handled_in_child && state.clicked {
+                    state.clicked = false;
+                    ctx.notify();
+                    if let Some(handler) = click_handler {
+                        if bounds.contains_point(*position) {
+                            handler(ctx);
+                        }
+                    }
+                    true
+                } else {
+                    handled_in_child
+                }
+            }
+            _ => handled_in_child,
+        })
+    }
+
+    fn debug(
+        &self,
+        _: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        ctx: &DebugContext,
+    ) -> serde_json::Value {
+        json!({
+            "type": "MouseEventHandler",
+            "child": self.child.debug(ctx),
+        })
+    }
+}

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/event.rs 🔗

@@ -1,6 +1,6 @@
 use crate::{geometry::vector::Vector2F, keymap::Keystroke};
 
-#[derive(Debug)]
+#[derive(Clone, Debug)]
 pub enum Event {
     KeyDown {
         keystroke: Keystroke,
@@ -21,4 +21,7 @@ pub enum Event {
     LeftMouseDragged {
         position: Vector2F,
     },
+    MouseMoved {
+        position: Vector2F,
+    },
 }

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

@@ -108,6 +108,12 @@ impl Event {
                 ),
                 precise: native_event.hasPreciseScrollingDeltas() == YES,
             }),
+            NSEventType::NSMouseMoved => window_height.map(|window_height| Self::MouseMoved {
+                position: vec2f(
+                    native_event.locationInWindow().x as f32,
+                    window_height - native_event.locationInWindow().y as f32,
+                ),
+            }),
             _ => None,
         }
     }

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

@@ -153,7 +153,8 @@ impl Renderer {
                     atlas_id,
                     shader_data: shaders::GPUISprite {
                         origin: origin.floor().to_float2(),
-                        size: size.to_float2(),
+                        target_size: size.to_float2(),
+                        source_size: size.to_float2(),
                         atlas_origin: atlas_origin.to_float2(),
                         color: path.color.to_uchar4(),
                         compute_winding: 1,
@@ -493,7 +494,8 @@ impl Renderer {
                     .or_insert_with(Vec::new)
                     .push(shaders::GPUISprite {
                         origin: origin.to_float2(),
-                        size: sprite.size.to_float2(),
+                        target_size: sprite.size.to_float2(),
+                        source_size: sprite.size.to_float2(),
                         atlas_origin: sprite.atlas_origin.to_float2(),
                         color: glyph.color.to_uchar4(),
                         compute_winding: 0,
@@ -502,21 +504,21 @@ 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(),
-            );
+            let origin = icon.bounds.origin() * scene.scale_factor();
+            let target_size = icon.bounds.size() * scene.scale_factor();
+            let source_size = (target_size * 2.).ceil().to_i32();
+
+            let sprite =
+                self.sprite_cache
+                    .render_icon(source_size, icon.path.clone(), icon.svg.clone());
 
-            // 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(),
+                    target_size: target_size.to_float2(),
+                    source_size: sprite.size.to_float2(),
                     atlas_origin: sprite.atlas_origin.to_float2(),
                     color: icon.color.to_uchar4(),
                     compute_winding: 0,

gpui/src/platform/mac/shaders/shaders.h 🔗

@@ -49,7 +49,8 @@ typedef enum {
 
 typedef struct {
     vector_float2 origin;
-    vector_float2 size;
+    vector_float2 target_size;
+    vector_float2 source_size;
     vector_float2 atlas_origin;
     vector_uchar4 color;
     uint8_t compute_winding;

gpui/src/platform/mac/shaders/shaders.metal 🔗

@@ -186,9 +186,9 @@ vertex SpriteFragmentInput sprite_vertex(
 ) {
     float2 unit_vertex = unit_vertices[unit_vertex_id];
     GPUISprite sprite = sprites[sprite_id];
-    float2 position = unit_vertex * sprite.size + sprite.origin;
+    float2 position = unit_vertex * sprite.target_size + sprite.origin;
     float4 device_position = to_device_position(position, *viewport_size);
-    float2 atlas_position = (unit_vertex * sprite.size + sprite.atlas_origin) / *atlas_size;
+    float2 atlas_position = (unit_vertex * sprite.source_size + sprite.atlas_origin) / *atlas_size;
 
     return SpriteFragmentInput {
         device_position,

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,
 }
@@ -137,15 +137,13 @@ impl SpriteCache {
 
     pub fn render_icon(
         &mut self,
-        size: Vector2F,
-        path: String,
+        size: Vector2I,
+        path: Cow<'static, str>,
         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

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

@@ -82,6 +82,10 @@ unsafe fn build_classes() {
             sel!(mouseUp:),
             handle_view_event as extern "C" fn(&Object, Sel, id),
         );
+        decl.add_method(
+            sel!(mouseMoved:),
+            handle_view_event as extern "C" fn(&Object, Sel, id),
+        );
         decl.add_method(
             sel!(mouseDragged:),
             handle_view_event as extern "C" fn(&Object, Sel, id),

gpui/src/presenter.rs 🔗

@@ -9,7 +9,11 @@ use crate::{
 };
 use pathfinder_geometry::vector::{vec2f, Vector2F};
 use serde_json::json;
-use std::{any::Any, collections::HashMap, sync::Arc};
+use std::{
+    any::Any,
+    collections::{HashMap, HashSet},
+    sync::Arc,
+};
 
 pub struct Presenter {
     window_id: usize,
@@ -18,6 +22,7 @@ pub struct Presenter {
     font_cache: Arc<FontCache>,
     text_layout_cache: TextLayoutCache,
     asset_cache: Arc<AssetCache>,
+    last_mouse_moved_event: Option<Event>,
 }
 
 impl Presenter {
@@ -35,6 +40,7 @@ impl Presenter {
             font_cache,
             text_layout_cache,
             asset_cache,
+            last_mouse_moved_event: None,
         }
     }
 
@@ -80,6 +86,10 @@ impl Presenter {
             };
             ctx.paint(root_view_id, Vector2F::zero());
             self.text_layout_cache.finish_frame();
+
+            if let Some(event) = self.last_mouse_moved_event.clone() {
+                self.dispatch_event(event, app)
+            }
         } else {
             log::error!("could not find root_view_id for window {}", self.window_id);
         }
@@ -114,20 +124,37 @@ impl Presenter {
         }
     }
 
-    pub fn dispatch_event(&mut self, event: Event, app: &AppContext) -> Vec<ActionToDispatch> {
+    pub fn dispatch_event(&mut self, event: Event, app: &mut MutableAppContext) {
         if let Some(root_view_id) = app.root_view_id(self.window_id) {
+            if matches!(event, Event::MouseMoved { .. }) {
+                self.last_mouse_moved_event = Some(event.clone());
+            }
+
             let mut ctx = EventContext {
                 rendered_views: &mut self.rendered_views,
-                actions: Vec::new(),
+                actions: Default::default(),
                 font_cache: &self.font_cache,
                 text_layout_cache: &self.text_layout_cache,
-                view_stack: Vec::new(),
-                app,
+                view_stack: Default::default(),
+                invalidated_views: Default::default(),
+                app: app.as_ref(),
             };
             ctx.dispatch_event(root_view_id, &event);
-            ctx.actions
-        } else {
-            Vec::new()
+
+            let invalidated_views = ctx.invalidated_views;
+            let actions = ctx.actions;
+
+            for view_id in invalidated_views {
+                app.notify_view(self.window_id, view_id);
+            }
+            for action in actions {
+                app.dispatch_action_any(
+                    self.window_id,
+                    &action.path,
+                    action.name,
+                    action.arg.as_ref(),
+                );
+            }
         }
     }
 
@@ -214,6 +241,7 @@ pub struct EventContext<'a> {
     pub text_layout_cache: &'a TextLayoutCache,
     pub app: &'a AppContext,
     view_stack: Vec<usize>,
+    invalidated_views: HashSet<usize>,
 }
 
 impl<'a> EventContext<'a> {
@@ -236,6 +264,11 @@ impl<'a> EventContext<'a> {
             arg: Box::new(arg),
         });
     }
+
+    pub fn notify(&mut self) {
+        self.invalidated_views
+            .insert(*self.view_stack.last().unwrap());
+    }
 }
 
 pub struct DebugContext<'a> {

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="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M17.6568 6.34314L12 12M12 12L6.34314 17.6568M12 12L17.6568 17.6568M12 12L6.34314 6.34314" stroke="black" stroke-width="2"/>
+</svg>

zed/src/editor/buffer_element.rs 🔗

@@ -474,6 +474,7 @@ impl Element for BufferElement {
                     precise,
                 } => self.scroll(*position, *delta, *precise, layout, paint, ctx),
                 Event::KeyDown { chars, .. } => self.key_down(chars, ctx),
+                _ => false,
             }
         } else {
             false

zed/src/file_finder.rs 🔗

@@ -189,7 +189,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 🔗

@@ -1,7 +1,7 @@
 use super::{ItemViewHandle, SplitDirection};
 use crate::{settings::Settings, watch};
 use gpui::{
-    color::{ColorF, ColorU},
+    color::ColorU,
     elements::*,
     geometry::{rect::RectF, vector::vec2f},
     keymap::Binding,
@@ -25,6 +25,12 @@ pub fn init(app: &mut MutableAppContext) {
     app.add_action("pane:close_active_item", |pane: &mut Pane, _: &(), ctx| {
         pane.close_active_item(ctx);
     });
+    app.add_action(
+        "pane:close_item",
+        |pane: &mut Pane, item_id: &usize, ctx| {
+            pane.close_item(*item_id, ctx);
+        },
+    );
     app.add_action("pane:split_up", |pane: &mut Pane, _: &(), ctx| {
         pane.split(SplitDirection::Up, ctx);
     });
@@ -155,15 +161,17 @@ impl Pane {
 
     pub fn close_active_item(&mut self, ctx: &mut ViewContext<Self>) {
         if !self.items.is_empty() {
-            self.items.remove(self.active_item);
-            if self.active_item >= self.items.len() {
-                self.active_item = self.items.len().saturating_sub(1);
-            }
-            ctx.notify();
+            self.close_item(self.items[self.active_item].id(), ctx)
         }
+    }
+
+    pub fn close_item(&mut self, item_id: usize, ctx: &mut ViewContext<Self>) {
+        self.items.retain(|item| item.id() != item_id);
+        self.active_item = cmp::min(self.active_item, self.items.len().saturating_sub(1));
         if self.items.is_empty() {
             ctx.emit(Event::Remove);
         }
+        ctx.notify();
     }
 
     fn focus_active_item(&mut self, ctx: &mut ViewContext<Self>) {
@@ -176,67 +184,80 @@ impl Pane {
         ctx.emit(Event::Split(direction));
     }
 
-    fn render_tabs(&self, app: &AppContext) -> ElementBox {
+    fn render_tabs(&self, ctx: &AppContext) -> ElementBox {
         let settings = smol::block_on(self.settings.read());
         let border_color = ColorU::from_u32(0xdbdbdcff);
+        let line_height = ctx.font_cache().line_height(
+            ctx.font_cache().default_font(settings.ui_font_family),
+            settings.ui_font_size,
+        );
 
         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(app);
-
-            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(app)))
-                                .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,
-                    ConstrainedBox::new(
-                        EventHandler::new(container.boxed())
-                            .on_mouse_down(move |ctx| {
-                                ctx.dispatch_action("pane:activate_item", ix);
-                                true
-                            })
-                            .boxed(),
-                    )
-                    .with_min_width(80.0)
-                    .with_max_width(264.0)
+                    MouseEventHandler::new::<Tab, _>(item.id(), ctx, |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 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(
+                                    Align::new(Self::render_tab_icon(
+                                        item.id(),
+                                        line_height - 2.,
+                                        mouse_state.hovered,
+                                        item.is_dirty(ctx),
+                                        ctx,
+                                    ))
+                                    .right()
+                                    .boxed(),
+                                )
+                                .boxed(),
+                        )
+                        .with_horizontal_padding(10.)
+                        .with_border(border);
+
+                        if ix == self.active_item {
+                            container = container
+                                .with_background_color(ColorU::white())
+                                .with_padding_bottom(border.width);
+                        } else {
+                            container =
+                                container.with_background_color(ColorU::from_u32(0xeaeaebff));
+                        }
+
+                        ConstrainedBox::new(
+                            EventHandler::new(container.boxed())
+                                .on_mouse_down(move |ctx| {
+                                    ctx.dispatch_action("pane:activate_item", ix);
+                                    true
+                                })
+                                .boxed(),
+                        )
+                        .with_min_width(80.0)
+                        .with_max_width(264.0)
+                        .boxed()
+                    })
                     .boxed(),
                 )
                 .named("tab"),
@@ -247,17 +268,9 @@ impl Pane {
         // so that the tab's border doesn't abut the window's border.
         row.add_child(
             ConstrainedBox::new(
-                Container::new(
-                    LineBox::new(
-                        settings.ui_font_family,
-                        settings.ui_font_size,
-                        Empty::new().boxed(),
-                    )
+                Container::new(Empty::new().boxed())
+                    .with_border(Border::bottom(1.0, border_color))
                     .boxed(),
-                )
-                .with_uniform_padding(6.0)
-                .with_border(Border::bottom(1.0, border_color))
-                .boxed(),
             )
             .with_min_width(20.)
             .named("fixed-filler"),
@@ -266,43 +279,77 @@ impl Pane {
         row.add_child(
             Expanded::new(
                 0.0,
-                Container::new(
-                    LineBox::new(
-                        settings.ui_font_family,
-                        settings.ui_font_size,
-                        Empty::new().boxed(),
-                    )
+                Container::new(Empty::new().boxed())
+                    .with_border(Border::bottom(1.0, border_color))
                     .boxed(),
-                )
-                .with_uniform_padding(6.0)
-                .with_border(Border::bottom(1.0, border_color))
-                .boxed(),
             )
             .named("filler"),
         );
 
-        row.named("tabs")
+        ConstrainedBox::new(row.boxed())
+            .with_height(line_height + 16.)
+            .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.,
-                    });
+    fn render_tab_icon(
+        item_id: usize,
+        close_icon_size: f32,
+        tab_hovered: bool,
+        is_modified: bool,
+        ctx: &AppContext,
+    ) -> ElementBox {
+        enum TabCloseButton {}
+
+        let modified_color = ColorU::from_u32(0x556de8ff);
+        let mut clicked_color = modified_color;
+        clicked_color.a = 180;
+
+        let icon = if tab_hovered {
+            let mut icon = Svg::new("icons/x.svg");
+
+            MouseEventHandler::new::<TabCloseButton, _>(item_id, ctx, |mouse_state| {
+                if mouse_state.hovered {
+                    Container::new(icon.with_color(ColorU::white()).boxed())
+                        .with_background_color(if mouse_state.clicked {
+                            clicked_color
+                        } else {
+                            modified_color
+                        })
+                        .with_corner_radius(close_icon_size / 2.)
+                        .boxed()
+                } else {
+                    if is_modified {
+                        icon = icon.with_color(modified_color);
+                    }
+                    icon.boxed()
                 }
             })
-            .boxed(),
-        )
-        .with_width(diameter)
-        .with_height(diameter)
-        .named("tab-right-icon")
+            .on_click(move |ctx| ctx.dispatch_action("pane:close_item", item_id))
+            .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")
+        };
+
+        ConstrainedBox::new(Align::new(icon).boxed())
+            .with_width(close_icon_size)
+            .named("tab-icon")
     }
 }