Merge pull request #1515 from zed-industries/drag-and-drop

K Simmons created

Drag and drop

Change summary

Cargo.lock                                      |   9 
crates/contacts_panel/src/contacts_panel.rs     |   2 
crates/drag_and_drop/Cargo.toml                 |  15 
crates/drag_and_drop/src/drag_and_drop.rs       | 154 +++
crates/gpui/src/app.rs                          |  33 
crates/gpui/src/elements/container.rs           |  25 
crates/gpui/src/elements/list.rs                |   4 
crates/gpui/src/elements/mouse_event_handler.rs |  71 +
crates/gpui/src/elements/tooltip.rs             |   9 
crates/gpui/src/presenter.rs                    | 503 +++++----
crates/gpui/src/presenter/event_context.rs      | 100 +
crates/gpui/src/presenter/event_dispatcher.rs   | 308 ++++++
crates/gpui/src/scene.rs                        |   2 
crates/gpui/src/scene/mouse_region.rs           | 198 +--
crates/gpui/src/scene/mouse_region_event.rs     | 233 ++++
crates/project_panel/src/project_panel.rs       |  58 
crates/terminal/src/terminal.rs                 |  22 
crates/terminal/src/terminal_element.rs         |  42 
crates/theme/src/theme.rs                       |  18 
crates/workspace/Cargo.toml                     |   1 
crates/workspace/src/drag_and_drop.rs           |   0 
crates/workspace/src/pane.rs                    | 959 ++++++++++++++----
crates/workspace/src/sidebar.rs                 |  32 
crates/workspace/src/workspace.rs               |  45 
styles/package-lock.json                        |   1 
styles/src/styleTree/components.ts              |   8 
styles/src/styleTree/tabBar.ts                  |  18 
27 files changed, 2,118 insertions(+), 752 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1571,6 +1571,14 @@ version = "0.15.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
 
+[[package]]
+name = "drag_and_drop"
+version = "0.1.0"
+dependencies = [
+ "collections",
+ "gpui",
+]
+
 [[package]]
 name = "dwrote"
 version = "0.11.0"
@@ -6935,6 +6943,7 @@ dependencies = [
  "clock",
  "collections",
  "context_menu",
+ "drag_and_drop",
  "futures",
  "gpui",
  "language",

crates/contacts_panel/src/contacts_panel.rs 🔗

@@ -566,7 +566,7 @@ impl ContactsPanel {
                         button
                             .with_cursor_style(CursorStyle::PointingHand)
                             .on_click(MouseButton::Left, move |_, cx| {
-                                let project = project_handle.upgrade(cx.deref_mut());
+                                let project = project_handle.upgrade(cx.app);
                                 cx.dispatch_action(ToggleProjectOnline { project })
                             })
                             .with_tooltip::<ToggleOnline, _>(

crates/drag_and_drop/Cargo.toml 🔗

@@ -0,0 +1,15 @@
+[package]
+name = "drag_and_drop"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+path = "src/drag_and_drop.rs"
+doctest = false
+
+[dependencies]
+collections = { path = "../collections" }
+gpui = { path = "../gpui" }
+
+[dev-dependencies]
+gpui = { path = "../gpui", features = ["test-support"] }

crates/drag_and_drop/src/drag_and_drop.rs 🔗

@@ -0,0 +1,154 @@
+use std::{any::Any, rc::Rc};
+
+use gpui::{
+    elements::{Container, MouseEventHandler},
+    geometry::vector::Vector2F,
+    scene::DragRegionEvent,
+    Element, ElementBox, EventContext, MouseButton, RenderContext, View, ViewContext,
+    WeakViewHandle,
+};
+
+struct State<V: View> {
+    position: Vector2F,
+    region_offset: Vector2F,
+    payload: Rc<dyn Any + 'static>,
+    render: Rc<dyn Fn(Rc<dyn Any>, &mut RenderContext<V>) -> ElementBox>,
+}
+
+impl<V: View> Clone for State<V> {
+    fn clone(&self) -> Self {
+        Self {
+            position: self.position.clone(),
+            region_offset: self.region_offset.clone(),
+            payload: self.payload.clone(),
+            render: self.render.clone(),
+        }
+    }
+}
+
+pub struct DragAndDrop<V: View> {
+    parent: WeakViewHandle<V>,
+    currently_dragged: Option<State<V>>,
+}
+
+impl<V: View> DragAndDrop<V> {
+    pub fn new(parent: WeakViewHandle<V>, cx: &mut ViewContext<V>) -> Self {
+        // TODO: Figure out if detaching here would result in a memory leak
+        cx.observe_global::<Self, _>(|cx| {
+            if let Some(parent) = cx.global::<Self>().parent.upgrade(cx) {
+                parent.update(cx, |_, cx| cx.notify())
+            }
+        })
+        .detach();
+
+        Self {
+            parent,
+            currently_dragged: None,
+        }
+    }
+
+    pub fn currently_dragged<T: Any>(&self) -> Option<(Vector2F, Rc<T>)> {
+        self.currently_dragged.as_ref().and_then(
+            |State {
+                 position, payload, ..
+             }| {
+                payload
+                    .clone()
+                    .downcast::<T>()
+                    .ok()
+                    .map(|payload| (position.clone(), payload))
+            },
+        )
+    }
+
+    pub fn dragging<T: Any>(
+        event: DragRegionEvent,
+        payload: Rc<T>,
+        cx: &mut EventContext,
+        render: Rc<impl 'static + Fn(&T, &mut RenderContext<V>) -> ElementBox>,
+    ) {
+        cx.update_global::<Self, _, _>(|this, cx| {
+            let region_offset = if let Some(previous_state) = this.currently_dragged.as_ref() {
+                previous_state.region_offset
+            } else {
+                event.region.origin() - event.prev_mouse_position
+            };
+
+            this.currently_dragged = Some(State {
+                region_offset,
+                position: event.position,
+                payload,
+                render: Rc::new(move |payload, cx| {
+                    render(payload.downcast_ref::<T>().unwrap(), cx)
+                }),
+            });
+
+            if let Some(parent) = this.parent.upgrade(cx) {
+                parent.update(cx, |_, cx| cx.notify())
+            }
+        });
+    }
+
+    pub fn render(cx: &mut RenderContext<V>) -> Option<ElementBox> {
+        let currently_dragged = cx.global::<Self>().currently_dragged.clone();
+
+        currently_dragged.map(
+            |State {
+                 region_offset,
+                 position,
+                 payload,
+                 render,
+             }| {
+                let position = position + region_offset;
+
+                MouseEventHandler::new::<Self, _, _>(0, cx, |_, cx| {
+                    Container::new(render(payload, cx))
+                        .with_margin_left(position.x())
+                        .with_margin_top(position.y())
+                        .aligned()
+                        .top()
+                        .left()
+                        .boxed()
+                })
+                .on_up(MouseButton::Left, |_, cx| {
+                    cx.defer(|cx| {
+                        cx.update_global::<Self, _, _>(|this, _| this.currently_dragged.take());
+                    });
+                    cx.propogate_event();
+                })
+                // Don't block hover events or invalidations
+                .with_hoverable(false)
+                .boxed()
+            },
+        )
+    }
+}
+
+pub trait Draggable {
+    fn as_draggable<V: View, P: Any>(
+        self,
+        payload: P,
+        render: impl 'static + Fn(&P, &mut RenderContext<V>) -> ElementBox,
+    ) -> Self
+    where
+        Self: Sized;
+}
+
+impl Draggable for MouseEventHandler {
+    fn as_draggable<V: View, P: Any>(
+        self,
+        payload: P,
+        render: impl 'static + Fn(&P, &mut RenderContext<V>) -> ElementBox,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        let payload = Rc::new(payload);
+        let render = Rc::new(render);
+        self.on_drag(MouseButton::Left, move |e, cx| {
+            let payload = payload.clone();
+            let render = render.clone();
+            DragAndDrop::<V>::dragging(e, payload, cx, render)
+        })
+    }
+}

crates/gpui/src/app.rs 🔗

@@ -9,7 +9,7 @@ use crate::{
     platform::{self, KeyDownEvent, Platform, PromptLevel, WindowOptions},
     presenter::Presenter,
     util::post_inc,
-    AssetCache, AssetSource, ClipboardItem, FontCache, InputHandler, MouseRegionId,
+    AssetCache, AssetSource, ClipboardItem, FontCache, InputHandler, MouseButton, MouseRegionId,
     PathPromptOptions, TextLayoutCache,
 };
 pub use action::*;
@@ -490,6 +490,7 @@ impl TestAppContext {
                     keystroke: keystroke.clone(),
                     is_held,
                 }),
+                false,
                 cx,
             ) {
                 return true;
@@ -576,8 +577,7 @@ impl TestAppContext {
                 view_type: PhantomData,
                 titlebar_height: 0.,
                 hovered_region_ids: Default::default(),
-                clicked_region_id: None,
-                right_clicked_region_id: None,
+                clicked_region_ids: None,
                 refreshing: false,
             };
             f(view, &mut render_cx)
@@ -1285,8 +1285,7 @@ impl MutableAppContext {
                         view_id,
                         titlebar_height,
                         hovered_region_ids: Default::default(),
-                        clicked_region_id: None,
-                        right_clicked_region_id: None,
+                        clicked_region_ids: None,
                         refreshing: false,
                     })
                     .unwrap(),
@@ -1970,7 +1969,7 @@ impl MutableAppContext {
                             }
                         }
 
-                        presenter.borrow_mut().dispatch_event(event, cx)
+                        presenter.borrow_mut().dispatch_event(event, false, cx)
                     } else {
                         false
                     }
@@ -4029,8 +4028,7 @@ pub struct RenderParams {
     pub view_id: usize,
     pub titlebar_height: f32,
     pub hovered_region_ids: HashSet<MouseRegionId>,
-    pub clicked_region_id: Option<MouseRegionId>,
-    pub right_clicked_region_id: Option<MouseRegionId>,
+    pub clicked_region_ids: Option<(Vec<MouseRegionId>, MouseButton)>,
     pub refreshing: bool,
 }
 
@@ -4039,8 +4037,7 @@ pub struct RenderContext<'a, T: View> {
     pub(crate) view_id: usize,
     pub(crate) view_type: PhantomData<T>,
     pub(crate) hovered_region_ids: HashSet<MouseRegionId>,
-    pub(crate) clicked_region_id: Option<MouseRegionId>,
-    pub(crate) right_clicked_region_id: Option<MouseRegionId>,
+    pub(crate) clicked_region_ids: Option<(Vec<MouseRegionId>, MouseButton)>,
     pub app: &'a mut MutableAppContext,
     pub titlebar_height: f32,
     pub refreshing: bool,
@@ -4049,8 +4046,7 @@ pub struct RenderContext<'a, T: View> {
 #[derive(Clone, Copy, Default)]
 pub struct MouseState {
     pub hovered: bool,
-    pub clicked: bool,
-    pub right_clicked: bool,
+    pub clicked: Option<MouseButton>,
 }
 
 impl<'a, V: View> RenderContext<'a, V> {
@@ -4062,8 +4058,7 @@ impl<'a, V: View> RenderContext<'a, V> {
             view_type: PhantomData,
             titlebar_height: params.titlebar_height,
             hovered_region_ids: params.hovered_region_ids.clone(),
-            clicked_region_id: params.clicked_region_id,
-            right_clicked_region_id: params.right_clicked_region_id,
+            clicked_region_ids: params.clicked_region_ids.clone(),
             refreshing: params.refreshing,
         }
     }
@@ -4087,8 +4082,13 @@ impl<'a, V: View> RenderContext<'a, V> {
         };
         MouseState {
             hovered: self.hovered_region_ids.contains(&region_id),
-            clicked: self.clicked_region_id == Some(region_id),
-            right_clicked: self.right_clicked_region_id == Some(region_id),
+            clicked: self.clicked_region_ids.as_ref().and_then(|(ids, button)| {
+                if ids.contains(&region_id) {
+                    Some(*button)
+                } else {
+                    None
+                }
+            }),
         }
     }
 
@@ -6041,6 +6041,7 @@ mod tests {
                 cmd: false,
                 click_count: 1,
             }),
+            false,
             cx,
         );
         assert_eq!(mouse_down_count.load(SeqCst), 1);

crates/gpui/src/elements/container.rs 🔗

@@ -24,6 +24,8 @@ pub struct ContainerStyle {
     pub padding: Padding,
     #[serde(rename = "background")]
     pub background_color: Option<Color>,
+    #[serde(rename = "overlay")]
+    pub overlay_color: Option<Color>,
     #[serde(default)]
     pub border: Border,
     #[serde(default)]
@@ -104,6 +106,11 @@ impl Container {
         self
     }
 
+    pub fn with_padding_top(mut self, padding: f32) -> Self {
+        self.style.padding.top = padding;
+        self
+    }
+
     pub fn with_padding_bottom(mut self, padding: f32) -> Self {
         self.style.padding.bottom = padding;
         self
@@ -114,6 +121,11 @@ impl Container {
         self
     }
 
+    pub fn with_overlay_color(mut self, color: Color) -> Self {
+        self.style.overlay_color = Some(color);
+        self
+    }
+
     pub fn with_border(mut self, border: Border) -> Self {
         self.style.border = border;
         self
@@ -240,7 +252,7 @@ impl Element for Container {
             cx.scene.push_layer(None);
             cx.scene.push_quad(Quad {
                 bounds: quad_bounds,
-                background: Default::default(),
+                background: self.style.overlay_color,
                 border: self.style.border,
                 corner_radius: self.style.corner_radius,
             });
@@ -259,6 +271,17 @@ impl Element for Container {
                     self.style.border.top_width(),
                 );
             self.child.paint(child_origin, visible_bounds, cx);
+
+            if self.style.overlay_color.is_some() {
+                cx.scene.push_layer(None);
+                cx.scene.push_quad(Quad {
+                    bounds: quad_bounds,
+                    background: self.style.overlay_color,
+                    border: Default::default(),
+                    corner_radius: 0.,
+                });
+                cx.scene.pop_layer();
+            }
         }
     }
 

crates/gpui/src/elements/list.rs 🔗

@@ -699,7 +699,7 @@ mod tests {
             40.,
             vec2f(0., -54.),
             true,
-            &mut presenter.build_event_context(cx),
+            &mut presenter.build_event_context(&mut Default::default(), cx),
         );
         let (_, logical_scroll_top) = list.layout(
             constraint,
@@ -808,7 +808,7 @@ mod tests {
                         height,
                         delta,
                         true,
-                        &mut presenter.build_event_context(cx),
+                        &mut presenter.build_event_context(&mut Default::default(), cx),
                     );
                 }
                 30..=34 => {

crates/gpui/src/elements/mouse_event_handler.rs 🔗

@@ -5,10 +5,12 @@ use crate::{
         vector::{vec2f, Vector2F},
     },
     platform::CursorStyle,
-    scene::{CursorRegion, HandlerSet},
+    scene::{
+        ClickRegionEvent, CursorRegion, DownOutRegionEvent, DownRegionEvent, DragRegionEvent,
+        HandlerSet, HoverRegionEvent, MoveRegionEvent, UpOutRegionEvent, UpRegionEvent,
+    },
     DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, MeasurementContext,
-    MouseButton, MouseButtonEvent, MouseMovedEvent, MouseRegion, MouseState, PaintContext,
-    RenderContext, SizeConstraint, View,
+    MouseButton, MouseRegion, MouseState, PaintContext, RenderContext, SizeConstraint, View,
 };
 use serde_json::json;
 use std::{any::TypeId, ops::Range};
@@ -18,6 +20,7 @@ pub struct MouseEventHandler {
     discriminant: (TypeId, usize),
     cursor_style: Option<CursorStyle>,
     handlers: HandlerSet,
+    hoverable: bool,
     padding: Padding,
 }
 
@@ -33,6 +36,7 @@ impl MouseEventHandler {
             cursor_style: None,
             discriminant: (TypeId::of::<Tag>(), id),
             handlers: Default::default(),
+            hoverable: true,
             padding: Default::default(),
         }
     }
@@ -42,19 +46,41 @@ impl MouseEventHandler {
         self
     }
 
+    pub fn capture_all(mut self) -> Self {
+        self.handlers = HandlerSet::capture_all();
+        self
+    }
+
+    pub fn on_move(
+        mut self,
+        handler: impl Fn(MoveRegionEvent, &mut EventContext) + 'static,
+    ) -> Self {
+        self.handlers = self.handlers.on_move(handler);
+        self
+    }
+
     pub fn on_down(
         mut self,
         button: MouseButton,
-        handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static,
+        handler: impl Fn(DownRegionEvent, &mut EventContext) + 'static,
     ) -> Self {
         self.handlers = self.handlers.on_down(button, handler);
         self
     }
 
+    pub fn on_up(
+        mut self,
+        button: MouseButton,
+        handler: impl Fn(UpRegionEvent, &mut EventContext) + 'static,
+    ) -> Self {
+        self.handlers = self.handlers.on_up(button, handler);
+        self
+    }
+
     pub fn on_click(
         mut self,
         button: MouseButton,
-        handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static,
+        handler: impl Fn(ClickRegionEvent, &mut EventContext) + 'static,
     ) -> Self {
         self.handlers = self.handlers.on_click(button, handler);
         self
@@ -63,16 +89,25 @@ impl MouseEventHandler {
     pub fn on_down_out(
         mut self,
         button: MouseButton,
-        handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static,
+        handler: impl Fn(DownOutRegionEvent, &mut EventContext) + 'static,
     ) -> Self {
         self.handlers = self.handlers.on_down_out(button, handler);
         self
     }
 
+    pub fn on_up_out(
+        mut self,
+        button: MouseButton,
+        handler: impl Fn(UpOutRegionEvent, &mut EventContext) + 'static,
+    ) -> Self {
+        self.handlers = self.handlers.on_up_out(button, handler);
+        self
+    }
+
     pub fn on_drag(
         mut self,
         button: MouseButton,
-        handler: impl Fn(Vector2F, MouseMovedEvent, &mut EventContext) + 'static,
+        handler: impl Fn(DragRegionEvent, &mut EventContext) + 'static,
     ) -> Self {
         self.handlers = self.handlers.on_drag(button, handler);
         self
@@ -80,12 +115,17 @@ impl MouseEventHandler {
 
     pub fn on_hover(
         mut self,
-        handler: impl Fn(bool, MouseMovedEvent, &mut EventContext) + 'static,
+        handler: impl Fn(HoverRegionEvent, &mut EventContext) + 'static,
     ) -> Self {
         self.handlers = self.handlers.on_hover(handler);
         self
     }
 
+    pub fn with_hoverable(mut self, is_hoverable: bool) -> Self {
+        self.hoverable = is_hoverable;
+        self
+    }
+
     pub fn with_padding(mut self, padding: Padding) -> Self {
         self.padding = padding;
         self
@@ -127,12 +167,15 @@ impl Element for MouseEventHandler {
             });
         }
 
-        cx.scene.push_mouse_region(MouseRegion::from_handlers(
-            cx.current_view_id(),
-            Some(self.discriminant),
-            hit_bounds,
-            self.handlers.clone(),
-        ));
+        cx.scene.push_mouse_region(
+            MouseRegion::from_handlers(
+                cx.current_view_id(),
+                Some(self.discriminant),
+                hit_bounds,
+                self.handlers.clone(),
+            )
+            .with_hoverable(self.hoverable),
+        );
 
         self.child.paint(bounds.origin(), visible_bounds, cx);
     }

crates/gpui/src/elements/tooltip.rs 🔗

@@ -7,8 +7,8 @@ use crate::{
     geometry::{rect::RectF, vector::Vector2F},
     json::json,
     presenter::MeasurementContext,
-    Action, Axis, ElementStateHandle, LayoutContext, MouseMovedEvent, PaintContext, RenderContext,
-    SizeConstraint, Task, View,
+    Action, Axis, ElementStateHandle, LayoutContext, PaintContext, RenderContext, SizeConstraint,
+    Task, View,
 };
 use serde::Deserialize;
 use std::{
@@ -93,10 +93,11 @@ impl Tooltip {
         };
         let child =
             MouseEventHandler::new::<MouseEventHandlerState<Tag>, _, _>(id, cx, |_, _| child)
-                .on_hover(move |hover, MouseMovedEvent { position, .. }, cx| {
+                .on_hover(move |e, cx| {
+                    let position = e.position;
                     let window_id = cx.window_id();
                     if let Some(view_id) = cx.view_id() {
-                        if hover {
+                        if e.started {
                             if !state.visible.get() {
                                 state.position.set(position);
 

crates/gpui/src/presenter.rs 🔗

@@ -6,12 +6,15 @@ use crate::{
     json::{self, ToJson},
     keymap::Keystroke,
     platform::{CursorStyle, Event},
-    scene::{CursorRegion, MouseRegionEvent},
+    scene::{
+        ClickRegionEvent, CursorRegion, DownOutRegionEvent, DownRegionEvent, DragRegionEvent,
+        HoverRegionEvent, MouseRegionEvent, MoveRegionEvent, UpOutRegionEvent, UpRegionEvent,
+    },
     text_layout::TextLayoutCache,
     Action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AssetCache, ElementBox, Entity,
-    FontSystem, ModelHandle, MouseButtonEvent, MouseMovedEvent, MouseRegion, MouseRegionId,
-    ParentId, ReadModel, ReadView, RenderContext, RenderParams, Scene, UpgradeModelHandle,
-    UpgradeViewHandle, View, ViewHandle, WeakModelHandle, WeakViewHandle,
+    FontSystem, ModelHandle, MouseButton, MouseMovedEvent, MouseRegion, MouseRegionId, ParentId,
+    ReadModel, ReadView, RenderContext, RenderParams, Scene, UpgradeModelHandle, UpgradeViewHandle,
+    View, ViewHandle, WeakModelHandle, WeakViewHandle,
 };
 use collections::{HashMap, HashSet};
 use pathfinder_geometry::vector::{vec2f, Vector2F};
@@ -31,11 +34,11 @@ pub struct Presenter {
     font_cache: Arc<FontCache>,
     text_layout_cache: TextLayoutCache,
     asset_cache: Arc<AssetCache>,
-    last_mouse_moved_event: Option<MouseMovedEvent>,
+    last_mouse_moved_event: Option<Event>,
     hovered_region_ids: HashSet<MouseRegionId>,
-    clicked_region: Option<MouseRegion>,
-    right_clicked_region: Option<MouseRegion>,
-    prev_drag_position: Option<Vector2F>,
+    clicked_regions: Vec<MouseRegion>,
+    clicked_button: Option<MouseButton>,
+    mouse_position: Vector2F,
     titlebar_height: f32,
 }
 
@@ -58,30 +61,13 @@ impl Presenter {
             asset_cache,
             last_mouse_moved_event: None,
             hovered_region_ids: Default::default(),
-            clicked_region: None,
-            right_clicked_region: None,
-            prev_drag_position: None,
+            clicked_regions: Vec::new(),
+            clicked_button: None,
+            mouse_position: vec2f(0., 0.),
             titlebar_height,
         }
     }
 
-    // pub fn dispatch_path(&self, app: &AppContext) -> Vec<usize> {
-    //     let mut path = Vec::new();
-    //     if let Some(view_id) = app.focused_view_id(self.window_id) {
-    //         self.compute_dispatch_path_from(view_id, &mut path)
-    //     }
-    //     path
-    // }
-
-    // pub(crate) fn compute_dispatch_path_from(&self, mut view_id: usize, path: &mut Vec<usize>) {
-    //     path.push(view_id);
-    //     while let Some(parent_id) = self.parents.get(&view_id).copied() {
-    //         path.push(parent_id);
-    //         view_id = parent_id;
-    //     }
-    //     path.reverse();
-    // }
-
     pub fn invalidate(
         &mut self,
         invalidation: &mut WindowInvalidation,
@@ -100,11 +86,15 @@ impl Presenter {
                     view_id: *view_id,
                     titlebar_height: self.titlebar_height,
                     hovered_region_ids: self.hovered_region_ids.clone(),
-                    clicked_region_id: self.clicked_region.as_ref().and_then(MouseRegion::id),
-                    right_clicked_region_id: self
-                        .right_clicked_region
-                        .as_ref()
-                        .and_then(MouseRegion::id),
+                    clicked_region_ids: self.clicked_button.map(|button| {
+                        (
+                            self.clicked_regions
+                                .iter()
+                                .filter_map(MouseRegion::id)
+                                .collect(),
+                            button,
+                        )
+                    }),
                     refreshing: false,
                 })
                 .unwrap(),
@@ -122,11 +112,15 @@ impl Presenter {
                         view_id: *view_id,
                         titlebar_height: self.titlebar_height,
                         hovered_region_ids: self.hovered_region_ids.clone(),
-                        clicked_region_id: self.clicked_region.as_ref().and_then(MouseRegion::id),
-                        right_clicked_region_id: self
-                            .right_clicked_region
-                            .as_ref()
-                            .and_then(MouseRegion::id),
+                        clicked_region_ids: self.clicked_button.map(|button| {
+                            (
+                                self.clicked_regions
+                                    .iter()
+                                    .filter_map(MouseRegion::id)
+                                    .collect(),
+                                button,
+                            )
+                        }),
                         refreshing: true,
                     })
                     .unwrap();
@@ -157,12 +151,7 @@ impl Presenter {
 
             if cx.window_is_active(self.window_id) {
                 if let Some(event) = self.last_mouse_moved_event.clone() {
-                    let mut invalidated_views = Vec::new();
-                    self.handle_hover_events(&event, &mut invalidated_views, cx);
-
-                    for view_id in invalidated_views {
-                        cx.notify_view(self.window_id, view_id);
-                    }
+                    self.dispatch_event(event, true, cx);
                 }
             }
         } else {
@@ -195,8 +184,15 @@ impl Presenter {
             view_stack: Vec::new(),
             refreshing,
             hovered_region_ids: self.hovered_region_ids.clone(),
-            clicked_region_id: self.clicked_region.as_ref().and_then(MouseRegion::id),
-            right_clicked_region_id: self.right_clicked_region.as_ref().and_then(MouseRegion::id),
+            clicked_region_ids: self.clicked_button.map(|button| {
+                (
+                    self.clicked_regions
+                        .iter()
+                        .filter_map(MouseRegion::id)
+                        .collect(),
+                    button,
+                )
+            }),
             titlebar_height: self.titlebar_height,
             window_size,
             app: cx,
@@ -231,246 +227,249 @@ impl Presenter {
         })
     }
 
-    pub fn dispatch_event(&mut self, event: Event, cx: &mut MutableAppContext) -> bool {
+    pub fn dispatch_event(
+        &mut self,
+        event: Event,
+        event_reused: bool,
+        cx: &mut MutableAppContext,
+    ) -> bool {
         if let Some(root_view_id) = cx.root_view_id(self.window_id) {
-            let mut invalidated_views = Vec::new();
-            let mut mouse_down_out_handlers = Vec::new();
-            let mut mouse_moved_region = None;
-            let mut mouse_down_region = None;
-            let mut mouse_up_region = None;
-            let mut clicked_region = None;
-            let mut dragged_region = None;
+            let mut events_to_send = Vec::new();
 
+            // 1. Allocate the correct set of GPUI events generated from the platform events
+            //  -> These are usually small: [Mouse Down] or [Mouse up, Click] or [Mouse Moved, Mouse Dragged?]
+            //  -> Also moves around mouse related state
             match &event {
-                Event::MouseDown(
-                    e @ MouseButtonEvent {
-                        position, button, ..
-                    },
-                ) => {
-                    let mut hit = false;
-                    for (region, _) in self.mouse_regions.iter().rev() {
-                        if region.bounds.contains_point(*position) {
-                            if !hit {
-                                hit = true;
-                                invalidated_views.push(region.view_id);
-                                mouse_down_region =
-                                    Some((region.clone(), MouseRegionEvent::Down(e.clone())));
-                                self.clicked_region = Some(region.clone());
-                                self.prev_drag_position = Some(*position);
-                            }
-                        } else if let Some(handler) = region
-                            .handlers
-                            .get(&(MouseRegionEvent::down_out_disc(), Some(*button)))
-                        {
-                            mouse_down_out_handlers.push((
-                                handler,
-                                region.view_id,
-                                MouseRegionEvent::DownOut(e.clone()),
-                            ));
-                        }
+                Event::MouseDown(e) => {
+                    // Click events are weird because they can be fired after a drag event.
+                    // MDN says that browsers handle this by starting from 'the most
+                    // specific ancestor element that contained both [positions]'
+                    // So we need to store the overlapping regions on mouse down.
+
+                    // If there is already clicked_button stored, don't replace it.
+                    if self.clicked_button.is_none() {
+                        self.clicked_regions = self
+                            .mouse_regions
+                            .iter()
+                            .filter_map(|(region, _)| {
+                                region
+                                    .bounds
+                                    .contains_point(e.position)
+                                    .then(|| region.clone())
+                            })
+                            .collect();
+                        self.clicked_button = Some(e.button);
                     }
-                }
 
-                &Event::MouseUp(
-                    e @ MouseButtonEvent {
-                        position, button, ..
+                    events_to_send.push(MouseRegionEvent::Down(DownRegionEvent {
+                        region: Default::default(),
+                        platform_event: e.clone(),
+                    }));
+                    events_to_send.push(MouseRegionEvent::DownOut(DownOutRegionEvent {
+                        region: Default::default(),
+                        platform_event: e.clone(),
+                    }));
+                }
+                Event::MouseUp(e) => {
+                    // NOTE: The order of event pushes is important! MouseUp events MUST be fired
+                    // before click events, and so the UpRegionEvent events need to be pushed before
+                    // ClickRegionEvents
+                    events_to_send.push(MouseRegionEvent::Up(UpRegionEvent {
+                        region: Default::default(),
+                        platform_event: e.clone(),
+                    }));
+                    events_to_send.push(MouseRegionEvent::UpOut(UpOutRegionEvent {
+                        region: Default::default(),
+                        platform_event: e.clone(),
+                    }));
+                    events_to_send.push(MouseRegionEvent::Click(ClickRegionEvent {
+                        region: Default::default(),
+                        platform_event: e.clone(),
+                    }));
+                }
+                Event::MouseMoved(
+                    e @ MouseMovedEvent {
+                        position,
+                        pressed_button,
+                        ..
                     },
                 ) => {
-                    self.prev_drag_position.take();
-                    if let Some(region) = self.clicked_region.take() {
-                        invalidated_views.push(region.view_id);
-                        if region.bounds.contains_point(position) {
-                            clicked_region = Some((region, MouseRegionEvent::Click(e.clone())));
-                        }
-                    }
-
-                    for (region, _) in self.mouse_regions.iter().rev() {
-                        if region.bounds.contains_point(position) {
-                            invalidated_views.push(region.view_id);
-                            mouse_up_region =
-                                Some((region.clone(), MouseRegionEvent::Up(e.clone())));
+                    let mut style_to_assign = CursorStyle::Arrow;
+                    for region in self.cursor_regions.iter().rev() {
+                        if region.bounds.contains_point(*position) {
+                            style_to_assign = region.style;
                             break;
                         }
                     }
-
-                    if let Some(moved) = &mut self.last_mouse_moved_event {
-                        if moved.pressed_button == Some(button) {
-                            moved.pressed_button = None;
+                    cx.platform().set_cursor_style(style_to_assign);
+
+                    if !event_reused {
+                        if pressed_button.is_some() {
+                            events_to_send.push(MouseRegionEvent::Drag(DragRegionEvent {
+                                region: Default::default(),
+                                prev_mouse_position: self.mouse_position,
+                                platform_event: e.clone(),
+                            }));
                         }
-                    }
-                }
-
-                Event::MouseMoved(e @ MouseMovedEvent { position, .. }) => {
-                    if let Some((clicked_region, prev_drag_position)) = self
-                        .clicked_region
-                        .as_ref()
-                        .zip(self.prev_drag_position.as_mut())
-                    {
-                        dragged_region = Some((
-                            clicked_region.clone(),
-                            MouseRegionEvent::Drag(*prev_drag_position, *e),
-                        ));
-                        *prev_drag_position = *position;
+                        events_to_send.push(MouseRegionEvent::Move(MoveRegionEvent {
+                            region: Default::default(),
+                            platform_event: e.clone(),
+                        }));
                     }
 
-                    for (region, _) in self.mouse_regions.iter().rev() {
-                        if region.bounds.contains_point(*position) {
-                            invalidated_views.push(region.view_id);
-                            mouse_moved_region =
-                                Some((region.clone(), MouseRegionEvent::Move(e.clone())));
-                            break;
-                        }
-                    }
+                    events_to_send.push(MouseRegionEvent::Hover(HoverRegionEvent {
+                        region: Default::default(),
+                        platform_event: e.clone(),
+                        started: false,
+                    }));
 
-                    self.last_mouse_moved_event = Some(e.clone());
+                    self.last_mouse_moved_event = Some(event.clone());
                 }
 
                 _ => {}
             }
 
-            let (mut handled, mut event_cx) = if let Event::MouseMoved(e) = &event {
-                self.handle_hover_events(e, &mut invalidated_views, cx)
-            } else {
-                (false, self.build_event_context(cx))
-            };
-
-            for (handler, view_id, region_event) in mouse_down_out_handlers {
-                event_cx.with_current_view(view_id, |event_cx| handler(region_event, event_cx))
+            if let Some(position) = event.position() {
+                self.mouse_position = position;
             }
 
-            if let Some((mouse_down_region, region_event)) = mouse_down_region {
-                handled = true;
-                if let Some(mouse_down_callback) =
-                    mouse_down_region.handlers.get(&region_event.handler_key())
-                {
-                    event_cx.with_current_view(mouse_down_region.view_id, |event_cx| {
-                        mouse_down_callback(region_event, event_cx);
-                    })
-                }
-            }
+            let mut invalidated_views: HashSet<usize> = Default::default();
+            let mut any_event_handled = false;
+            // 2. Process the raw mouse events into region events
+            for mut region_event in events_to_send {
+                let mut valid_regions = Vec::new();
+
+                // GPUI elements are arranged by depth but sibling elements can register overlapping
+                // mouse regions. As such, hover events are only fired on overlapping elements which
+                // are at the same depth as the topmost element which overlaps with the mouse.
+
+                match &region_event {
+                    MouseRegionEvent::Hover(_) => {
+                        let mut top_most_depth = None;
+                        let mouse_position = self.mouse_position.clone();
+                        for (region, depth) in self.mouse_regions.iter().rev() {
+                            // Allow mouse regions to appear transparent to hovers
+                            if !region.hoverable {
+                                continue;
+                            }
 
-            if let Some((move_moved_region, region_event)) = mouse_moved_region {
-                handled = true;
-                if let Some(mouse_moved_callback) =
-                    move_moved_region.handlers.get(&region_event.handler_key())
-                {
-                    event_cx.with_current_view(move_moved_region.view_id, |event_cx| {
-                        mouse_moved_callback(region_event, event_cx);
-                    })
-                }
-            }
+                            let contains_mouse = region.bounds.contains_point(mouse_position);
 
-            if let Some((mouse_up_region, region_event)) = mouse_up_region {
-                handled = true;
-                if let Some(mouse_up_callback) =
-                    mouse_up_region.handlers.get(&region_event.handler_key())
-                {
-                    event_cx.with_current_view(mouse_up_region.view_id, |event_cx| {
-                        mouse_up_callback(region_event, event_cx);
-                    })
-                }
-            }
+                            if contains_mouse && top_most_depth.is_none() {
+                                top_most_depth = Some(depth);
+                            }
 
-            if let Some((clicked_region, region_event)) = clicked_region {
-                handled = true;
-                if let Some(click_callback) =
-                    clicked_region.handlers.get(&region_event.handler_key())
-                {
-                    event_cx.with_current_view(clicked_region.view_id, |event_cx| {
-                        click_callback(region_event, event_cx);
-                    })
-                }
-            }
+                            if let Some(region_id) = region.id() {
+                                // This unwrap relies on short circuiting boolean expressions
+                                // The right side of the && is only executed when contains_mouse
+                                // is true, and we know above that when contains_mouse is true
+                                // top_most_depth is set
+                                if contains_mouse && depth == top_most_depth.unwrap() {
+                                    //Ensure that hover entrance events aren't sent twice
+                                    if self.hovered_region_ids.insert(region_id) {
+                                        valid_regions.push(region.clone());
+                                        invalidated_views.insert(region.view_id);
+                                    }
+                                } else {
+                                    // Ensure that hover exit events aren't sent twice
+                                    if self.hovered_region_ids.remove(&region_id) {
+                                        valid_regions.push(region.clone());
+                                        invalidated_views.insert(region.view_id);
+                                    }
+                                }
+                            }
+                        }
+                    }
+                    MouseRegionEvent::Click(e) => {
+                        if e.button == self.clicked_button.unwrap() {
+                            // Clear clicked regions and clicked button
+                            let clicked_regions =
+                                std::mem::replace(&mut self.clicked_regions, Vec::new());
+                            self.clicked_button = None;
+
+                            // Find regions which still overlap with the mouse since the last MouseDown happened
+                            for clicked_region in clicked_regions.into_iter().rev() {
+                                if clicked_region.bounds.contains_point(e.position) {
+                                    valid_regions.push(clicked_region);
+                                }
+                            }
+                        }
+                    }
+                    MouseRegionEvent::Drag(_) => {
+                        for clicked_region in self.clicked_regions.iter().rev() {
+                            valid_regions.push(clicked_region.clone());
+                        }
+                    }
 
-            if let Some((dragged_region, region_event)) = dragged_region {
-                handled = true;
-                if let Some(drag_callback) =
-                    dragged_region.handlers.get(&region_event.handler_key())
-                {
-                    event_cx.with_current_view(dragged_region.view_id, |event_cx| {
-                        drag_callback(region_event, event_cx);
-                    })
+                    MouseRegionEvent::UpOut(_) | MouseRegionEvent::DownOut(_) => {
+                        for (mouse_region, _) in self.mouse_regions.iter().rev() {
+                            // NOT contains
+                            if !mouse_region.bounds.contains_point(self.mouse_position) {
+                                valid_regions.push(mouse_region.clone());
+                            }
+                        }
+                    }
+                    _ => {
+                        for (mouse_region, _) in self.mouse_regions.iter().rev() {
+                            // Contains
+                            if mouse_region.bounds.contains_point(self.mouse_position) {
+                                valid_regions.push(mouse_region.clone());
+                            }
+                        }
+                    }
                 }
-            }
 
-            if !handled {
-                handled = event_cx.dispatch_event(root_view_id, &event);
-            }
-
-            invalidated_views.extend(event_cx.invalidated_views);
+                //3. Fire region events
+                let hovered_region_ids = self.hovered_region_ids.clone();
+                for valid_region in valid_regions.into_iter() {
+                    region_event.set_region(valid_region.bounds);
+                    if let MouseRegionEvent::Hover(e) = &mut region_event {
+                        e.started = valid_region
+                            .id()
+                            .map(|region_id| hovered_region_ids.contains(&region_id))
+                            .unwrap_or(false)
+                    }
 
-            for view_id in invalidated_views {
-                cx.notify_view(self.window_id, view_id);
-            }
+                    if let Some(callback) = valid_region.handlers.get(&region_event.handler_key()) {
+                        invalidated_views.insert(valid_region.view_id);
 
-            handled
-        } else {
-            false
-        }
-    }
+                        let mut event_cx = self.build_event_context(&mut invalidated_views, cx);
+                        event_cx.handled = true;
+                        event_cx.with_current_view(valid_region.view_id, {
+                            let region_event = region_event.clone();
+                            |cx| {
+                                callback(region_event, cx);
+                            }
+                        });
 
-    fn handle_hover_events<'a>(
-        &'a mut self,
-        e @ MouseMovedEvent {
-            position,
-            pressed_button,
-            ..
-        }: &MouseMovedEvent,
-        invalidated_views: &mut Vec<usize>,
-        cx: &'a mut MutableAppContext,
-    ) -> (bool, EventContext<'a>) {
-        let mut hover_regions = Vec::new();
-
-        if pressed_button.is_none() {
-            let mut style_to_assign = CursorStyle::Arrow;
-            for region in self.cursor_regions.iter().rev() {
-                if region.bounds.contains_point(*position) {
-                    style_to_assign = region.style;
-                    break;
-                }
-            }
-            cx.platform().set_cursor_style(style_to_assign);
-
-            let mut hover_depth = None;
-            for (region, depth) in self.mouse_regions.iter().rev() {
-                if region.bounds.contains_point(*position)
-                    && hover_depth.map_or(true, |hover_depth| hover_depth == *depth)
-                {
-                    hover_depth = Some(*depth);
-                    if let Some(region_id) = region.id() {
-                        if !self.hovered_region_ids.contains(&region_id) {
-                            invalidated_views.push(region.view_id);
-                            hover_regions.push((region.clone(), MouseRegionEvent::Hover(true, *e)));
-                            self.hovered_region_ids.insert(region_id);
+                        any_event_handled = any_event_handled || event_cx.handled;
+                        // For bubbling events, if the event was handled, don't continue dispatching
+                        // This only makes sense for local events.
+                        if event_cx.handled && region_event.is_capturable() {
+                            break;
                         }
                     }
-                } else if let Some(region_id) = region.id() {
-                    if self.hovered_region_ids.contains(&region_id) {
-                        invalidated_views.push(region.view_id);
-                        hover_regions.push((region.clone(), MouseRegionEvent::Hover(false, *e)));
-                        self.hovered_region_ids.remove(&region_id);
-                    }
                 }
             }
-        }
 
-        let mut event_cx = self.build_event_context(cx);
-        let mut handled = false;
+            if !any_event_handled && !event_reused {
+                let mut event_cx = self.build_event_context(&mut invalidated_views, cx);
+                any_event_handled = event_cx.dispatch_event(root_view_id, &event);
+            }
 
-        for (hover_region, region_event) in hover_regions {
-            handled = true;
-            if let Some(hover_callback) = hover_region.handlers.get(&region_event.handler_key()) {
-                event_cx.with_current_view(hover_region.view_id, |event_cx| {
-                    hover_callback(region_event, event_cx);
-                })
+            for view_id in invalidated_views {
+                cx.notify_view(self.window_id, view_id);
             }
-        }
 
-        (handled, event_cx)
+            any_event_handled
+        } else {
+            false
+        }
     }
 
     pub fn build_event_context<'a>(
         &'a mut self,
+        invalidated_views: &'a mut HashSet<usize>,
         cx: &'a mut MutableAppContext,
     ) -> EventContext<'a> {
         EventContext {
@@ -478,8 +477,9 @@ impl Presenter {
             font_cache: &self.font_cache,
             text_layout_cache: &self.text_layout_cache,
             view_stack: Default::default(),
-            invalidated_views: Default::default(),
+            invalidated_views,
             notify_count: 0,
+            handled: false,
             window_id: self.window_id,
             app: cx,
         }
@@ -514,8 +514,7 @@ pub struct LayoutContext<'a> {
     pub window_size: Vector2F,
     titlebar_height: f32,
     hovered_region_ids: HashSet<MouseRegionId>,
-    clicked_region_id: Option<MouseRegionId>,
-    right_clicked_region_id: Option<MouseRegionId>,
+    clicked_region_ids: Option<(Vec<MouseRegionId>, MouseButton)>,
 }
 
 impl<'a> LayoutContext<'a> {
@@ -586,8 +585,7 @@ impl<'a> LayoutContext<'a> {
                 view_type: PhantomData,
                 titlebar_height: self.titlebar_height,
                 hovered_region_ids: self.hovered_region_ids.clone(),
-                clicked_region_id: self.clicked_region_id,
-                right_clicked_region_id: self.right_clicked_region_id,
+                clicked_region_ids: self.clicked_region_ids.clone(),
                 refreshing: self.refreshing,
             };
             f(view, &mut render_cx)
@@ -699,7 +697,8 @@ pub struct EventContext<'a> {
     pub window_id: usize,
     pub notify_count: usize,
     view_stack: Vec<usize>,
-    invalidated_views: HashSet<usize>,
+    handled: bool,
+    invalidated_views: &'a mut HashSet<usize>,
 }
 
 impl<'a> EventContext<'a> {
@@ -765,6 +764,10 @@ impl<'a> EventContext<'a> {
     pub fn notify_count(&self) -> usize {
         self.notify_count
     }
+
+    pub fn propogate_event(&mut self) {
+        self.handled = false;
+    }
 }
 
 impl<'a> Deref for EventContext<'a> {

crates/gpui/src/presenter/event_context.rs 🔗

@@ -0,0 +1,100 @@
+use std::ops::{Deref, DerefMut};
+
+use collections::{HashMap, HashSet};
+
+use crate::{Action, ElementBox, Event, FontCache, MutableAppContext, TextLayoutCache};
+
+pub struct EventContext<'a> {
+    rendered_views: &'a mut HashMap<usize, ElementBox>,
+    pub font_cache: &'a FontCache,
+    pub text_layout_cache: &'a TextLayoutCache,
+    pub app: &'a mut MutableAppContext,
+    pub window_id: usize,
+    pub notify_count: usize,
+    view_stack: Vec<usize>,
+    pub(crate) handled: bool,
+    pub(crate) invalidated_views: HashSet<usize>,
+}
+
+impl<'a> EventContext<'a> {
+    pub(crate) fn dispatch_event(&mut self, view_id: usize, event: &Event) -> bool {
+        if let Some(mut element) = self.rendered_views.remove(&view_id) {
+            let result =
+                self.with_current_view(view_id, |this| element.dispatch_event(event, this));
+            self.rendered_views.insert(view_id, element);
+            result
+        } else {
+            false
+        }
+    }
+
+    pub(crate) fn with_current_view<F, T>(&mut self, view_id: usize, f: F) -> T
+    where
+        F: FnOnce(&mut Self) -> T,
+    {
+        self.view_stack.push(view_id);
+        let result = f(self);
+        self.view_stack.pop();
+        result
+    }
+
+    pub fn window_id(&self) -> usize {
+        self.window_id
+    }
+
+    pub fn view_id(&self) -> Option<usize> {
+        self.view_stack.last().copied()
+    }
+
+    pub fn is_parent_view_focused(&self) -> bool {
+        if let Some(parent_view_id) = self.view_stack.last() {
+            self.app.focused_view_id(self.window_id) == Some(*parent_view_id)
+        } else {
+            false
+        }
+    }
+
+    pub fn focus_parent_view(&mut self) {
+        if let Some(parent_view_id) = self.view_stack.last() {
+            self.app.focus(self.window_id, Some(*parent_view_id))
+        }
+    }
+
+    pub fn dispatch_any_action(&mut self, action: Box<dyn Action>) {
+        self.app
+            .dispatch_any_action_at(self.window_id, *self.view_stack.last().unwrap(), action)
+    }
+
+    pub fn dispatch_action<A: Action>(&mut self, action: A) {
+        self.dispatch_any_action(Box::new(action));
+    }
+
+    pub fn notify(&mut self) {
+        self.notify_count += 1;
+        if let Some(view_id) = self.view_stack.last() {
+            self.invalidated_views.insert(*view_id);
+        }
+    }
+
+    pub fn notify_count(&self) -> usize {
+        self.notify_count
+    }
+
+    pub fn propogate_event(&mut self) {
+        self.handled = false;
+    }
+}
+
+impl<'a> Deref for EventContext<'a> {
+    type Target = MutableAppContext;
+
+    fn deref(&self) -> &Self::Target {
+        self.app
+    }
+}
+
+impl<'a> DerefMut for EventContext<'a> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        self.app
+    }
+}

crates/gpui/src/presenter/event_dispatcher.rs 🔗

@@ -0,0 +1,308 @@
+use std::sync::Arc;
+
+use collections::{HashMap, HashSet};
+use pathfinder_geometry::vector::Vector2F;
+
+use crate::{
+    scene::{
+        ClickRegionEvent, DownOutRegionEvent, DownRegionEvent, DragRegionEvent, HoverRegionEvent,
+        MouseRegionEvent, MoveRegionEvent, UpOutRegionEvent, UpRegionEvent,
+    },
+    CursorRegion, CursorStyle, ElementBox, Event, EventContext, FontCache, MouseButton,
+    MouseMovedEvent, MouseRegion, MouseRegionId, MutableAppContext, Scene, TextLayoutCache,
+};
+
+pub struct EventDispatcher {
+    window_id: usize,
+    font_cache: Arc<FontCache>,
+
+    last_mouse_moved_event: Option<Event>,
+    cursor_regions: Vec<CursorRegion>,
+    mouse_regions: Vec<(MouseRegion, usize)>,
+    clicked_regions: Vec<MouseRegion>,
+    clicked_button: Option<MouseButton>,
+    mouse_position: Vector2F,
+    hovered_region_ids: HashSet<MouseRegionId>,
+}
+
+impl EventDispatcher {
+    pub fn new(window_id: usize, font_cache: Arc<FontCache>) -> Self {
+        Self {
+            window_id,
+            font_cache,
+
+            last_mouse_moved_event: Default::default(),
+            cursor_regions: Default::default(),
+            mouse_regions: Default::default(),
+            clicked_regions: Default::default(),
+            clicked_button: Default::default(),
+            mouse_position: Default::default(),
+            hovered_region_ids: Default::default(),
+        }
+    }
+
+    pub fn clicked_region_ids(&self) -> Option<(Vec<MouseRegionId>, MouseButton)> {
+        self.clicked_button.map(|button| {
+            (
+                self.clicked_regions
+                    .iter()
+                    .filter_map(MouseRegion::id)
+                    .collect(),
+                button,
+            )
+        })
+    }
+
+    pub fn hovered_region_ids(&self) -> HashSet<MouseRegionId> {
+        self.hovered_region_ids.clone()
+    }
+
+    pub fn update_mouse_regions(&mut self, scene: &Scene) {
+        self.cursor_regions = scene.cursor_regions();
+        self.mouse_regions = scene.mouse_regions();
+    }
+
+    pub fn redispatch_mouse_moved_event<'a>(&'a mut self, cx: &mut EventContext<'a>) {
+        if let Some(event) = self.last_mouse_moved_event.clone() {
+            self.dispatch_event(event, true, cx);
+        }
+    }
+
+    pub fn dispatch_event<'a>(
+        &'a mut self,
+        event: Event,
+        event_reused: bool,
+        cx: &mut EventContext<'a>,
+    ) -> bool {
+        let root_view_id = cx.root_view_id(self.window_id);
+        if root_view_id.is_none() {
+            return false;
+        }
+
+        let root_view_id = root_view_id.unwrap();
+        //1. Allocate the correct set of GPUI events generated from the platform events
+        // -> These are usually small: [Mouse Down] or [Mouse up, Click] or [Mouse Moved, Mouse Dragged?]
+        // -> Also moves around mouse related state
+        let events_to_send = self.select_region_events(&event, cx, event_reused);
+
+        // For a given platform event, potentially multiple mouse region events can be created. For a given
+        // region event, dispatch continues until a mouse region callback fails to propogate (handled is set to true)
+        // If no region handles any of the produced platform events, we fallback to the old dispatch event style.
+        let mut invalidated_views: HashSet<usize> = Default::default();
+        let mut any_event_handled = false;
+        for mut region_event in events_to_send {
+            //2. Find mouse regions relevant to each region_event. For example, if the event is click, select
+            // the clicked_regions that overlap with the mouse position
+            let valid_regions = self.select_relevant_mouse_regions(&region_event);
+            let hovered_region_ids = self.hovered_region_ids.clone();
+
+            //3. Dispatch region event ot each valid mouse region
+            for valid_region in valid_regions.into_iter() {
+                region_event.set_region(valid_region.bounds);
+                if let MouseRegionEvent::Hover(e) = &mut region_event {
+                    e.started = valid_region
+                        .id()
+                        .map(|region_id| hovered_region_ids.contains(&region_id))
+                        .unwrap_or(false)
+                }
+
+                if let Some(callback) = valid_region.handlers.get(&region_event.handler_key()) {
+                    if !event_reused {
+                        invalidated_views.insert(valid_region.view_id);
+                    }
+
+                    cx.handled = true;
+                    cx.with_current_view(valid_region.view_id, {
+                        let region_event = region_event.clone();
+                        |cx| {
+                            callback(region_event, cx);
+                        }
+                    });
+
+                    // For bubbling events, if the event was handled, don't continue dispatching
+                    // This only makes sense for local events.
+                    if cx.handled && region_event.is_local() {
+                        break;
+                    }
+                }
+            }
+
+            // Keep track if any platform event was handled
+            any_event_handled = any_event_handled && cx.handled;
+        }
+
+        if !any_event_handled {
+            // No platform event was handled, so fall back to old mouse event dispatch style
+            any_event_handled = cx.dispatch_event(root_view_id, &event);
+        }
+
+        // Notify any views which have been validated from event callbacks
+        for view_id in invalidated_views {
+            cx.notify_view(self.window_id, view_id);
+        }
+
+        any_event_handled
+    }
+
+    fn select_region_events(
+        &mut self,
+        event: &Event,
+        cx: &mut MutableAppContext,
+        event_reused: bool,
+    ) -> Vec<MouseRegionEvent> {
+        let mut events_to_send = Vec::new();
+        match event {
+            Event::MouseDown(e) => {
+                //Click events are weird because they can be fired after a drag event.
+                //MDN says that browsers handle this by starting from 'the most
+                //specific ancestor element that contained both [positions]'
+                //So we need to store the overlapping regions on mouse down.
+                self.clicked_regions = self
+                    .mouse_regions
+                    .iter()
+                    .filter_map(|(region, _)| {
+                        region
+                            .bounds
+                            .contains_point(e.position)
+                            .then(|| region.clone())
+                    })
+                    .collect();
+                self.clicked_button = Some(e.button);
+
+                events_to_send.push(MouseRegionEvent::Down(DownRegionEvent {
+                    region: Default::default(),
+                    platform_event: e.clone(),
+                }));
+                events_to_send.push(MouseRegionEvent::DownOut(DownOutRegionEvent {
+                    region: Default::default(),
+                    platform_event: e.clone(),
+                }));
+            }
+            Event::MouseUp(e) => {
+                //NOTE: The order of event pushes is important! MouseUp events MUST be fired
+                //before click events, and so the UpRegionEvent events need to be pushed before
+                //ClickRegionEvents
+                events_to_send.push(MouseRegionEvent::Up(UpRegionEvent {
+                    region: Default::default(),
+                    platform_event: e.clone(),
+                }));
+                events_to_send.push(MouseRegionEvent::UpOut(UpOutRegionEvent {
+                    region: Default::default(),
+                    platform_event: e.clone(),
+                }));
+                events_to_send.push(MouseRegionEvent::Click(ClickRegionEvent {
+                    region: Default::default(),
+                    platform_event: e.clone(),
+                }));
+            }
+            Event::MouseMoved(
+                e @ MouseMovedEvent {
+                    position,
+                    pressed_button,
+                    ..
+                },
+            ) => {
+                let mut style_to_assign = CursorStyle::Arrow;
+                for region in self.cursor_regions.iter().rev() {
+                    if region.bounds.contains_point(*position) {
+                        style_to_assign = region.style;
+                        break;
+                    }
+                }
+                cx.platform().set_cursor_style(style_to_assign);
+
+                if !event_reused {
+                    if pressed_button.is_some() {
+                        events_to_send.push(MouseRegionEvent::Drag(DragRegionEvent {
+                            region: Default::default(),
+                            prev_mouse_position: self.mouse_position,
+                            platform_event: e.clone(),
+                        }));
+                    }
+                    events_to_send.push(MouseRegionEvent::Move(MoveRegionEvent {
+                        region: Default::default(),
+                        platform_event: e.clone(),
+                    }));
+                }
+
+                events_to_send.push(MouseRegionEvent::Hover(HoverRegionEvent {
+                    region: Default::default(),
+                    platform_event: e.clone(),
+                    started: false,
+                }));
+
+                self.last_mouse_moved_event = Some(event.clone());
+            }
+            _ => {}
+        }
+        if let Some(position) = event.position() {
+            self.mouse_position = position;
+        }
+        events_to_send
+    }
+
+    fn select_relevant_mouse_regions(
+        &mut self,
+        region_event: &MouseRegionEvent,
+    ) -> Vec<MouseRegion> {
+        let mut valid_regions = Vec::new();
+        //GPUI elements are arranged by depth but sibling elements can register overlapping
+        //mouse regions. As such, hover events are only fired on overlapping elements which
+        //are at the same depth as the deepest element which overlaps with the mouse.
+        if let MouseRegionEvent::Hover(_) = *region_event {
+            let mut top_most_depth = None;
+            let mouse_position = self.mouse_position.clone();
+            for (region, depth) in self.mouse_regions.iter().rev() {
+                let contains_mouse = region.bounds.contains_point(mouse_position);
+
+                if contains_mouse && top_most_depth.is_none() {
+                    top_most_depth = Some(depth);
+                }
+
+                if let Some(region_id) = region.id() {
+                    //This unwrap relies on short circuiting boolean expressions
+                    //The right side of the && is only executed when contains_mouse
+                    //is true, and we know above that when contains_mouse is true
+                    //top_most_depth is set
+                    if contains_mouse && depth == top_most_depth.unwrap() {
+                        //Ensure that hover entrance events aren't sent twice
+                        if self.hovered_region_ids.insert(region_id) {
+                            valid_regions.push(region.clone());
+                        }
+                    } else {
+                        //Ensure that hover exit events aren't sent twice
+                        if self.hovered_region_ids.remove(&region_id) {
+                            valid_regions.push(region.clone());
+                        }
+                    }
+                }
+            }
+        } else if let MouseRegionEvent::Click(e) = region_event {
+            //Clear stored clicked_regions
+            let clicked_regions = std::mem::replace(&mut self.clicked_regions, Vec::new());
+            self.clicked_button = None;
+
+            //Find regions which still overlap with the mouse since the last MouseDown happened
+            for clicked_region in clicked_regions.into_iter().rev() {
+                if clicked_region.bounds.contains_point(e.position) {
+                    valid_regions.push(clicked_region);
+                }
+            }
+        } else if region_event.is_local() {
+            for (mouse_region, _) in self.mouse_regions.iter().rev() {
+                //Contains
+                if mouse_region.bounds.contains_point(self.mouse_position) {
+                    valid_regions.push(mouse_region.clone());
+                }
+            }
+        } else {
+            for (mouse_region, _) in self.mouse_regions.iter().rev() {
+                //NOT contains
+                if !mouse_region.bounds.contains_point(self.mouse_position) {
+                    valid_regions.push(mouse_region.clone());
+                }
+            }
+        }
+        valid_regions
+    }
+}

crates/gpui/src/scene.rs 🔗

@@ -1,4 +1,5 @@
 mod mouse_region;
+mod mouse_region_event;
 
 use serde::Deserialize;
 use serde_json::json;
@@ -13,6 +14,7 @@ use crate::{
     ImageData,
 };
 pub use mouse_region::*;
+pub use mouse_region_event::*;
 
 pub struct Scene {
     scale_factor: f32,

crates/gpui/src/scene/mouse_region.rs 🔗

@@ -2,9 +2,14 @@ use std::{any::TypeId, mem::Discriminant, rc::Rc};
 
 use collections::HashMap;
 
-use pathfinder_geometry::{rect::RectF, vector::Vector2F};
+use pathfinder_geometry::rect::RectF;
 
-use crate::{EventContext, MouseButton, MouseButtonEvent, MouseMovedEvent, ScrollWheelEvent};
+use crate::{EventContext, MouseButton};
+
+use super::mouse_region_event::{
+    ClickRegionEvent, DownOutRegionEvent, DownRegionEvent, DragRegionEvent, HoverRegionEvent,
+    MouseRegionEvent, MoveRegionEvent, UpOutRegionEvent, UpRegionEvent,
+};
 
 #[derive(Clone, Default)]
 pub struct MouseRegion {
@@ -12,6 +17,7 @@ pub struct MouseRegion {
     pub discriminant: Option<(TypeId, usize)>,
     pub bounds: RectF,
     pub handlers: HandlerSet,
+    pub hoverable: bool,
 }
 
 impl MouseRegion {
@@ -30,6 +36,7 @@ impl MouseRegion {
             discriminant,
             bounds,
             handlers,
+            hoverable: true,
         }
     }
 
@@ -42,14 +49,15 @@ impl MouseRegion {
             view_id,
             discriminant,
             bounds,
-            handlers: HandlerSet::handle_all(),
+            handlers: HandlerSet::capture_all(),
+            hoverable: true,
         }
     }
 
     pub fn on_down(
         mut self,
         button: MouseButton,
-        handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static,
+        handler: impl Fn(DownRegionEvent, &mut EventContext) + 'static,
     ) -> Self {
         self.handlers = self.handlers.on_down(button, handler);
         self
@@ -58,7 +66,7 @@ impl MouseRegion {
     pub fn on_up(
         mut self,
         button: MouseButton,
-        handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static,
+        handler: impl Fn(UpRegionEvent, &mut EventContext) + 'static,
     ) -> Self {
         self.handlers = self.handlers.on_up(button, handler);
         self
@@ -67,7 +75,7 @@ impl MouseRegion {
     pub fn on_click(
         mut self,
         button: MouseButton,
-        handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static,
+        handler: impl Fn(ClickRegionEvent, &mut EventContext) + 'static,
     ) -> Self {
         self.handlers = self.handlers.on_click(button, handler);
         self
@@ -76,16 +84,25 @@ impl MouseRegion {
     pub fn on_down_out(
         mut self,
         button: MouseButton,
-        handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static,
+        handler: impl Fn(DownOutRegionEvent, &mut EventContext) + 'static,
     ) -> Self {
         self.handlers = self.handlers.on_down_out(button, handler);
         self
     }
 
+    pub fn on_up_out(
+        mut self,
+        button: MouseButton,
+        handler: impl Fn(UpOutRegionEvent, &mut EventContext) + 'static,
+    ) -> Self {
+        self.handlers = self.handlers.on_up_out(button, handler);
+        self
+    }
+
     pub fn on_drag(
         mut self,
         button: MouseButton,
-        handler: impl Fn(Vector2F, MouseMovedEvent, &mut EventContext) + 'static,
+        handler: impl Fn(DragRegionEvent, &mut EventContext) + 'static,
     ) -> Self {
         self.handlers = self.handlers.on_drag(button, handler);
         self
@@ -93,7 +110,7 @@ impl MouseRegion {
 
     pub fn on_hover(
         mut self,
-        handler: impl Fn(bool, MouseMovedEvent, &mut EventContext) + 'static,
+        handler: impl Fn(HoverRegionEvent, &mut EventContext) + 'static,
     ) -> Self {
         self.handlers = self.handlers.on_hover(handler);
         self
@@ -101,14 +118,19 @@ impl MouseRegion {
 
     pub fn on_move(
         mut self,
-        handler: impl Fn(MouseMovedEvent, &mut EventContext) + 'static,
+        handler: impl Fn(MoveRegionEvent, &mut EventContext) + 'static,
     ) -> Self {
         self.handlers = self.handlers.on_move(handler);
         self
     }
+
+    pub fn with_hoverable(mut self, is_hoverable: bool) -> Self {
+        self.hoverable = is_hoverable;
+        self
+    }
 }
 
-#[derive(Copy, Clone, Eq, PartialEq, Hash)]
+#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
 pub struct MouseRegionId {
     pub view_id: usize,
     pub discriminant: (TypeId, usize),
@@ -124,7 +146,7 @@ pub struct HandlerSet {
 }
 
 impl HandlerSet {
-    pub fn handle_all() -> Self {
+    pub fn capture_all() -> Self {
         #[allow(clippy::type_complexity)]
         let mut set: HashMap<
             (Discriminant<MouseRegionEvent>, Option<MouseButton>),
@@ -154,6 +176,10 @@ impl HandlerSet {
                 (MouseRegionEvent::down_out_disc(), Some(button)),
                 Rc::new(|_, _| {}),
             );
+            set.insert(
+                (MouseRegionEvent::up_out_disc(), Some(button)),
+                Rc::new(|_, _| {}),
+            );
         }
         set.insert(
             (MouseRegionEvent::scroll_wheel_disc(), None),
@@ -170,15 +196,32 @@ impl HandlerSet {
         self.set.get(key).cloned()
     }
 
+    pub fn on_move(
+        mut self,
+        handler: impl Fn(MoveRegionEvent, &mut EventContext) + 'static,
+    ) -> Self {
+        self.set.insert((MouseRegionEvent::move_disc(), None),
+            Rc::new(move |region_event, cx| {
+                if let MouseRegionEvent::Move(e) = region_event {
+                    handler(e, cx);
+                } else {
+                    panic!(
+                        "Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::Move, found {:?}", 
+                        region_event);
+                }
+            }));
+        self
+    }
+
     pub fn on_down(
         mut self,
         button: MouseButton,
-        handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static,
+        handler: impl Fn(DownRegionEvent, &mut EventContext) + 'static,
     ) -> Self {
         self.set.insert((MouseRegionEvent::down_disc(), Some(button)),
             Rc::new(move |region_event, cx| {
-                if let MouseRegionEvent::Down(mouse_button_event) = region_event {
-                    handler(mouse_button_event, cx);
+                if let MouseRegionEvent::Down(e) = region_event {
+                    handler(e, cx);
                 } else {
                     panic!(
                         "Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::Down, found {:?}", 
@@ -191,12 +234,12 @@ impl HandlerSet {
     pub fn on_up(
         mut self,
         button: MouseButton,
-        handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static,
+        handler: impl Fn(UpRegionEvent, &mut EventContext) + 'static,
     ) -> Self {
         self.set.insert((MouseRegionEvent::up_disc(), Some(button)),
             Rc::new(move |region_event, cx| {
-                if let MouseRegionEvent::Up(mouse_button_event) = region_event {
-                    handler(mouse_button_event, cx);
+                if let MouseRegionEvent::Up(e) = region_event {
+                    handler(e, cx);
                 } else {
                     panic!(
                         "Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::Up, found {:?}", 
@@ -209,12 +252,12 @@ impl HandlerSet {
     pub fn on_click(
         mut self,
         button: MouseButton,
-        handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static,
+        handler: impl Fn(ClickRegionEvent, &mut EventContext) + 'static,
     ) -> Self {
         self.set.insert((MouseRegionEvent::click_disc(), Some(button)),
             Rc::new(move |region_event, cx| {
-                if let MouseRegionEvent::Click(mouse_button_event) = region_event {
-                    handler(mouse_button_event, cx);
+                if let MouseRegionEvent::Click(e) = region_event {
+                    handler(e, cx);
                 } else {
                     panic!(
                         "Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::Click, found {:?}", 
@@ -227,12 +270,12 @@ impl HandlerSet {
     pub fn on_down_out(
         mut self,
         button: MouseButton,
-        handler: impl Fn(MouseButtonEvent, &mut EventContext) + 'static,
+        handler: impl Fn(DownOutRegionEvent, &mut EventContext) + 'static,
     ) -> Self {
         self.set.insert((MouseRegionEvent::down_out_disc(), Some(button)),
             Rc::new(move |region_event, cx| {
-                if let MouseRegionEvent::DownOut(mouse_button_event) = region_event {
-                    handler(mouse_button_event, cx);
+                if let MouseRegionEvent::DownOut(e) = region_event {
+                    handler(e, cx);
                 } else {
                     panic!(
                         "Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::DownOut, found {:?}", 
@@ -242,123 +285,56 @@ impl HandlerSet {
         self
     }
 
-    pub fn on_drag(
+    pub fn on_up_out(
         mut self,
         button: MouseButton,
-        handler: impl Fn(Vector2F, MouseMovedEvent, &mut EventContext) + 'static,
+        handler: impl Fn(UpOutRegionEvent, &mut EventContext) + 'static,
     ) -> Self {
-        self.set.insert((MouseRegionEvent::drag_disc(), Some(button)),
+        self.set.insert((MouseRegionEvent::up_out_disc(), Some(button)),
             Rc::new(move |region_event, cx| {
-                if let MouseRegionEvent::Drag(prev_drag_position, mouse_moved_event) = region_event {
-                    handler(prev_drag_position, mouse_moved_event, cx);
+                if let MouseRegionEvent::UpOut(e) = region_event {
+                    handler(e, cx);
                 } else {
                     panic!(
-                        "Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::Drag, found {:?}", 
+                        "Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::UpOut, found {:?}", 
                         region_event);
                 }
             }));
         self
     }
 
-    pub fn on_hover(
+    pub fn on_drag(
         mut self,
-        handler: impl Fn(bool, MouseMovedEvent, &mut EventContext) + 'static,
+        button: MouseButton,
+        handler: impl Fn(DragRegionEvent, &mut EventContext) + 'static,
     ) -> Self {
-        self.set.insert((MouseRegionEvent::hover_disc(), None),
+        self.set.insert((MouseRegionEvent::drag_disc(), Some(button)),
             Rc::new(move |region_event, cx| {
-                if let MouseRegionEvent::Hover(hover, mouse_moved_event) = region_event {
-                    handler(hover, mouse_moved_event, cx);
+                if let MouseRegionEvent::Drag(e) = region_event {
+                    handler(e, cx);
                 } else {
                     panic!(
-                        "Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::Hover, found {:?}", 
+                        "Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::Drag, found {:?}", 
                         region_event);
                 }
             }));
         self
     }
 
-    pub fn on_move(
+    pub fn on_hover(
         mut self,
-        handler: impl Fn(MouseMovedEvent, &mut EventContext) + 'static,
+        handler: impl Fn(HoverRegionEvent, &mut EventContext) + 'static,
     ) -> Self {
-        self.set.insert((MouseRegionEvent::move_disc(), None),
+        self.set.insert((MouseRegionEvent::hover_disc(), None),
             Rc::new(move |region_event, cx| {
-                if let MouseRegionEvent::Move(move_event)= region_event {
-                    handler(move_event, cx);
-                }  else {
+                if let MouseRegionEvent::Hover(e) = region_event {
+                    handler(e, cx);
+                } else {
                     panic!(
-                        "Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::Move, found {:?}", 
+                        "Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::Hover, found {:?}", 
                         region_event);
                 }
             }));
         self
     }
 }
-
-#[derive(Debug)]
-pub enum MouseRegionEvent {
-    Move(MouseMovedEvent),
-    Drag(Vector2F, MouseMovedEvent),
-    Hover(bool, MouseMovedEvent),
-    Down(MouseButtonEvent),
-    Up(MouseButtonEvent),
-    Click(MouseButtonEvent),
-    DownOut(MouseButtonEvent),
-    ScrollWheel(ScrollWheelEvent),
-}
-
-impl MouseRegionEvent {
-    pub fn move_disc() -> Discriminant<MouseRegionEvent> {
-        std::mem::discriminant(&MouseRegionEvent::Move(Default::default()))
-    }
-    pub fn drag_disc() -> Discriminant<MouseRegionEvent> {
-        std::mem::discriminant(&MouseRegionEvent::Drag(
-            Default::default(),
-            Default::default(),
-        ))
-    }
-    pub fn hover_disc() -> Discriminant<MouseRegionEvent> {
-        std::mem::discriminant(&MouseRegionEvent::Hover(
-            Default::default(),
-            Default::default(),
-        ))
-    }
-    pub fn down_disc() -> Discriminant<MouseRegionEvent> {
-        std::mem::discriminant(&MouseRegionEvent::Down(Default::default()))
-    }
-    pub fn up_disc() -> Discriminant<MouseRegionEvent> {
-        std::mem::discriminant(&MouseRegionEvent::Up(Default::default()))
-    }
-    pub fn click_disc() -> Discriminant<MouseRegionEvent> {
-        std::mem::discriminant(&MouseRegionEvent::Click(Default::default()))
-    }
-    pub fn down_out_disc() -> Discriminant<MouseRegionEvent> {
-        std::mem::discriminant(&MouseRegionEvent::DownOut(Default::default()))
-    }
-    pub fn scroll_wheel_disc() -> Discriminant<MouseRegionEvent> {
-        std::mem::discriminant(&MouseRegionEvent::ScrollWheel(Default::default()))
-    }
-
-    pub fn handler_key(&self) -> (Discriminant<MouseRegionEvent>, Option<MouseButton>) {
-        match self {
-            MouseRegionEvent::Move(_) => (Self::move_disc(), None),
-            MouseRegionEvent::Drag(_, MouseMovedEvent { pressed_button, .. }) => {
-                (Self::drag_disc(), *pressed_button)
-            }
-            MouseRegionEvent::Hover(_, _) => (Self::hover_disc(), None),
-            MouseRegionEvent::Down(MouseButtonEvent { button, .. }) => {
-                (Self::down_disc(), Some(*button))
-            }
-            MouseRegionEvent::Up(MouseButtonEvent { button, .. }) => {
-                (Self::up_disc(), Some(*button))
-            }
-            MouseRegionEvent::Click(MouseButtonEvent { button, .. }) => {
-                (Self::click_disc(), Some(*button))
-            }
-            MouseRegionEvent::DownOut(MouseButtonEvent { button, .. }) => {
-                (Self::down_out_disc(), Some(*button))
-            }
-            MouseRegionEvent::ScrollWheel(_) => (Self::scroll_wheel_disc(), None),
-        }
-    }
-}

crates/gpui/src/scene/mouse_region_event.rs 🔗

@@ -0,0 +1,233 @@
+use std::{
+    mem::{discriminant, Discriminant},
+    ops::Deref,
+};
+
+use pathfinder_geometry::{rect::RectF, vector::Vector2F};
+
+use crate::{MouseButton, MouseButtonEvent, MouseMovedEvent, ScrollWheelEvent};
+
+#[derive(Debug, Default, Clone)]
+pub struct MoveRegionEvent {
+    pub region: RectF,
+    pub platform_event: MouseMovedEvent,
+}
+
+impl Deref for MoveRegionEvent {
+    type Target = MouseMovedEvent;
+
+    fn deref(&self) -> &Self::Target {
+        &self.platform_event
+    }
+}
+
+#[derive(Debug, Default, Clone)]
+pub struct DragRegionEvent {
+    pub region: RectF,
+    pub prev_mouse_position: Vector2F,
+    pub platform_event: MouseMovedEvent,
+}
+
+impl Deref for DragRegionEvent {
+    type Target = MouseMovedEvent;
+
+    fn deref(&self) -> &Self::Target {
+        &self.platform_event
+    }
+}
+
+#[derive(Debug, Default, Clone)]
+pub struct HoverRegionEvent {
+    pub region: RectF,
+    pub started: bool,
+    pub platform_event: MouseMovedEvent,
+}
+
+impl Deref for HoverRegionEvent {
+    type Target = MouseMovedEvent;
+
+    fn deref(&self) -> &Self::Target {
+        &self.platform_event
+    }
+}
+
+#[derive(Debug, Default, Clone)]
+pub struct DownRegionEvent {
+    pub region: RectF,
+    pub platform_event: MouseButtonEvent,
+}
+
+impl Deref for DownRegionEvent {
+    type Target = MouseButtonEvent;
+
+    fn deref(&self) -> &Self::Target {
+        &self.platform_event
+    }
+}
+
+#[derive(Debug, Default, Clone)]
+pub struct UpRegionEvent {
+    pub region: RectF,
+    pub platform_event: MouseButtonEvent,
+}
+
+impl Deref for UpRegionEvent {
+    type Target = MouseButtonEvent;
+
+    fn deref(&self) -> &Self::Target {
+        &self.platform_event
+    }
+}
+
+#[derive(Debug, Default, Clone)]
+pub struct ClickRegionEvent {
+    pub region: RectF,
+    pub platform_event: MouseButtonEvent,
+}
+
+impl Deref for ClickRegionEvent {
+    type Target = MouseButtonEvent;
+
+    fn deref(&self) -> &Self::Target {
+        &self.platform_event
+    }
+}
+
+#[derive(Debug, Default, Clone)]
+pub struct DownOutRegionEvent {
+    pub region: RectF,
+    pub platform_event: MouseButtonEvent,
+}
+
+impl Deref for DownOutRegionEvent {
+    type Target = MouseButtonEvent;
+
+    fn deref(&self) -> &Self::Target {
+        &self.platform_event
+    }
+}
+
+#[derive(Debug, Default, Clone)]
+pub struct UpOutRegionEvent {
+    pub region: RectF,
+    pub platform_event: MouseButtonEvent,
+}
+
+impl Deref for UpOutRegionEvent {
+    type Target = MouseButtonEvent;
+
+    fn deref(&self) -> &Self::Target {
+        &self.platform_event
+    }
+}
+
+#[derive(Debug, Default, Clone)]
+pub struct ScrollWheelRegionEvent {
+    pub region: RectF,
+    pub platform_event: ScrollWheelEvent,
+}
+
+impl Deref for ScrollWheelRegionEvent {
+    type Target = ScrollWheelEvent;
+
+    fn deref(&self) -> &Self::Target {
+        &self.platform_event
+    }
+}
+
+#[derive(Debug, Clone)]
+pub enum MouseRegionEvent {
+    Move(MoveRegionEvent),
+    Drag(DragRegionEvent),
+    Hover(HoverRegionEvent),
+    Down(DownRegionEvent),
+    Up(UpRegionEvent),
+    Click(ClickRegionEvent),
+    DownOut(DownOutRegionEvent),
+    UpOut(UpOutRegionEvent),
+    ScrollWheel(ScrollWheelRegionEvent),
+}
+
+impl MouseRegionEvent {
+    pub fn set_region(&mut self, region: RectF) {
+        match self {
+            MouseRegionEvent::Move(r) => r.region = region,
+            MouseRegionEvent::Drag(r) => r.region = region,
+            MouseRegionEvent::Hover(r) => r.region = region,
+            MouseRegionEvent::Down(r) => r.region = region,
+            MouseRegionEvent::Up(r) => r.region = region,
+            MouseRegionEvent::Click(r) => r.region = region,
+            MouseRegionEvent::DownOut(r) => r.region = region,
+            MouseRegionEvent::UpOut(r) => r.region = region,
+            MouseRegionEvent::ScrollWheel(r) => r.region = region,
+        }
+    }
+
+    /// When true, mouse event handlers must call cx.propagate_event() to bubble
+    /// the event to handlers they are painted on top of.
+    pub fn is_capturable(&self) -> bool {
+        match self {
+            MouseRegionEvent::Move(_) => true,
+            MouseRegionEvent::Drag(_) => false,
+            MouseRegionEvent::Hover(_) => true,
+            MouseRegionEvent::Down(_) => true,
+            MouseRegionEvent::Up(_) => true,
+            MouseRegionEvent::Click(_) => true,
+            MouseRegionEvent::DownOut(_) => false,
+            MouseRegionEvent::UpOut(_) => false,
+            MouseRegionEvent::ScrollWheel(_) => true,
+        }
+    }
+}
+
+impl MouseRegionEvent {
+    pub fn move_disc() -> Discriminant<MouseRegionEvent> {
+        discriminant(&MouseRegionEvent::Move(Default::default()))
+    }
+
+    pub fn drag_disc() -> Discriminant<MouseRegionEvent> {
+        discriminant(&MouseRegionEvent::Drag(Default::default()))
+    }
+
+    pub fn hover_disc() -> Discriminant<MouseRegionEvent> {
+        discriminant(&MouseRegionEvent::Hover(Default::default()))
+    }
+
+    pub fn down_disc() -> Discriminant<MouseRegionEvent> {
+        discriminant(&MouseRegionEvent::Down(Default::default()))
+    }
+
+    pub fn up_disc() -> Discriminant<MouseRegionEvent> {
+        discriminant(&MouseRegionEvent::Up(Default::default()))
+    }
+
+    pub fn up_out_disc() -> Discriminant<MouseRegionEvent> {
+        discriminant(&MouseRegionEvent::UpOut(Default::default()))
+    }
+
+    pub fn click_disc() -> Discriminant<MouseRegionEvent> {
+        discriminant(&MouseRegionEvent::Click(Default::default()))
+    }
+
+    pub fn down_out_disc() -> Discriminant<MouseRegionEvent> {
+        discriminant(&MouseRegionEvent::DownOut(Default::default()))
+    }
+
+    pub fn scroll_wheel_disc() -> Discriminant<MouseRegionEvent> {
+        discriminant(&MouseRegionEvent::ScrollWheel(Default::default()))
+    }
+
+    pub fn handler_key(&self) -> (Discriminant<MouseRegionEvent>, Option<MouseButton>) {
+        match self {
+            MouseRegionEvent::Move(_) => (Self::move_disc(), None),
+            MouseRegionEvent::Drag(e) => (Self::drag_disc(), e.pressed_button),
+            MouseRegionEvent::Hover(_) => (Self::hover_disc(), None),
+            MouseRegionEvent::Down(e) => (Self::down_disc(), Some(e.button)),
+            MouseRegionEvent::Up(e) => (Self::up_disc(), Some(e.button)),
+            MouseRegionEvent::Click(e) => (Self::click_disc(), Some(e.button)),
+            MouseRegionEvent::UpOut(e) => (Self::up_out_disc(), Some(e.button)),
+            MouseRegionEvent::DownOut(e) => (Self::down_out_disc(), Some(e.button)),
+            MouseRegionEvent::ScrollWheel(_) => (Self::scroll_wheel_disc(), None),
+        }
+    }
+}

crates/project_panel/src/project_panel.rs 🔗

@@ -12,8 +12,7 @@ use gpui::{
     impl_internal_actions, keymap,
     platform::CursorStyle,
     AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle, MouseButton,
-    MouseButtonEvent, MutableAppContext, PromptLevel, RenderContext, Task, View, ViewContext,
-    ViewHandle,
+    MutableAppContext, PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle,
 };
 use menu::{Confirm, SelectNext, SelectPrev};
 use project::{Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
@@ -1074,25 +1073,22 @@ impl ProjectPanel {
                 .with_padding_left(padding)
                 .boxed()
         })
-        .on_click(
-            MouseButton::Left,
-            move |MouseButtonEvent { click_count, .. }, cx| {
-                if kind == EntryKind::Dir {
-                    cx.dispatch_action(ToggleExpanded(entry_id))
-                } else {
-                    cx.dispatch_action(Open {
-                        entry_id,
-                        change_focus: click_count > 1,
-                    })
-                }
-            },
-        )
-        .on_down(
-            MouseButton::Right,
-            move |MouseButtonEvent { position, .. }, cx| {
-                cx.dispatch_action(DeployContextMenu { entry_id, position })
-            },
-        )
+        .on_click(MouseButton::Left, move |e, cx| {
+            if kind == EntryKind::Dir {
+                cx.dispatch_action(ToggleExpanded(entry_id))
+            } else {
+                cx.dispatch_action(Open {
+                    entry_id,
+                    change_focus: e.click_count > 1,
+                })
+            }
+        })
+        .on_down(MouseButton::Right, move |e, cx| {
+            cx.dispatch_action(DeployContextMenu {
+                entry_id,
+                position: e.position,
+            })
+        })
         .with_cursor_style(CursorStyle::PointingHand)
         .boxed()
     }
@@ -1139,16 +1135,16 @@ impl View for ProjectPanel {
                     .expanded()
                     .boxed()
                 })
-                .on_down(
-                    MouseButton::Right,
-                    move |MouseButtonEvent { position, .. }, cx| {
-                        // When deploying the context menu anywhere below the last project entry,
-                        // act as if the user clicked the root of the last worktree.
-                        if let Some(entry_id) = last_worktree_root_id {
-                            cx.dispatch_action(DeployContextMenu { entry_id, position })
-                        }
-                    },
-                )
+                .on_down(MouseButton::Right, move |e, cx| {
+                    // When deploying the context menu anywhere below the last project entry,
+                    // act as if the user clicked the root of the last worktree.
+                    if let Some(entry_id) = last_worktree_root_id {
+                        cx.dispatch_action(DeployContextMenu {
+                            entry_id,
+                            position: e.position,
+                        })
+                    }
+                })
                 .boxed(),
             )
             .with_child(ChildView::new(&self.context_menu).boxed())

crates/terminal/src/terminal.rs 🔗

@@ -39,13 +39,11 @@ use std::{
 use thiserror::Error;
 
 use gpui::{
-    geometry::{
-        rect::RectF,
-        vector::{vec2f, Vector2F},
-    },
+    geometry::vector::{vec2f, Vector2F},
     keymap::Keystroke,
-    ClipboardItem, Entity, ModelContext, MouseButton, MouseButtonEvent, MouseMovedEvent,
-    MutableAppContext, ScrollWheelEvent,
+    scene::{ClickRegionEvent, DownRegionEvent, DragRegionEvent, UpRegionEvent},
+    ClipboardItem, Entity, ModelContext, MouseButton, MouseMovedEvent, MutableAppContext,
+    ScrollWheelEvent,
 };
 
 use crate::mappings::{
@@ -676,7 +674,7 @@ impl Terminal {
         }
     }
 
-    pub fn mouse_drag(&mut self, e: MouseMovedEvent, origin: Vector2F, bounds: RectF) {
+    pub fn mouse_drag(&mut self, e: DragRegionEvent, origin: Vector2F) {
         let position = e.position.sub(origin);
 
         if !self.mouse_mode(e.shift) {
@@ -687,8 +685,8 @@ impl Terminal {
             // Doesn't make sense to scroll the alt screen
             if !self.last_mode.contains(TermMode::ALT_SCREEN) {
                 //TODO: Why do these need to be doubled?
-                let top = bounds.origin_y() + (self.cur_size.line_height * 2.);
-                let bottom = bounds.lower_left().y() - (self.cur_size.line_height * 2.);
+                let top = e.region.origin_y() + (self.cur_size.line_height * 2.);
+                let bottom = e.region.lower_left().y() - (self.cur_size.line_height * 2.);
 
                 let scroll_delta = if e.position.y() < top {
                     (top - e.position.y()).powf(1.1)
@@ -705,7 +703,7 @@ impl Terminal {
         }
     }
 
-    pub fn mouse_down(&mut self, e: &MouseButtonEvent, origin: Vector2F) {
+    pub fn mouse_down(&mut self, e: &DownRegionEvent, origin: Vector2F) {
         let position = e.position.sub(origin);
         let point = mouse_point(position, self.cur_size, self.last_offset);
         let side = mouse_side(position, self.cur_size);
@@ -719,7 +717,7 @@ impl Terminal {
         }
     }
 
-    pub fn left_click(&mut self, e: &MouseButtonEvent, origin: Vector2F) {
+    pub fn left_click(&mut self, e: &ClickRegionEvent, origin: Vector2F) {
         let position = e.position.sub(origin);
 
         if !self.mouse_mode(e.shift) {
@@ -743,7 +741,7 @@ impl Terminal {
         }
     }
 
-    pub fn mouse_up(&mut self, e: &MouseButtonEvent, origin: Vector2F) {
+    pub fn mouse_up(&mut self, e: &UpRegionEvent, origin: Vector2F) {
         let position = e.position.sub(origin);
         if self.mouse_mode(e.shift) {
             let point = mouse_point(position, self.cur_size, self.last_offset);

crates/terminal/src/terminal_element.rs 🔗

@@ -18,9 +18,8 @@ use gpui::{
     },
     serde_json::json,
     text_layout::{Line, RunStyle},
-    Element, Event, EventContext, FontCache, KeyDownEvent, ModelContext, MouseButton,
-    MouseButtonEvent, MouseRegion, PaintContext, Quad, TextLayoutCache, WeakModelHandle,
-    WeakViewHandle,
+    Element, Event, EventContext, FontCache, KeyDownEvent, ModelContext, MouseButton, MouseRegion,
+    PaintContext, Quad, TextLayoutCache, WeakModelHandle, WeakViewHandle,
 };
 use itertools::Itertools;
 use ordered_float::OrderedFloat;
@@ -410,11 +409,11 @@ impl TerminalElement {
         }
     }
 
-    fn generic_button_handler(
+    fn generic_button_handler<E>(
         connection: WeakModelHandle<Terminal>,
         origin: Vector2F,
-        f: impl Fn(&mut Terminal, Vector2F, MouseButtonEvent, &mut ModelContext<Terminal>),
-    ) -> impl Fn(MouseButtonEvent, &mut EventContext) {
+        f: impl Fn(&mut Terminal, Vector2F, E, &mut ModelContext<Terminal>),
+    ) -> impl Fn(E, &mut EventContext) {
         move |event, cx| {
             cx.focus_parent_view();
             if let Some(conn_handle) = connection.upgrade(cx.app) {
@@ -453,11 +452,11 @@ impl TerminalElement {
                 ),
             )
             // Update drag selections
-            .on_drag(MouseButton::Left, move |_prev, event, cx| {
+            .on_drag(MouseButton::Left, move |event, cx| {
                 if cx.is_parent_view_focused() {
                     if let Some(conn_handle) = connection.upgrade(cx.app) {
                         conn_handle.update(cx.app, |terminal, cx| {
-                            terminal.mouse_drag(event, origin, visible_bounds);
+                            terminal.mouse_drag(event, origin);
                             cx.notify();
                         })
                     }
@@ -486,20 +485,19 @@ impl TerminalElement {
                 ),
             )
             // Context menu
-            .on_click(
-                MouseButton::Right,
-                move |e @ MouseButtonEvent { position, .. }, cx| {
-                    let mouse_mode = if let Some(conn_handle) = connection.upgrade(cx.app) {
-                        conn_handle.update(cx.app, |terminal, _cx| terminal.mouse_mode(e.shift))
-                    } else {
-                        // If we can't get the model handle, probably can't deploy the context menu
-                        true
-                    };
-                    if !mouse_mode {
-                        cx.dispatch_action(DeployContextMenu { position });
-                    }
-                },
-            );
+            .on_click(MouseButton::Right, move |e, cx| {
+                let mouse_mode = if let Some(conn_handle) = connection.upgrade(cx.app) {
+                    conn_handle.update(cx.app, |terminal, _cx| terminal.mouse_mode(e.shift))
+                } else {
+                    // If we can't get the model handle, probably can't deploy the context menu
+                    true
+                };
+                if !mouse_mode {
+                    cx.dispatch_action(DeployContextMenu {
+                        position: e.position,
+                    });
+                }
+            });
 
         // Mouse mode handlers:
         // All mouse modes need the extra click handlers

crates/theme/src/theme.rs 🔗

@@ -75,7 +75,25 @@ pub struct TabBar {
     pub pane_button: Interactive<IconButton>,
     pub active_pane: TabStyles,
     pub inactive_pane: TabStyles,
+    pub dragged_tab: Tab,
     pub height: f32,
+    pub drop_target_overlay_color: Color,
+}
+
+impl TabBar {
+    pub fn tab_style(&self, pane_active: bool, tab_active: bool) -> &Tab {
+        let tabs = if pane_active {
+            &self.active_pane
+        } else {
+            &self.inactive_pane
+        };
+
+        if tab_active {
+            &tabs.active_tab
+        } else {
+            &tabs.inactive_tab
+        }
+    }
 }
 
 #[derive(Clone, Deserialize, Default)]

crates/workspace/Cargo.toml 🔗

@@ -15,6 +15,7 @@ client = { path = "../client" }
 clock = { path = "../clock" }
 collections = { path = "../collections" }
 context_menu = { path = "../context_menu" }
+drag_and_drop = { path = "../drag_and_drop" }
 gpui = { path = "../gpui" }
 language = { path = "../language" }
 menu = { path = "../menu" }

crates/workspace/src/pane.rs 🔗

@@ -3,9 +3,11 @@ use crate::{toolbar::Toolbar, Item, NewFile, NewSearch, NewTerminal, WeakItemHan
 use anyhow::Result;
 use collections::{HashMap, HashSet, VecDeque};
 use context_menu::{ContextMenu, ContextMenuItem};
+use drag_and_drop::{DragAndDrop, Draggable};
 use futures::StreamExt;
 use gpui::{
     actions,
+    color::Color,
     elements::*,
     geometry::{
         rect::RectF,
@@ -14,13 +16,14 @@ use gpui::{
     impl_actions, impl_internal_actions,
     platform::{CursorStyle, NavigationDirection},
     AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, EventContext,
-    ModelHandle, MouseButton, MouseButtonEvent, MutableAppContext, PromptLevel, Quad,
-    RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle,
+    ModelHandle, MouseButton, MutableAppContext, PromptLevel, Quad, RenderContext, Task, View,
+    ViewContext, ViewHandle, WeakViewHandle,
 };
 use project::{Project, ProjectEntryId, ProjectPath};
 use serde::Deserialize;
 use settings::{Autosave, Settings};
-use std::{any::Any, cell::RefCell, mem, path::Path, rc::Rc};
+use std::{any::Any, cell::RefCell, cmp, mem, path::Path, rc::Rc};
+use theme::Theme;
 use util::ResultExt;
 
 #[derive(Clone, Deserialize, PartialEq)]
@@ -48,6 +51,14 @@ pub struct CloseItem {
     pub pane: WeakViewHandle<Pane>,
 }
 
+#[derive(Clone, PartialEq)]
+pub struct MoveItem {
+    pub item_id: usize,
+    pub from: WeakViewHandle<Pane>,
+    pub to: WeakViewHandle<Pane>,
+    pub destination_index: usize,
+}
+
 #[derive(Clone, Deserialize, PartialEq)]
 pub struct GoBack {
     #[serde(skip_deserializing)]
@@ -71,16 +82,16 @@ pub struct DeployNewMenu {
 }
 
 impl_actions!(pane, [GoBack, GoForward, ActivateItem]);
-impl_internal_actions!(pane, [CloseItem, DeploySplitMenu, DeployNewMenu]);
+impl_internal_actions!(pane, [CloseItem, DeploySplitMenu, DeployNewMenu, MoveItem]);
 
 const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
 
 pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
-        pane.activate_item(action.0, true, true, false, cx);
+        pane.activate_item(action.0, true, true, cx);
     });
     cx.add_action(|pane: &mut Pane, _: &ActivateLastItem, cx| {
-        pane.activate_item(pane.items.len() - 1, true, true, false, cx);
+        pane.activate_item(pane.items.len() - 1, true, true, cx);
     });
     cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
         pane.activate_prev_item(cx);
@@ -98,6 +109,32 @@ pub fn init(cx: &mut MutableAppContext) {
             Ok(())
         }))
     });
+    cx.add_action(
+        |workspace,
+         MoveItem {
+             from,
+             to,
+             item_id,
+             destination_index,
+         },
+         cx| {
+            // Get item handle to move
+            let from = if let Some(from) = from.upgrade(cx) {
+                from
+            } else {
+                return;
+            };
+
+            // Add item to new pane at given index
+            let to = if let Some(to) = to.upgrade(cx) {
+                to
+            } else {
+                return;
+            };
+
+            Pane::move_item(workspace, from, to, *item_id, *destination_index, cx)
+        },
+    );
     cx.add_action(|pane: &mut Pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx));
     cx.add_action(|pane: &mut Pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx));
     cx.add_action(|pane: &mut Pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx));
@@ -186,6 +223,17 @@ pub struct NavigationEntry {
     pub data: Option<Box<dyn Any>>,
 }
 
+struct DraggedItem {
+    item: Box<dyn ItemHandle>,
+    pane: WeakViewHandle<Pane>,
+}
+
+pub enum ReorderBehavior {
+    None,
+    MoveAfterActive,
+    MoveToIndex(usize),
+}
+
 impl Pane {
     pub fn new(cx: &mut ViewContext<Self>) -> Self {
         let handle = cx.weak_handle();
@@ -300,7 +348,7 @@ impl Pane {
                 {
                     let prev_active_item_index = pane.active_item_index;
                     pane.nav_history.borrow_mut().set_mode(mode);
-                    pane.activate_item(index, true, true, false, cx);
+                    pane.activate_item(index, true, true, cx);
                     pane.nav_history
                         .borrow_mut()
                         .set_mode(NavigationMode::Normal);
@@ -387,63 +435,100 @@ impl Pane {
         build_item: impl FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
     ) -> Box<dyn ItemHandle> {
         let existing_item = pane.update(cx, |pane, cx| {
-            for (ix, item) in pane.items.iter().enumerate() {
+            for item in pane.items.iter() {
                 if item.project_path(cx).is_some()
                     && item.project_entry_ids(cx).as_slice() == [project_entry_id]
                 {
                     let item = item.boxed_clone();
-                    pane.activate_item(ix, true, focus_item, true, cx);
                     return Some(item);
                 }
             }
             None
         });
-        if let Some(existing_item) = existing_item {
-            existing_item
-        } else {
-            let item = pane.update(cx, |_, cx| build_item(cx));
-            Self::add_item(workspace, pane, item.boxed_clone(), true, focus_item, cx);
-            item
-        }
+
+        // Even if the item exists, we re-add it to reorder it after the active item.
+        // We may revisit this behavior after adding an "activation history" for pane items.
+        let item = existing_item.unwrap_or_else(|| pane.update(cx, |_, cx| build_item(cx)));
+        Pane::add_item(workspace, &pane, item.clone(), true, focus_item, None, cx);
+        item
     }
 
-    pub(crate) fn add_item(
+    pub fn add_item(
         workspace: &mut Workspace,
-        pane: ViewHandle<Pane>,
+        pane: &ViewHandle<Pane>,
         item: Box<dyn ItemHandle>,
         activate_pane: bool,
         focus_item: bool,
+        destination_index: Option<usize>,
         cx: &mut ViewContext<Workspace>,
     ) {
-        // Prevent adding the same item to the pane more than once.
-        // If there is already an active item, reorder the desired item to be after it
-        // and activate it.
-        if let Some(item_ix) = pane.read(cx).items.iter().position(|i| i.id() == item.id()) {
+        // If no destination index is specified, add or move the item after the active item.
+        let mut insertion_index = {
+            let pane = pane.read(cx);
+            cmp::min(
+                if let Some(destination_index) = destination_index {
+                    destination_index
+                } else {
+                    pane.active_item_index + 1
+                },
+                pane.items.len(),
+            )
+        };
+
+        // Does the item already exist?
+        if let Some(existing_item_index) = pane.read(cx).items.iter().position(|existing_item| {
+            let existing_item_entry_ids = existing_item.project_entry_ids(cx);
+            let added_item_entry_ids = item.project_entry_ids(cx);
+            let entries_match = !existing_item_entry_ids.is_empty()
+                && existing_item_entry_ids == added_item_entry_ids;
+
+            existing_item.id() == item.id() || entries_match
+        }) {
+            // If the item already exists, move it to the desired destination and activate it
             pane.update(cx, |pane, cx| {
-                pane.activate_item(item_ix, activate_pane, focus_item, true, cx)
-            });
-            return;
-        }
+                if existing_item_index != insertion_index {
+                    cx.reparent(&item);
+                    let existing_item_is_active = existing_item_index == pane.active_item_index;
+
+                    // If the caller didn't specify a destination and the added item is already
+                    // the active one, don't move it
+                    if existing_item_is_active && destination_index.is_none() {
+                        insertion_index = existing_item_index;
+                    } else {
+                        pane.items.remove(existing_item_index);
+                        if existing_item_index < pane.active_item_index {
+                            pane.active_item_index -= 1;
+                        }
+                        insertion_index = insertion_index.min(pane.items.len());
 
-        item.added_to_pane(workspace, pane.clone(), cx);
-        pane.update(cx, |pane, cx| {
-            // If there is already an active item, then insert the new item
-            // right after it. Otherwise, adjust the `active_item_index` field
-            // before activating the new item, so that in the `activate_item`
-            // method, we can detect that the active item is changing.
-            let item_ix;
-            if pane.active_item_index < pane.items.len() {
-                item_ix = pane.active_item_index + 1
-            } else {
-                item_ix = pane.items.len();
-                pane.active_item_index = usize::MAX;
-            };
+                        pane.items.insert(insertion_index, item.clone());
 
-            cx.reparent(&item);
-            pane.items.insert(item_ix, item);
-            pane.activate_item(item_ix, activate_pane, focus_item, false, cx);
-            cx.notify();
-        });
+                        if existing_item_is_active {
+                            pane.active_item_index = insertion_index;
+                        } else if insertion_index <= pane.active_item_index {
+                            pane.active_item_index += 1;
+                        }
+                    }
+
+                    cx.notify();
+                }
+
+                pane.activate_item(insertion_index, activate_pane, focus_item, cx);
+            });
+        } else {
+            // If the item doesn't already exist, add it and activate it
+            item.added_to_pane(workspace, pane.clone(), cx);
+            pane.update(cx, |pane, cx| {
+                cx.reparent(&item);
+                pane.items.insert(insertion_index, item);
+                if insertion_index <= pane.active_item_index {
+                    pane.active_item_index += 1;
+                }
+
+                pane.activate_item(insertion_index, activate_pane, focus_item, cx);
+                cx.notify();
+            });
+        }
     }
 
     pub fn items(&self) -> impl Iterator<Item = &Box<dyn ItemHandle>> {
@@ -480,31 +565,13 @@ impl Pane {
 
     pub fn activate_item(
         &mut self,
-        mut index: usize,
+        index: usize,
         activate_pane: bool,
         focus_item: bool,
-        move_after_current_active: bool,
         cx: &mut ViewContext<Self>,
     ) {
         use NavigationMode::{GoingBack, GoingForward};
         if index < self.items.len() {
-            if move_after_current_active {
-                // If there is already an active item, reorder the desired item to be after it
-                // and activate it.
-                if self.active_item_index != index && self.active_item_index < self.items.len() {
-                    let pane_to_activate = self.items.remove(index);
-                    if self.active_item_index < index {
-                        index = self.active_item_index + 1;
-                    } else if self.active_item_index < self.items.len() + 1 {
-                        index = self.active_item_index;
-                        // Index is less than active_item_index. Reordering will decrement the
-                        // active_item_index, so adjust it accordingly
-                        self.active_item_index = index - 1;
-                    }
-                    self.items.insert(index, pane_to_activate);
-                }
-            }
-
             let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
             if prev_active_item_ix != self.active_item_index
                 || matches!(self.nav_history.borrow().mode, GoingBack | GoingForward)
@@ -535,7 +602,7 @@ impl Pane {
         } else if !self.items.is_empty() {
             index = self.items.len() - 1;
         }
-        self.activate_item(index, true, true, false, cx);
+        self.activate_item(index, true, true, cx);
     }
 
     pub fn activate_next_item(&mut self, cx: &mut ViewContext<Self>) {
@@ -545,7 +612,7 @@ impl Pane {
         } else {
             index = 0;
         }
-        self.activate_item(index, true, true, false, cx);
+        self.activate_item(index, true, true, cx);
     }
 
     pub fn close_active_item(
@@ -672,48 +739,7 @@ impl Pane {
                 // Remove the item from the pane.
                 pane.update(&mut cx, |pane, cx| {
                     if let Some(item_ix) = pane.items.iter().position(|i| i.id() == item.id()) {
-                        if item_ix == pane.active_item_index {
-                            // Activate the previous item if possible.
-                            // This returns the user to the previously opened tab if they closed
-                            // a ne item they just navigated to.
-                            if item_ix > 0 {
-                                pane.activate_prev_item(cx);
-                            } else if item_ix + 1 < pane.items.len() {
-                                pane.activate_next_item(cx);
-                            }
-                        }
-
-                        let item = pane.items.remove(item_ix);
-                        cx.emit(Event::RemoveItem);
-                        if pane.items.is_empty() {
-                            item.deactivated(cx);
-                            pane.update_toolbar(cx);
-                            cx.emit(Event::Remove);
-                        }
-
-                        if item_ix < pane.active_item_index {
-                            pane.active_item_index -= 1;
-                        }
-
-                        pane.nav_history
-                            .borrow_mut()
-                            .set_mode(NavigationMode::ClosingItem);
-                        item.deactivated(cx);
-                        pane.nav_history
-                            .borrow_mut()
-                            .set_mode(NavigationMode::Normal);
-
-                        if let Some(path) = item.project_path(cx) {
-                            pane.nav_history
-                                .borrow_mut()
-                                .paths_by_item
-                                .insert(item.id(), path);
-                        } else {
-                            pane.nav_history
-                                .borrow_mut()
-                                .paths_by_item
-                                .remove(&item.id());
-                        }
+                        pane.remove_item(item_ix, cx);
                     }
                 });
             }
@@ -723,6 +749,53 @@ impl Pane {
         })
     }
 
+    fn remove_item(&mut self, item_ix: usize, cx: &mut ViewContext<Self>) {
+        if item_ix == self.active_item_index {
+            // Activate the previous item if possible.
+            // This returns the user to the previously opened tab if they closed
+            // a new item they just navigated to.
+            if item_ix > 0 {
+                self.activate_prev_item(cx);
+            } else if item_ix + 1 < self.items.len() {
+                self.activate_next_item(cx);
+            }
+        }
+
+        let item = self.items.remove(item_ix);
+        cx.emit(Event::RemoveItem);
+        if self.items.is_empty() {
+            item.deactivated(cx);
+            self.update_toolbar(cx);
+            cx.emit(Event::Remove);
+        }
+
+        if item_ix < self.active_item_index {
+            self.active_item_index -= 1;
+        }
+
+        self.nav_history
+            .borrow_mut()
+            .set_mode(NavigationMode::ClosingItem);
+        item.deactivated(cx);
+        self.nav_history
+            .borrow_mut()
+            .set_mode(NavigationMode::Normal);
+
+        if let Some(path) = item.project_path(cx) {
+            self.nav_history
+                .borrow_mut()
+                .paths_by_item
+                .insert(item.id(), path);
+        } else {
+            self.nav_history
+                .borrow_mut()
+                .paths_by_item
+                .remove(&item.id());
+        }
+
+        cx.notify();
+    }
+
     pub async fn save_item(
         project: ModelHandle<Project>,
         pane: &ViewHandle<Pane>,
@@ -746,7 +819,7 @@ impl Pane {
 
         if has_conflict && can_save {
             let mut answer = pane.update(cx, |pane, cx| {
-                pane.activate_item(item_ix, true, true, false, cx);
+                pane.activate_item(item_ix, true, true, cx);
                 cx.prompt(
                     PromptLevel::Warning,
                     CONFLICT_MESSAGE,
@@ -767,7 +840,7 @@ impl Pane {
             });
             let should_save = if should_prompt_for_save && !will_autosave {
                 let mut answer = pane.update(cx, |pane, cx| {
-                    pane.activate_item(item_ix, true, true, false, cx);
+                    pane.activate_item(item_ix, true, true, cx);
                     cx.prompt(
                         PromptLevel::Warning,
                         DIRTY_MESSAGE,
@@ -829,6 +902,42 @@ impl Pane {
         }
     }
 
+    fn move_item(
+        workspace: &mut Workspace,
+        from: ViewHandle<Pane>,
+        to: ViewHandle<Pane>,
+        item_to_move: usize,
+        destination_index: usize,
+        cx: &mut ViewContext<Workspace>,
+    ) {
+        let (item_ix, item_handle) = from
+            .read(cx)
+            .items()
+            .enumerate()
+            .find(|(_, item_handle)| item_handle.id() == item_to_move)
+            .expect("Tried to move item handle which was not in from pane");
+
+        // This automatically removes duplicate items in the pane
+        Pane::add_item(
+            workspace,
+            &to,
+            item_handle.clone(),
+            true,
+            true,
+            Some(destination_index),
+            cx,
+        );
+
+        if from != to {
+            // Close item from previous pane
+            from.update(cx, |from, cx| {
+                from.remove_item(item_ix, cx);
+            });
+        }
+
+        cx.focus(to);
+    }
+
     pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
         cx.emit(Event::Split(direction));
     }
@@ -876,144 +985,58 @@ impl Pane {
         });
     }
 
-    fn render_tabs(&mut self, cx: &mut RenderContext<Self>) -> impl Element {
+    fn render_tab_bar(&mut self, cx: &mut RenderContext<Self>) -> impl Element {
         let theme = cx.global::<Settings>().theme.clone();
+        let filler_index = self.items.len();
 
         enum Tabs {}
         enum Tab {}
+        enum Filler {}
         let pane = cx.handle();
-        MouseEventHandler::new::<Tabs, _, _>(0, cx, |mouse_state, cx| {
+        MouseEventHandler::new::<Tabs, _, _>(0, cx, |_, cx| {
             let autoscroll = if mem::take(&mut self.autoscroll) {
                 Some(self.active_item_index)
             } else {
                 None
             };
 
-            let is_pane_active = self.is_active;
-
-            let tab_styles = match is_pane_active {
-                true => theme.workspace.tab_bar.active_pane.clone(),
-                false => theme.workspace.tab_bar.inactive_pane.clone(),
-            };
-            let filler_style = tab_styles.inactive_tab.clone();
+            let pane_active = self.is_active;
 
             let mut row = Flex::row().scrollable::<Tabs, _>(1, autoscroll, cx);
-            for (ix, (item, detail)) in self.items.iter().zip(self.tab_details(cx)).enumerate() {
-                let item_id = item.id();
+            for (ix, (item, detail)) in self
+                .items
+                .iter()
+                .cloned()
+                .zip(self.tab_details(cx))
+                .enumerate()
+            {
                 let detail = if detail == 0 { None } else { Some(detail) };
-                let is_tab_active = ix == self.active_item_index;
-
-                let close_tab_callback = {
-                    let pane = pane.clone();
-                    move |_, cx: &mut EventContext| {
-                        cx.dispatch_action(CloseItem {
-                            item_id,
-                            pane: pane.clone(),
-                        })
-                    }
-                };
+                let tab_active = ix == self.active_item_index;
 
                 row.add_child({
-                    let mut tab_style = match is_tab_active {
-                        true => tab_styles.active_tab.clone(),
-                        false => tab_styles.inactive_tab.clone(),
-                    };
-
-                    let title = item.tab_content(detail, &tab_style, cx);
-
-                    if ix == 0 {
-                        tab_style.container.border.left = false;
-                    }
-
-                    MouseEventHandler::new::<Tab, _, _>(ix, cx, |_, cx| {
-                        Container::new(
-                            Flex::row()
-                                .with_child(
-                                    Align::new({
-                                        let diameter = 7.0;
-                                        let icon_color = if item.has_conflict(cx) {
-                                            Some(tab_style.icon_conflict)
-                                        } else if item.is_dirty(cx) {
-                                            Some(tab_style.icon_dirty)
-                                        } else {
-                                            None
-                                        };
-
-                                        ConstrainedBox::new(
-                                            Canvas::new(move |bounds, _, cx| {
-                                                if let Some(color) = icon_color {
-                                                    let square = RectF::new(
-                                                        bounds.origin(),
-                                                        vec2f(diameter, diameter),
-                                                    );
-                                                    cx.scene.push_quad(Quad {
-                                                        bounds: square,
-                                                        background: Some(color),
-                                                        border: Default::default(),
-                                                        corner_radius: diameter / 2.,
-                                                    });
-                                                }
-                                            })
-                                            .boxed(),
-                                        )
-                                        .with_width(diameter)
-                                        .with_height(diameter)
-                                        .boxed()
-                                    })
-                                    .boxed(),
-                                )
-                                .with_child(
-                                    Container::new(Align::new(title).boxed())
-                                        .with_style(ContainerStyle {
-                                            margin: Margin {
-                                                left: tab_style.spacing,
-                                                right: tab_style.spacing,
-                                                ..Default::default()
-                                            },
-                                            ..Default::default()
-                                        })
-                                        .boxed(),
-                                )
-                                .with_child(
-                                    Align::new(
-                                        ConstrainedBox::new(if mouse_state.hovered {
-                                            enum TabCloseButton {}
-                                            let icon = Svg::new("icons/x_mark_thin_8.svg");
-                                            MouseEventHandler::new::<TabCloseButton, _, _>(
-                                                item_id,
-                                                cx,
-                                                |mouse_state, _| {
-                                                    if mouse_state.hovered {
-                                                        icon.with_color(tab_style.icon_close_active)
-                                                            .boxed()
-                                                    } else {
-                                                        icon.with_color(tab_style.icon_close)
-                                                            .boxed()
-                                                    }
-                                                },
-                                            )
-                                            .with_padding(Padding::uniform(4.))
-                                            .with_cursor_style(CursorStyle::PointingHand)
-                                            .on_click(MouseButton::Left, close_tab_callback.clone())
-                                            .on_click(
-                                                MouseButton::Middle,
-                                                close_tab_callback.clone(),
-                                            )
-                                            .named("close-tab-icon")
-                                        } else {
-                                            Empty::new().boxed()
-                                        })
-                                        .with_width(tab_style.icon_width)
-                                        .boxed(),
-                                    )
-                                    .boxed(),
-                                )
-                                .boxed(),
-                        )
-                        .with_style(tab_style.container)
-                        .boxed()
+                    MouseEventHandler::new::<Tab, _, _>(ix, cx, {
+                        let item = item.clone();
+                        let pane = pane.clone();
+                        let detail = detail.clone();
+
+                        let theme = cx.global::<Settings>().theme.clone();
+
+                        move |mouse_state, cx| {
+                            let tab_style =
+                                theme.workspace.tab_bar.tab_style(pane_active, tab_active);
+                            let hovered = mouse_state.hovered;
+                            Self::render_tab(
+                                &item,
+                                pane,
+                                detail,
+                                hovered,
+                                Self::tab_overlay_color(hovered, theme.as_ref(), cx),
+                                tab_style,
+                                cx,
+                            )
+                        }
                     })
-                    .with_cursor_style(if is_tab_active && is_pane_active {
+                    .with_cursor_style(if pane_active && tab_active {
                         CursorStyle::Arrow
                     } else {
                         CursorStyle::PointingHand
@@ -1021,22 +1044,73 @@ impl Pane {
                     .on_down(MouseButton::Left, move |_, cx| {
                         cx.dispatch_action(ActivateItem(ix));
                     })
-                    .on_click(MouseButton::Middle, close_tab_callback)
+                    .on_click(MouseButton::Middle, {
+                        let item = item.clone();
+                        let pane = pane.clone();
+                        move |_, cx: &mut EventContext| {
+                            cx.dispatch_action(CloseItem {
+                                item_id: item.id(),
+                                pane: pane.clone(),
+                            })
+                        }
+                    })
+                    .on_up(MouseButton::Left, {
+                        let pane = pane.clone();
+                        move |_, cx: &mut EventContext| Pane::handle_dropped_item(&pane, ix, cx)
+                    })
+                    .as_draggable(
+                        DraggedItem {
+                            item,
+                            pane: pane.clone(),
+                        },
+                        {
+                            let theme = cx.global::<Settings>().theme.clone();
+
+                            let detail = detail.clone();
+                            move |dragged_item, cx: &mut RenderContext<Workspace>| {
+                                let tab_style = &theme.workspace.tab_bar.dragged_tab;
+                                Pane::render_tab(
+                                    &dragged_item.item,
+                                    dragged_item.pane.clone(),
+                                    detail,
+                                    false,
+                                    None,
+                                    &tab_style,
+                                    cx,
+                                )
+                            }
+                        },
+                    )
                     .boxed()
                 })
             }
 
+            // Use the inactive tab style along with the current pane's active status to decide how to render
+            // the filler
+            let filler_style = theme.workspace.tab_bar.tab_style(pane_active, false);
             row.add_child(
-                Empty::new()
-                    .contained()
-                    .with_style(filler_style.container)
-                    .with_border(filler_style.container.border)
-                    .flex(0., true)
-                    .named("filler"),
+                MouseEventHandler::new::<Filler, _, _>(0, cx, |mouse_state, cx| {
+                    let mut filler = Empty::new()
+                        .contained()
+                        .with_style(filler_style.container)
+                        .with_border(filler_style.container.border);
+
+                    if let Some(overlay) = Self::tab_overlay_color(mouse_state.hovered, &theme, cx)
+                    {
+                        filler = filler.with_overlay_color(overlay);
+                    }
+
+                    filler.boxed()
+                })
+                .flex(1., true)
+                .named("filler"),
             );
 
             row.boxed()
         })
+        .on_up(MouseButton::Left, move |_, cx| {
+            Pane::handle_dropped_item(&pane, filler_index, cx)
+        })
     }
 
     fn tab_details(&self, cx: &AppContext) -> Vec<usize> {
@@ -1075,6 +1149,142 @@ impl Pane {
 
         tab_details
     }
+
+    fn render_tab<V: View>(
+        item: &Box<dyn ItemHandle>,
+        pane: WeakViewHandle<Pane>,
+        detail: Option<usize>,
+        hovered: bool,
+        overlay: Option<Color>,
+        tab_style: &theme::Tab,
+        cx: &mut RenderContext<V>,
+    ) -> ElementBox {
+        let title = item.tab_content(detail, &tab_style, cx);
+
+        let mut tab = Flex::row()
+            .with_child(
+                Align::new({
+                    let diameter = 7.0;
+                    let icon_color = if item.has_conflict(cx) {
+                        Some(tab_style.icon_conflict)
+                    } else if item.is_dirty(cx) {
+                        Some(tab_style.icon_dirty)
+                    } else {
+                        None
+                    };
+
+                    ConstrainedBox::new(
+                        Canvas::new(move |bounds, _, cx| {
+                            if let Some(color) = icon_color {
+                                let square = RectF::new(bounds.origin(), vec2f(diameter, diameter));
+                                cx.scene.push_quad(Quad {
+                                    bounds: square,
+                                    background: Some(color),
+                                    border: Default::default(),
+                                    corner_radius: diameter / 2.,
+                                });
+                            }
+                        })
+                        .boxed(),
+                    )
+                    .with_width(diameter)
+                    .with_height(diameter)
+                    .boxed()
+                })
+                .boxed(),
+            )
+            .with_child(
+                Container::new(Align::new(title).boxed())
+                    .with_style(ContainerStyle {
+                        margin: Margin {
+                            left: tab_style.spacing,
+                            right: tab_style.spacing,
+                            ..Default::default()
+                        },
+                        ..Default::default()
+                    })
+                    .boxed(),
+            )
+            .with_child(
+                Align::new(
+                    ConstrainedBox::new(if hovered {
+                        let item_id = item.id();
+                        enum TabCloseButton {}
+                        let icon = Svg::new("icons/x_mark_thin_8.svg");
+                        MouseEventHandler::new::<TabCloseButton, _, _>(
+                            item_id,
+                            cx,
+                            |mouse_state, _| {
+                                if mouse_state.hovered {
+                                    icon.with_color(tab_style.icon_close_active).boxed()
+                                } else {
+                                    icon.with_color(tab_style.icon_close).boxed()
+                                }
+                            },
+                        )
+                        .with_padding(Padding::uniform(4.))
+                        .with_cursor_style(CursorStyle::PointingHand)
+                        .on_click(MouseButton::Left, {
+                            let pane = pane.clone();
+                            move |_, cx| {
+                                cx.dispatch_action(CloseItem {
+                                    item_id,
+                                    pane: pane.clone(),
+                                })
+                            }
+                        })
+                        .on_click(MouseButton::Middle, |_, cx| cx.propogate_event())
+                        .named("close-tab-icon")
+                    } else {
+                        Empty::new().boxed()
+                    })
+                    .with_width(tab_style.icon_width)
+                    .boxed(),
+                )
+                .boxed(),
+            )
+            .contained()
+            .with_style(tab_style.container);
+
+        if let Some(overlay) = overlay {
+            tab = tab.with_overlay_color(overlay);
+        }
+
+        tab.constrained().with_height(tab_style.height).boxed()
+    }
+
+    fn handle_dropped_item(pane: &WeakViewHandle<Pane>, index: usize, cx: &mut EventContext) {
+        if let Some((_, dragged_item)) = cx
+            .global::<DragAndDrop<Workspace>>()
+            .currently_dragged::<DraggedItem>()
+        {
+            cx.dispatch_action(MoveItem {
+                item_id: dragged_item.item.id(),
+                from: dragged_item.pane.clone(),
+                to: pane.clone(),
+                destination_index: index,
+            })
+        } else {
+            cx.propogate_event();
+        }
+    }
+
+    fn tab_overlay_color(
+        hovered: bool,
+        theme: &Theme,
+        cx: &mut RenderContext<Self>,
+    ) -> Option<Color> {
+        if hovered
+            && cx
+                .global::<DragAndDrop<Workspace>>()
+                .currently_dragged::<DraggedItem>()
+                .is_some()
+        {
+            Some(theme.workspace.tab_bar.drop_target_overlay_color)
+        } else {
+            None
+        }
+    }
 }
 
 impl Entity for Pane {
@@ -1097,7 +1307,7 @@ impl View for Pane {
                     Flex::column()
                         .with_child({
                             let mut tab_row = Flex::row()
-                                .with_child(self.render_tabs(cx).flex(1., true).named("tabs"));
+                                .with_child(self.render_tab_bar(cx).flex(1., true).named("tabs"));
 
                             if self.is_active {
                                 tab_row.add_children([
@@ -1124,12 +1334,11 @@ impl View for Pane {
                                         },
                                     )
                                     .with_cursor_style(CursorStyle::PointingHand)
-                                    .on_down(
-                                        MouseButton::Left,
-                                        |MouseButtonEvent { position, .. }, cx| {
-                                            cx.dispatch_action(DeployNewMenu { position });
-                                        },
-                                    )
+                                    .on_down(MouseButton::Left, |e, cx| {
+                                        cx.dispatch_action(DeployNewMenu {
+                                            position: e.position,
+                                        });
+                                    })
                                     .boxed(),
                                     MouseEventHandler::new::<SplitIcon, _, _>(
                                         1,
@@ -1154,12 +1363,11 @@ impl View for Pane {
                                         },
                                     )
                                     .with_cursor_style(CursorStyle::PointingHand)
-                                    .on_down(
-                                        MouseButton::Left,
-                                        |MouseButtonEvent { position, .. }, cx| {
-                                            cx.dispatch_action(DeploySplitMenu { position });
-                                        },
-                                    )
+                                    .on_down(MouseButton::Left, |e, cx| {
+                                        cx.dispatch_action(DeploySplitMenu {
+                                            position: e.position,
+                                        });
+                                    })
                                     .boxed(),
                                 ])
                             }
@@ -1167,7 +1375,7 @@ impl View for Pane {
                             tab_row
                                 .constrained()
                                 .with_height(cx.global::<Settings>().theme.workspace.tab_bar.height)
-                                .boxed()
+                                .named("tab bar")
                         })
                         .with_child(ChildView::new(&self.toolbar).boxed())
                         .with_child(ChildView::new(active_item).flex(1., true).boxed())
@@ -1324,3 +1532,252 @@ impl NavHistory {
         }
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use gpui::TestAppContext;
+    use project::FakeFs;
+
+    use crate::tests::TestItem;
+
+    use super::*;
+
+    #[gpui::test]
+    async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
+        cx.foreground().forbid_parking();
+        Settings::test_async(cx);
+        let fs = FakeFs::new(cx.background());
+
+        let project = Project::test(fs, None, cx).await;
+        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+
+        // 1. Add with a destination index
+        //   a. Add before the active item
+        set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx);
+        workspace.update(cx, |workspace, cx| {
+            Pane::add_item(
+                workspace,
+                &pane,
+                Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
+                false,
+                false,
+                Some(0),
+                cx,
+            );
+        });
+        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
+
+        //   b. Add after the active item
+        set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx);
+        workspace.update(cx, |workspace, cx| {
+            Pane::add_item(
+                workspace,
+                &pane,
+                Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
+                false,
+                false,
+                Some(2),
+                cx,
+            );
+        });
+        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
+
+        //   c. Add at the end of the item list (including off the length)
+        set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx);
+        workspace.update(cx, |workspace, cx| {
+            Pane::add_item(
+                workspace,
+                &pane,
+                Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
+                false,
+                false,
+                Some(5),
+                cx,
+            );
+        });
+        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
+
+        // 2. Add without a destination index
+        //   a. Add with active item at the start of the item list
+        set_labeled_items(&workspace, &pane, ["A*", "B", "C"], cx);
+        workspace.update(cx, |workspace, cx| {
+            Pane::add_item(
+                workspace,
+                &pane,
+                Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
+                false,
+                false,
+                None,
+                cx,
+            );
+        });
+        set_labeled_items(&workspace, &pane, ["A", "D*", "B", "C"], cx);
+
+        //   b. Add with active item at the end of the item list
+        set_labeled_items(&workspace, &pane, ["A", "B", "C*"], cx);
+        workspace.update(cx, |workspace, cx| {
+            Pane::add_item(
+                workspace,
+                &pane,
+                Box::new(cx.add_view(|_| TestItem::new().with_label("D"))),
+                false,
+                false,
+                None,
+                cx,
+            );
+        });
+        assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
+    }
+
+    #[gpui::test]
+    async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
+        cx.foreground().forbid_parking();
+        Settings::test_async(cx);
+        let fs = FakeFs::new(cx.background());
+
+        let project = Project::test(fs, None, cx).await;
+        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+
+        // 1. Add with a destination index
+        //   1a. Add before the active item
+        let [_, _, _, d] = set_labeled_items(&workspace, &pane, ["A", "B*", "C", "D"], cx);
+        workspace.update(cx, |workspace, cx| {
+            Pane::add_item(workspace, &pane, d, false, false, Some(0), cx);
+        });
+        assert_item_labels(&pane, ["D*", "A", "B", "C"], cx);
+
+        //   1b. Add after the active item
+        let [_, _, _, d] = set_labeled_items(&workspace, &pane, ["A", "B*", "C", "D"], cx);
+        workspace.update(cx, |workspace, cx| {
+            Pane::add_item(workspace, &pane, d, false, false, Some(2), cx);
+        });
+        assert_item_labels(&pane, ["A", "B", "D*", "C"], cx);
+
+        //   1c. Add at the end of the item list (including off the length)
+        let [a, _, _, _] = set_labeled_items(&workspace, &pane, ["A", "B*", "C", "D"], cx);
+        workspace.update(cx, |workspace, cx| {
+            Pane::add_item(workspace, &pane, a, false, false, Some(5), cx);
+        });
+        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
+
+        //   1d. Add same item to active index
+        let [_, b, _] = set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx);
+        workspace.update(cx, |workspace, cx| {
+            Pane::add_item(workspace, &pane, b, false, false, Some(1), cx);
+        });
+        assert_item_labels(&pane, ["A", "B*", "C"], cx);
+
+        //   1e. Add item to index after same item in last position
+        let [_, _, c] = set_labeled_items(&workspace, &pane, ["A", "B*", "C"], cx);
+        workspace.update(cx, |workspace, cx| {
+            Pane::add_item(workspace, &pane, c, false, false, Some(2), cx);
+        });
+        assert_item_labels(&pane, ["A", "B", "C*"], cx);
+
+        // 2. Add without a destination index
+        //   2a. Add with active item at the start of the item list
+        let [_, _, _, d] = set_labeled_items(&workspace, &pane, ["A*", "B", "C", "D"], cx);
+        workspace.update(cx, |workspace, cx| {
+            Pane::add_item(workspace, &pane, d, false, false, None, cx);
+        });
+        assert_item_labels(&pane, ["A", "D*", "B", "C"], cx);
+
+        //   2b. Add with active item at the end of the item list
+        let [a, _, _, _] = set_labeled_items(&workspace, &pane, ["A", "B", "C", "D*"], cx);
+        workspace.update(cx, |workspace, cx| {
+            Pane::add_item(workspace, &pane, a, false, false, None, cx);
+        });
+        assert_item_labels(&pane, ["B", "C", "D", "A*"], cx);
+
+        //   2c. Add active item to active item at end of list
+        let [_, _, c] = set_labeled_items(&workspace, &pane, ["A", "B", "C*"], cx);
+        workspace.update(cx, |workspace, cx| {
+            Pane::add_item(workspace, &pane, c, false, false, None, cx);
+        });
+        assert_item_labels(&pane, ["A", "B", "C*"], cx);
+
+        //   2d. Add active item to active item at start of list
+        let [a, _, _] = set_labeled_items(&workspace, &pane, ["A*", "B", "C"], cx);
+        workspace.update(cx, |workspace, cx| {
+            Pane::add_item(workspace, &pane, a, false, false, None, cx);
+        });
+        assert_item_labels(&pane, ["A*", "B", "C"], cx);
+    }
+
+    fn set_labeled_items<const COUNT: usize>(
+        workspace: &ViewHandle<Workspace>,
+        pane: &ViewHandle<Pane>,
+        labels: [&str; COUNT],
+        cx: &mut TestAppContext,
+    ) -> [Box<ViewHandle<TestItem>>; COUNT] {
+        pane.update(cx, |pane, _| {
+            pane.items.clear();
+        });
+
+        workspace.update(cx, |workspace, cx| {
+            let mut active_item_index = 0;
+
+            let mut index = 0;
+            let items = labels.map(|mut label| {
+                if label.ends_with("*") {
+                    label = label.trim_end_matches("*");
+                    active_item_index = index;
+                }
+
+                let labeled_item = Box::new(cx.add_view(|_| TestItem::new().with_label(label)));
+                Pane::add_item(
+                    workspace,
+                    pane,
+                    labeled_item.clone(),
+                    false,
+                    false,
+                    None,
+                    cx,
+                );
+                index += 1;
+                labeled_item
+            });
+
+            pane.update(cx, |pane, cx| {
+                pane.activate_item(active_item_index, false, false, cx)
+            });
+
+            items
+        })
+    }
+
+    // Assert the item label, with the active item label suffixed with a '*'
+    fn assert_item_labels<const COUNT: usize>(
+        pane: &ViewHandle<Pane>,
+        expected_states: [&str; COUNT],
+        cx: &mut TestAppContext,
+    ) {
+        pane.read_with(cx, |pane, cx| {
+            let actual_states = pane
+                .items
+                .iter()
+                .enumerate()
+                .map(|(ix, item)| {
+                    let mut state = item
+                        .to_any()
+                        .downcast::<TestItem>()
+                        .unwrap()
+                        .read(cx)
+                        .label
+                        .clone();
+                    if ix == pane.active_item_index {
+                        state.push('*');
+                    }
+                    state
+                })
+                .collect::<Vec<_>>();
+
+            assert_eq!(
+                actual_states, expected_states,
+                "pane items do not match expectation"
+            );
+        })
+    }
+}

crates/workspace/src/sidebar.rs 🔗

@@ -1,7 +1,7 @@
 use crate::StatusItemView;
 use gpui::{
     elements::*, impl_actions, platform::CursorStyle, AnyViewHandle, AppContext, Entity,
-    MouseButton, MouseMovedEvent, RenderContext, Subscription, View, ViewContext, ViewHandle,
+    MouseButton, RenderContext, Subscription, View, ViewContext, ViewHandle,
 };
 use serde::Deserialize;
 use settings::Settings;
@@ -189,26 +189,18 @@ impl Sidebar {
         })
         .with_cursor_style(CursorStyle::ResizeLeftRight)
         .on_down(MouseButton::Left, |_, _| {}) // This prevents the mouse down event from being propagated elsewhere
-        .on_drag(
-            MouseButton::Left,
-            move |old_position,
-                  MouseMovedEvent {
-                      position: new_position,
-                      ..
-                  },
-                  cx| {
-                let delta = new_position.x() - old_position.x();
-                let prev_width = *actual_width.borrow();
-                *custom_width.borrow_mut() = 0f32
-                    .max(match side {
-                        Side::Left => prev_width + delta,
-                        Side::Right => prev_width - delta,
-                    })
-                    .round();
+        .on_drag(MouseButton::Left, move |e, cx| {
+            let delta = e.position.x() - e.prev_mouse_position.x();
+            let prev_width = *actual_width.borrow();
+            *custom_width.borrow_mut() = 0f32
+                .max(match side {
+                    Side::Left => prev_width + delta,
+                    Side::Right => prev_width - delta,
+                })
+                .round();
 
-                cx.notify();
-            },
-        )
+            cx.notify();
+        })
         .boxed()
     }
 }

crates/workspace/src/workspace.rs 🔗

@@ -16,6 +16,7 @@ use client::{
 };
 use clock::ReplicaId;
 use collections::{hash_map, HashMap, HashSet};
+use drag_and_drop::DragAndDrop;
 use futures::{channel::oneshot, FutureExt};
 use gpui::{
     actions,
@@ -901,6 +902,9 @@ impl Workspace {
             status_bar
         });
 
+        let drag_and_drop = DragAndDrop::new(cx.weak_handle(), cx);
+        cx.set_global(drag_and_drop);
+
         let mut this = Workspace {
             modal: None,
             weak_self,
@@ -1444,8 +1448,8 @@ impl Workspace {
     }
 
     pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
-        let pane = self.active_pane().clone();
-        Pane::add_item(self, pane, item, true, true, cx);
+        let active_pane = self.active_pane().clone();
+        Pane::add_item(self, &active_pane, item, true, true, None, cx);
     }
 
     pub fn open_path(
@@ -1531,7 +1535,7 @@ impl Workspace {
                 .map(|ix| (pane.clone(), ix))
         });
         if let Some((pane, ix)) = result {
-            pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, false, cx));
+            pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx));
             true
         } else {
             false
@@ -1645,7 +1649,7 @@ impl Workspace {
         pane.read(cx).active_item().map(|item| {
             let new_pane = self.add_pane(cx);
             if let Some(clone) = item.clone_on_split(cx.as_mut()) {
-                Pane::add_item(self, new_pane.clone(), clone, true, true, cx);
+                Pane::add_item(self, &new_pane, clone, true, true, None, cx);
             }
             self.center.split(&pane, &new_pane, direction).unwrap();
             cx.notify();
@@ -2081,11 +2085,11 @@ impl Workspace {
         }
     }
 
-    fn render_disconnected_overlay(&self, cx: &AppContext) -> Option<ElementBox> {
+    fn render_disconnected_overlay(&self, cx: &mut RenderContext<Workspace>) -> Option<ElementBox> {
         if self.project.read(cx).is_read_only() {
-            let theme = &cx.global::<Settings>().theme;
             Some(
-                EventHandler::new(
+                MouseEventHandler::new::<Workspace, _, _>(0, cx, |_, cx| {
+                    let theme = &cx.global::<Settings>().theme;
                     Label::new(
                         "Your connection to the remote project has been lost.".to_string(),
                         theme.workspace.disconnected_overlay.text.clone(),
@@ -2093,9 +2097,9 @@ impl Workspace {
                     .aligned()
                     .contained()
                     .with_style(theme.workspace.disconnected_overlay.container)
-                    .boxed(),
-                )
-                .capture_all::<Self>(0)
+                    .boxed()
+                })
+                .capture_all()
                 .boxed(),
             )
         } else {
@@ -2388,7 +2392,7 @@ impl Workspace {
         }
 
         for (pane, item) in items_to_add {
-            Pane::add_item(self, pane.clone(), item.boxed_clone(), false, false, cx);
+            Pane::add_item(self, &pane, item.boxed_clone(), false, false, None, cx);
             if pane == self.active_pane {
                 pane.update(cx, |pane, cx| pane.focus_active_item(cx));
             }
@@ -2488,6 +2492,7 @@ impl View for Workspace {
                     .with_background_color(theme.workspace.background)
                     .boxed(),
             )
+            .with_children(DragAndDrop::render(cx))
             .with_children(self.render_disconnected_overlay(cx))
             .named("workspace")
     }
@@ -2999,7 +3004,7 @@ mod tests {
 
         let close_items = workspace.update(cx, |workspace, cx| {
             pane.update(cx, |pane, cx| {
-                pane.activate_item(1, true, true, false, cx);
+                pane.activate_item(1, true, true, cx);
                 assert_eq!(pane.active_item().unwrap().id(), item2.id());
             });
 
@@ -3101,7 +3106,7 @@ mod tests {
                 workspace.add_item(Box::new(cx.add_view(|_| item.clone())), cx);
             }
             left_pane.update(cx, |pane, cx| {
-                pane.activate_item(2, true, true, false, cx);
+                pane.activate_item(2, true, true, cx);
             });
 
             workspace
@@ -3325,8 +3330,9 @@ mod tests {
         });
     }
 
-    struct TestItem {
+    pub struct TestItem {
         state: String,
+        pub label: String,
         save_count: usize,
         save_as_count: usize,
         reload_count: usize,
@@ -3340,7 +3346,7 @@ mod tests {
         tab_detail: Cell<Option<usize>>,
     }
 
-    enum TestItemEvent {
+    pub enum TestItemEvent {
         Edit,
     }
 
@@ -3348,6 +3354,7 @@ mod tests {
         fn clone(&self) -> Self {
             Self {
                 state: self.state.clone(),
+                label: self.label.clone(),
                 save_count: self.save_count,
                 save_as_count: self.save_as_count,
                 reload_count: self.reload_count,
@@ -3364,9 +3371,10 @@ mod tests {
     }
 
     impl TestItem {
-        fn new() -> Self {
+        pub fn new() -> Self {
             Self {
                 state: String::new(),
+                label: String::new(),
                 save_count: 0,
                 save_as_count: 0,
                 reload_count: 0,
@@ -3381,6 +3389,11 @@ mod tests {
             }
         }
 
+        pub fn with_label(mut self, state: &str) -> Self {
+            self.label = state.to_string();
+            self
+        }
+
         fn set_state(&mut self, state: String, cx: &mut ViewContext<Self>) {
             self.push_to_nav_history(cx);
             self.state = state;

styles/package-lock.json 🔗

@@ -5,6 +5,7 @@
   "requires": true,
   "packages": {
     "": {
+      "name": "styles",
       "version": "1.0.0",
       "license": "ISC",
       "dependencies": {

styles/src/styleTree/components.ts 🔗

@@ -94,3 +94,11 @@ export function popoverShadow(theme: Theme) {
     offset: [1, 2],
   };
 }
+
+export function draggedShadow(theme: Theme) {
+  return {
+    blur: 6,
+    color: theme.shadow,
+    offset: [1, 2],
+  };
+}

styles/src/styleTree/tabBar.ts 🔗

@@ -1,5 +1,6 @@
 import Theme from "../themes/common/theme";
-import { iconColor, text, border, backgroundColor } from "./components";
+import { withOpacity } from "../utils/color";
+import { iconColor, text, border, backgroundColor, draggedShadow } from "./components";
 
 export default function tabBar(theme: Theme) {
   const height = 32;
@@ -55,9 +56,23 @@ export default function tabBar(theme: Theme) {
     },
   }
 
+  const draggedTab = {
+    ...activePaneActiveTab,
+    background: withOpacity(tab.background, 0.8),
+    border: {
+      ...tab.border,
+      top: false,
+      left: false,
+      right: false,
+      bottom: false,
+    },
+    shadow: draggedShadow(theme),
+  }
+
   return {
     height,
     background: backgroundColor(theme, 300),
+    dropTargetOverlayColor: withOpacity(theme.textColor.muted, 0.8),
     border: border(theme, "primary", {
       left: true,
       bottom: true,
@@ -71,6 +86,7 @@ export default function tabBar(theme: Theme) {
       activeTab: inactivePaneActiveTab,
       inactiveTab: inactivePaneInactiveTab,
     },
+    draggedTab,
     paneButton: {
       color: iconColor(theme, "secondary"),
       border: {