Merge pull request #1791 from zed-industries/drag-tabs-more-places

Kay Simmons created

Drag tabs more places

Change summary

Cargo.lock                                         |   1 
assets/keymaps/default.json                        |   6 
crates/drag_and_drop/src/drag_and_drop.rs          |   2 
crates/editor/Cargo.toml                           |   1 
crates/editor/src/editor_tests.rs                  |   2 
crates/editor/src/element.rs                       |  12 
crates/gpui/src/elements/flex.rs                   |   2 
crates/gpui/src/elements/mouse_event_handler.rs    |  68 ++
crates/gpui/src/elements/overlay.rs                |  35 
crates/gpui/src/elements/stack.rs                  |  21 
crates/gpui/src/elements/uniform_list.rs           |   2 
crates/gpui/src/presenter.rs                       |  25 
crates/theme/src/theme.rs                          |   2 
crates/workspace/src/dock.rs                       |  79 ++
crates/workspace/src/pane.rs                       | 383 ++++++---------
crates/workspace/src/pane/dragged_item_receiver.rs | 142 +++++
crates/workspace/src/pane_group.rs                 | 232 +-------
crates/workspace/src/workspace.rs                  |  64 ++
crates/zed/src/zed.rs                              |   6 
styles/src/styleTree/tabBar.ts                     |   4 
styles/src/styleTree/workspace.ts                  |   4 
21 files changed, 598 insertions(+), 495 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1736,6 +1736,7 @@ dependencies = [
  "collections",
  "context_menu",
  "ctor",
+ "drag_and_drop",
  "env_logger",
  "futures 0.3.24",
  "fuzzy",

assets/keymaps/default.json 🔗

@@ -431,6 +431,12 @@
             "shift-escape": "dock::HideDock"
         }
     },
+    {
+        "context": "Pane",
+        "bindings": {
+            "cmd-escape": "dock::MoveActiveItemToDock"
+        }
+    },
     {
         "context": "ProjectPanel",
         "bindings": {

crates/drag_and_drop/src/drag_and_drop.rs 🔗

@@ -125,7 +125,7 @@ impl<V: View> DragAndDrop<V> {
                             cx.defer(|cx| {
                                 cx.update_global::<Self, _, _>(|this, cx| this.stop_dragging(cx));
                             });
-                            cx.propogate_event();
+                            cx.propagate_event();
                         })
                         .on_up_out(MouseButton::Left, |_, cx| {
                             cx.defer(|cx| {

crates/editor/Cargo.toml 🔗

@@ -20,6 +20,7 @@ test-support = [
 ]
 
 [dependencies]
+drag_and_drop = { path = "../drag_and_drop" }
 text = { path = "../text" }
 clock = { path = "../clock" }
 collections = { path = "../collections" }

crates/editor/src/editor_tests.rs 🔗

@@ -1,5 +1,6 @@
 use std::{cell::RefCell, rc::Rc, time::Instant};
 
+use drag_and_drop::DragAndDrop;
 use futures::StreamExt;
 use indoc::indoc;
 use unindent::Unindent;
@@ -472,6 +473,7 @@ fn test_clone(cx: &mut gpui::MutableAppContext) {
 #[gpui::test]
 fn test_navigation_history(cx: &mut gpui::MutableAppContext) {
     cx.set_global(Settings::test(cx));
+    cx.set_global(DragAndDrop::<Workspace>::default());
     use workspace::Item;
     let (_, pane) = cx.add_window(Default::default(), |cx| Pane::new(None, cx));
     let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);

crates/editor/src/element.rs 🔗

@@ -137,7 +137,7 @@ impl EditorElement {
                             gutter_bounds,
                             cx,
                         ) {
-                            cx.propogate_event();
+                            cx.propagate_event();
                         }
                     }
                 })
@@ -150,7 +150,7 @@ impl EditorElement {
                             text_bounds,
                             cx,
                         ) {
-                            cx.propogate_event();
+                            cx.propagate_event();
                         }
                     }
                 })
@@ -167,7 +167,7 @@ impl EditorElement {
                             text_bounds,
                             cx,
                         ) {
-                            cx.propogate_event()
+                            cx.propagate_event()
                         }
                     }
                 })
@@ -182,7 +182,7 @@ impl EditorElement {
                             text_bounds,
                             cx,
                         ) {
-                            cx.propogate_event()
+                            cx.propagate_event()
                         }
                     }
                 })
@@ -190,7 +190,7 @@ impl EditorElement {
                     let position_map = position_map.clone();
                     move |e, cx| {
                         if !Self::mouse_moved(e.platform_event, &position_map, text_bounds, cx) {
-                            cx.propogate_event()
+                            cx.propagate_event()
                         }
                     }
                 })
@@ -199,7 +199,7 @@ impl EditorElement {
                     move |e, cx| {
                         if !Self::scroll(e.position, e.delta, e.precise, &position_map, bounds, cx)
                         {
-                            cx.propogate_event()
+                            cx.propagate_event()
                         }
                     }
                 }),

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

@@ -23,10 +23,13 @@ pub struct MouseEventHandler<Tag: 'static> {
     hoverable: bool,
     notify_on_hover: bool,
     notify_on_click: bool,
+    above: bool,
     padding: Padding,
     _tag: PhantomData<Tag>,
 }
 
+/// Element which provides a render_child callback with a MouseState and paints a mouse
+/// region under (or above) it for easy mouse event handling.
 impl<Tag> MouseEventHandler<Tag> {
     pub fn new<V, F>(region_id: usize, cx: &mut RenderContext<V>, render_child: F) -> Self
     where
@@ -45,11 +48,25 @@ impl<Tag> MouseEventHandler<Tag> {
             notify_on_hover,
             notify_on_click,
             hoverable: true,
+            above: false,
             padding: Default::default(),
             _tag: PhantomData,
         }
     }
 
+    /// Modifies the MouseEventHandler to render the MouseRegion above the child element. Useful
+    /// for drag and drop handling and similar events which should be captured before the child
+    /// gets the opportunity
+    pub fn above<V, F>(region_id: usize, cx: &mut RenderContext<V>, render_child: F) -> Self
+    where
+        V: View,
+        F: FnOnce(&mut MouseState, &mut RenderContext<V>) -> ElementBox,
+    {
+        let mut handler = Self::new(region_id, cx, render_child);
+        handler.above = true;
+        handler
+    }
+
     pub fn with_cursor_style(mut self, cursor: CursorStyle) -> Self {
         self.cursor_style = Some(cursor);
         self
@@ -149,6 +166,29 @@ impl<Tag> MouseEventHandler<Tag> {
         )
         .round_out()
     }
+
+    fn paint_regions(&self, bounds: RectF, visible_bounds: RectF, cx: &mut PaintContext) {
+        let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default();
+        let hit_bounds = self.hit_bounds(visible_bounds);
+
+        if let Some(style) = self.cursor_style {
+            cx.scene.push_cursor_region(CursorRegion {
+                bounds: hit_bounds,
+                style,
+            });
+        }
+        cx.scene.push_mouse_region(
+            MouseRegion::from_handlers::<Tag>(
+                cx.current_view_id(),
+                self.region_id,
+                hit_bounds,
+                self.handlers.clone(),
+            )
+            .with_hoverable(self.hoverable)
+            .with_notify_on_hover(self.notify_on_hover)
+            .with_notify_on_click(self.notify_on_click),
+        );
+    }
 }
 
 impl<Tag> Element for MouseEventHandler<Tag> {
@@ -170,28 +210,16 @@ impl<Tag> Element for MouseEventHandler<Tag> {
         _: &mut Self::LayoutState,
         cx: &mut PaintContext,
     ) -> Self::PaintState {
-        let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default();
-        let hit_bounds = self.hit_bounds(visible_bounds);
-        if let Some(style) = self.cursor_style {
-            cx.scene.push_cursor_region(CursorRegion {
-                bounds: hit_bounds,
-                style,
+        if self.above {
+            self.child.paint(bounds.origin(), visible_bounds, cx);
+
+            cx.paint_layer(None, |cx| {
+                self.paint_regions(bounds, visible_bounds, cx);
             });
+        } else {
+            self.paint_regions(bounds, visible_bounds, cx);
+            self.child.paint(bounds.origin(), visible_bounds, cx);
         }
-
-        cx.scene.push_mouse_region(
-            MouseRegion::from_handlers::<Tag>(
-                cx.current_view_id(),
-                self.region_id,
-                hit_bounds,
-                self.handlers.clone(),
-            )
-            .with_hoverable(self.hoverable)
-            .with_notify_on_hover(self.notify_on_hover)
-            .with_notify_on_click(self.notify_on_click),
-        );
-
-        self.child.paint(bounds.origin(), visible_bounds, cx);
     }
 
     fn rect_for_text_range(

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

@@ -204,25 +204,24 @@ impl Element for Overlay {
             OverlayFitMode::None => {}
         }
 
-        cx.scene.push_stacking_context(None);
-
-        if self.hoverable {
-            enum OverlayHoverCapture {}
-            // Block hovers in lower stacking contexts
-            cx.scene
-                .push_mouse_region(MouseRegion::new::<OverlayHoverCapture>(
-                    cx.current_view_id(),
-                    cx.current_view_id(),
-                    bounds,
-                ));
-        }
+        cx.paint_stacking_context(None, |cx| {
+            if self.hoverable {
+                enum OverlayHoverCapture {}
+                // Block hovers in lower stacking contexts
+                cx.scene
+                    .push_mouse_region(MouseRegion::new::<OverlayHoverCapture>(
+                        cx.current_view_id(),
+                        cx.current_view_id(),
+                        bounds,
+                    ));
+            }
 
-        self.child.paint(
-            bounds.origin(),
-            RectF::new(Vector2F::zero(), cx.window_size),
-            cx,
-        );
-        cx.scene.pop_stacking_context();
+            self.child.paint(
+                bounds.origin(),
+                RectF::new(Vector2F::zero(), cx.window_size),
+                cx,
+            );
+        });
     }
 
     fn rect_for_text_range(

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

@@ -7,6 +7,8 @@ use crate::{
     DebugContext, Element, ElementBox, LayoutContext, PaintContext, SizeConstraint,
 };
 
+/// Element which renders it's children in a stack on top of each other.
+/// The first child determines the size of the others.
 #[derive(Default)]
 pub struct Stack {
     children: Vec<ElementBox>,
@@ -24,13 +26,20 @@ impl Element for Stack {
 
     fn layout(
         &mut self,
-        constraint: SizeConstraint,
+        mut constraint: SizeConstraint,
         cx: &mut LayoutContext,
     ) -> (Vector2F, Self::LayoutState) {
         let mut size = constraint.min;
-        for child in &mut self.children {
-            size = size.max(child.layout(constraint, cx));
+        let mut children = self.children.iter_mut();
+        if let Some(bottom_child) = children.next() {
+            size = bottom_child.layout(constraint, cx);
+            constraint = SizeConstraint::strict(size);
+        }
+
+        for child in children {
+            child.layout(constraint, cx);
         }
+
         (size, ())
     }
 
@@ -42,9 +51,9 @@ impl Element for Stack {
         cx: &mut PaintContext,
     ) -> Self::PaintState {
         for child in &mut self.children {
-            cx.scene.push_layer(None);
-            child.paint(bounds.origin(), visible_bounds, cx);
-            cx.scene.pop_layer();
+            cx.paint_layer(None, |cx| {
+                child.paint(bounds.origin(), visible_bounds, cx);
+            });
         }
     }
 

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

@@ -304,7 +304,7 @@ impl Element for UniformList {
                       },
                       cx| {
                     if !Self::scroll(state.clone(), position, delta, precise, scroll_max, cx) {
-                        cx.propogate_event();
+                        cx.propagate_event();
                     }
                 }
             }),

crates/gpui/src/presenter.rs 🔗

@@ -402,10 +402,10 @@ impl Presenter {
                 MouseEvent::Down(_) | MouseEvent::Up(_) => {
                     for (region, _) in self.mouse_regions.iter().rev() {
                         if region.bounds.contains_point(self.mouse_position) {
+                            valid_regions.push(region.clone());
                             if region.notify_on_click {
                                 notified_views.insert(region.id().view_id());
                             }
-                            valid_regions.push(region.clone());
                         }
                     }
                 }
@@ -485,9 +485,7 @@ impl Presenter {
                     event_cx.handled = true;
                     event_cx.with_current_view(valid_region.id().view_id(), {
                         let region_event = mouse_event.clone();
-                        |cx| {
-                            callback(region_event, cx);
-                        }
+                        |cx| callback(region_event, cx)
                     });
                 }
 
@@ -707,6 +705,16 @@ impl<'a> PaintContext<'a> {
         }
     }
 
+    #[inline]
+    pub fn paint_stacking_context<F>(&mut self, clip_bounds: Option<RectF>, f: F)
+    where
+        F: FnOnce(&mut Self),
+    {
+        self.scene.push_stacking_context(clip_bounds);
+        f(self);
+        self.scene.pop_stacking_context();
+    }
+
     #[inline]
     pub fn paint_layer<F>(&mut self, clip_bounds: Option<RectF>, f: F)
     where
@@ -794,7 +802,7 @@ impl<'a> EventContext<'a> {
         self.notify_count
     }
 
-    pub fn propogate_event(&mut self) {
+    pub fn propagate_event(&mut self) {
         self.handled = false;
     }
 }
@@ -853,6 +861,13 @@ impl Axis {
             Self::Vertical => Self::Horizontal,
         }
     }
+
+    pub fn component(&self, point: Vector2F) -> f32 {
+        match self {
+            Self::Horizontal => point.x(),
+            Self::Vertical => point.y(),
+        }
+    }
 }
 
 impl ToJson for Axis {

crates/theme/src/theme.rs 🔗

@@ -62,6 +62,7 @@ pub struct Workspace {
     pub joining_project_message: ContainedText,
     pub external_location_message: ContainedText,
     pub dock: Dock,
+    pub drop_target_overlay_color: Color,
 }
 
 #[derive(Clone, Deserialize, Default)]
@@ -150,7 +151,6 @@ pub struct TabBar {
     pub inactive_pane: TabStyles,
     pub dragged_tab: Tab,
     pub height: f32,
-    pub drop_target_overlay_color: Color,
 }
 
 impl TabBar {

crates/workspace/src/dock.rs 🔗

@@ -1,7 +1,7 @@
 use collections::HashMap;
 use gpui::{
     actions,
-    elements::{ChildView, Container, Empty, MouseEventHandler, Side, Svg},
+    elements::{ChildView, Container, Empty, MouseEventHandler, ParentElement, Side, Stack, Svg},
     impl_internal_actions, Border, CursorStyle, Element, ElementBox, Entity, MouseButton,
     MutableAppContext, RenderContext, View, ViewContext, ViewHandle, WeakViewHandle,
 };
@@ -9,7 +9,9 @@ use serde::Deserialize;
 use settings::{DockAnchor, Settings};
 use theme::Theme;
 
-use crate::{sidebar::SidebarSide, ItemHandle, Pane, StatusItemView, Workspace};
+use crate::{
+    handle_dropped_item, sidebar::SidebarSide, ItemHandle, Pane, StatusItemView, Workspace,
+};
 
 #[derive(PartialEq, Clone, Deserialize)]
 pub struct MoveDock(pub DockAnchor);
@@ -24,7 +26,8 @@ actions!(
         HideDock,
         AnchorDockRight,
         AnchorDockBottom,
-        ExpandDock
+        ExpandDock,
+        MoveActiveItemToDock,
     ]
 );
 impl_internal_actions!(dock, [MoveDock, AddDefaultItemToDock]);
@@ -48,6 +51,30 @@ pub fn init(cx: &mut MutableAppContext) {
             Dock::move_dock(workspace, &MoveDock(DockAnchor::Expanded), cx)
         },
     );
+    cx.add_action(
+        |workspace: &mut Workspace, _: &MoveActiveItemToDock, cx: &mut ViewContext<Workspace>| {
+            if let Some(active_item) = workspace.active_item(cx) {
+                let item_id = active_item.id();
+
+                let from = workspace.active_pane();
+                let to = workspace.dock_pane();
+                if from.id() == to.id() {
+                    return;
+                }
+
+                let destination_index = to.read(cx).items_len() + 1;
+
+                Pane::move_item(
+                    workspace,
+                    from.clone(),
+                    to.clone(),
+                    item_id,
+                    destination_index,
+                    cx,
+                );
+            }
+        },
+    );
 }
 
 #[derive(Copy, Clone, PartialEq, Eq, Debug)]
@@ -283,25 +310,34 @@ impl Dock {
                 DockAnchor::Expanded => {
                     enum ExpandedDockWash {}
                     enum ExpandedDockPane {}
-                    Container::new(
-                        MouseEventHandler::<ExpandedDockWash>::new(0, cx, |_state, cx| {
+                    Stack::new()
+                        .with_child(
+                            // Render wash under the dock which when clicked hides it
+                            MouseEventHandler::<ExpandedDockWash>::new(0, cx, |_, _| {
+                                Empty::new()
+                                    .contained()
+                                    .with_background_color(style.wash_color)
+                                    .boxed()
+                            })
+                            .capture_all()
+                            .on_down(MouseButton::Left, |_, cx| {
+                                cx.dispatch_action(HideDock);
+                            })
+                            .with_cursor_style(CursorStyle::Arrow)
+                            .boxed(),
+                        )
+                        .with_child(
                             MouseEventHandler::<ExpandedDockPane>::new(0, cx, |_state, cx| {
                                 ChildView::new(&self.pane, cx).boxed()
                             })
+                            // Make sure all events directly under the dock pane
+                            // are captured
                             .capture_all()
                             .contained()
                             .with_style(style.maximized)
-                            .boxed()
-                        })
-                        .capture_all()
-                        .on_down(MouseButton::Left, |_, cx| {
-                            cx.dispatch_action(HideDock);
-                        })
-                        .with_cursor_style(CursorStyle::Arrow)
-                        .boxed(),
-                    )
-                    .with_background_color(style.wash_color)
-                    .boxed()
+                            .boxed(),
+                        )
+                        .boxed()
                 }
             })
     }
@@ -338,9 +374,11 @@ impl View for ToggleDockButton {
             return Empty::new().boxed();
         }
 
-        let dock_position = workspace.unwrap().read(cx).dock.position;
+        let workspace = workspace.unwrap();
+        let dock_position = workspace.read(cx).dock.position;
 
         let theme = cx.global::<Settings>().theme.clone();
+
         let button = MouseEventHandler::<Self>::new(0, cx, {
             let theme = theme.clone();
             move |state, _| {
@@ -361,7 +399,12 @@ impl View for ToggleDockButton {
                     .boxed()
             }
         })
-        .with_cursor_style(CursorStyle::PointingHand);
+        .with_cursor_style(CursorStyle::PointingHand)
+        .on_up(MouseButton::Left, move |event, cx| {
+            let dock_pane = workspace.read(cx.app).dock_pane();
+            let drop_index = dock_pane.read(cx.app).items_len() + 1;
+            handle_dropped_item(event, &dock_pane.downgrade(), drop_index, false, None, cx);
+        });
 
         if dock_position.is_visible() {
             button

crates/workspace/src/pane.rs 🔗

@@ -1,3 +1,5 @@
+mod dragged_item_receiver;
+
 use super::{ItemHandle, SplitDirection};
 use crate::{
     dock::{icon_for_dock_anchor, AnchorDockBottom, AnchorDockRight, ExpandDock, HideDock},
@@ -7,11 +9,11 @@ use crate::{
 use anyhow::Result;
 use collections::{HashMap, HashSet, VecDeque};
 use context_menu::{ContextMenu, ContextMenuItem};
-use drag_and_drop::{DragAndDrop, Draggable};
+use drag_and_drop::Draggable;
+pub use dragged_item_receiver::{dragged_item_receiver, handle_dropped_item};
 use futures::StreamExt;
 use gpui::{
     actions,
-    color::Color,
     elements::*,
     geometry::{
         rect::RectF,
@@ -98,7 +100,7 @@ impl_internal_actions!(
         DeploySplitMenu,
         DeployNewMenu,
         DeployDockMenu,
-        MoveItem
+        MoveItem,
     ]
 );
 
@@ -575,6 +577,10 @@ impl Pane {
         }
     }
 
+    pub fn items_len(&self) -> usize {
+        self.items.len()
+    }
+
     pub fn items(&self) -> impl Iterator<Item = &Box<dyn ItemHandle>> {
         self.items.iter()
     }
@@ -943,11 +949,11 @@ impl Pane {
         }
     }
 
-    fn move_item(
+    pub fn move_item(
         workspace: &mut Workspace,
         from: ViewHandle<Pane>,
         to: ViewHandle<Pane>,
-        item_to_move: usize,
+        item_id_to_move: usize,
         destination_index: usize,
         cx: &mut ViewContext<Workspace>,
     ) {
@@ -955,7 +961,7 @@ impl Pane {
             .read(cx)
             .items()
             .enumerate()
-            .find(|(_, item_handle)| item_handle.id() == item_to_move);
+            .find(|(_, item_handle)| item_handle.id() == item_id_to_move);
 
         if item_to_move.is_none() {
             log::warn!("Tried to move item handle which was not in `from` pane. Maybe tab was closed during drop");
@@ -1051,133 +1057,107 @@ impl Pane {
 
     fn render_tabs(&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::<Tabs>::new(0, cx, |_, cx| {
-            let autoscroll = if mem::take(&mut self.autoscroll) {
-                Some(self.active_item_index)
-            } else {
-                None
-            };
-
-            let pane_active = self.is_active;
-
-            let mut row = Flex::row().scrollable::<Tabs, _>(1, autoscroll, cx);
-            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 tab_active = ix == self.active_item_index;
+        let autoscroll = if mem::take(&mut self.autoscroll) {
+            Some(self.active_item_index)
+        } else {
+            None
+        };
 
-                row.add_child({
-                    MouseEventHandler::<Tab>::new(ix, cx, {
-                        let item = item.clone();
-                        let pane = pane.clone();
-                        let detail = detail.clone();
+        let pane_active = self.is_active;
 
+        enum Tabs {}
+        let mut row = Flex::row().scrollable::<Tabs, _>(1, autoscroll, cx);
+        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 tab_active = ix == self.active_item_index;
+
+            row.add_child({
+                enum Tab {}
+                dragged_item_receiver::<Tab, _>(ix, ix, true, None, 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, ix == 0, detail, hovered, tab_style, cx)
+                    }
+                })
+                .with_cursor_style(if pane_active && tab_active {
+                    CursorStyle::Arrow
+                } else {
+                    CursorStyle::PointingHand
+                })
+                .on_down(MouseButton::Left, move |_, cx| {
+                    cx.dispatch_action(ActivateItem(ix));
+                    cx.propagate_event();
+                })
+                .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(),
+                        })
+                    }
+                })
+                .as_draggable(
+                    DraggedItem {
+                        item,
+                        pane: pane.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();
+                        let detail = detail.clone();
+                        move |dragged_item, cx: &mut RenderContext<Workspace>| {
+                            let tab_style = &theme.workspace.tab_bar.dragged_tab;
                             Self::render_tab(
-                                &item,
-                                pane,
-                                ix == 0,
+                                &dragged_item.item,
+                                dragged_item.pane.clone(),
+                                false,
                                 detail,
-                                hovered,
-                                Self::tab_overlay_color(hovered, theme.as_ref(), cx),
-                                tab_style,
+                                false,
+                                &tab_style,
                                 cx,
                             )
                         }
-                    })
-                    .with_cursor_style(if pane_active && tab_active {
-                        CursorStyle::Arrow
-                    } else {
-                        CursorStyle::PointingHand
-                    })
-                    .on_down(MouseButton::Left, move |_, cx| {
-                        cx.dispatch_action(ActivateItem(ix));
-                    })
-                    .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;
-                                Self::render_tab(
-                                    &dragged_item.item,
-                                    dragged_item.pane.clone(),
-                                    false,
-                                    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(
-                MouseEventHandler::<Filler>::new(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);
-                    }
+                    },
+                )
+                .boxed()
+            })
+        }
 
-                    filler.boxed()
-                })
-                .flex(1., true)
-                .named("filler"),
-            );
+        // Use the inactive tab style along with the current pane's active status to decide how to render
+        // the filler
+        let filler_index = self.items.len();
+        let filler_style = theme.workspace.tab_bar.tab_style(pane_active, false);
+        enum Filler {}
+        row.add_child(
+            dragged_item_receiver::<Filler, _>(0, filler_index, true, None, cx, |_, _| {
+                Empty::new()
+                    .contained()
+                    .with_style(filler_style.container)
+                    .with_border(filler_style.container.border)
+                    .boxed()
+            })
+            .flex(1., true)
+            .named("filler"),
+        );
 
-            row.boxed()
-        })
-        .on_up(MouseButton::Left, move |_, cx| {
-            Pane::handle_dropped_item(&pane, filler_index, cx)
-        })
+        row
     }
 
     fn tab_details(&self, cx: &AppContext) -> Vec<usize> {
@@ -1223,7 +1203,6 @@ impl Pane {
         first: bool,
         detail: Option<usize>,
         hovered: bool,
-        overlay: Option<Color>,
         tab_style: &theme::Tab,
         cx: &mut RenderContext<V>,
     ) -> ElementBox {
@@ -1233,7 +1212,7 @@ impl Pane {
             container.border.left = false;
         }
 
-        let mut tab = Flex::row()
+        Flex::row()
             .with_child(
                 Align::new({
                     let diameter = 7.0;
@@ -1301,7 +1280,6 @@ impl Pane {
                                 })
                             }
                         })
-                        .on_click(MouseButton::Middle, |_, cx| cx.propogate_event())
                         .named("close-tab-icon")
                     } else {
                         Empty::new().boxed()
@@ -1312,46 +1290,46 @@ impl Pane {
                 .boxed(),
             )
             .contained()
-            .with_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.window_id)
-        {
-            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();
-        }
+            .with_style(container)
+            .constrained()
+            .with_height(tab_style.height)
+            .boxed()
     }
 
-    fn tab_overlay_color(
-        hovered: bool,
+    fn render_tab_bar_buttons(
+        &mut self,
         theme: &Theme,
         cx: &mut RenderContext<Self>,
-    ) -> Option<Color> {
-        if hovered
-            && cx
-                .global::<DragAndDrop<Workspace>>()
-                .currently_dragged::<DraggedItem>(cx.window_id())
-                .is_some()
-        {
-            Some(theme.workspace.tab_bar.drop_target_overlay_color)
-        } else {
-            None
-        }
+    ) -> ElementBox {
+        Flex::row()
+            // New menu
+            .with_child(tab_bar_button(0, "icons/plus_12.svg", cx, |position| {
+                DeployNewMenu { position }
+            }))
+            .with_child(
+                self.docked
+                    .map(|anchor| {
+                        // Add the dock menu button if this pane is a dock
+                        let dock_icon = icon_for_dock_anchor(anchor);
+
+                        tab_bar_button(1, dock_icon, cx, |position| DeployDockMenu { position })
+                    })
+                    .unwrap_or_else(|| {
+                        // Add the split menu if this pane is not a dock
+                        tab_bar_button(2, "icons/split_12.svg", cx, |position| DeploySplitMenu {
+                            position,
+                        })
+                    }),
+            )
+            // Add the close dock button if this pane is a dock
+            .with_children(
+                self.docked
+                    .map(|_| tab_bar_button(3, "icons/x_mark_thin_8.svg", cx, |_| HideDock)),
+            )
+            .contained()
+            .with_style(theme.workspace.tab_bar.pane_button_container)
+            .flex(1., false)
+            .boxed()
     }
 }
 
@@ -1376,60 +1354,12 @@ impl View for Pane {
                         Flex::column()
                             .with_child({
                                 let mut tab_row = Flex::row()
-                                    .with_child(self.render_tabs(cx).flex(1.0, true).named("tabs"));
+                                    .with_child(self.render_tabs(cx).flex(1., true).named("tabs"));
 
                                 // Render pane buttons
                                 let theme = cx.global::<Settings>().theme.clone();
                                 if self.is_active {
-                                    tab_row.add_child(
-                                        Flex::row()
-                                            // New menu
-                                            .with_child(tab_bar_button(
-                                                0,
-                                                "icons/plus_12.svg",
-                                                cx,
-                                                |position| DeployNewMenu { position },
-                                            ))
-                                            .with_child(
-                                                self.docked
-                                                    .map(|anchor| {
-                                                        // Add the dock menu button if this pane is a dock
-                                                        let dock_icon =
-                                                            icon_for_dock_anchor(anchor);
-
-                                                        tab_bar_button(
-                                                            1,
-                                                            dock_icon,
-                                                            cx,
-                                                            |position| DeployDockMenu { position },
-                                                        )
-                                                    })
-                                                    .unwrap_or_else(|| {
-                                                        // Add the split menu if this pane is not a dock
-                                                        tab_bar_button(
-                                                            2,
-                                                            "icons/split_12.svg",
-                                                            cx,
-                                                            |position| DeploySplitMenu { position },
-                                                        )
-                                                    }),
-                                            )
-                                            // Add the close dock button if this pane is a dock
-                                            .with_children(self.docked.map(|_| {
-                                                tab_bar_button(
-                                                    3,
-                                                    "icons/x_mark_thin_8.svg",
-                                                    cx,
-                                                    |_| HideDock,
-                                                )
-                                            }))
-                                            .contained()
-                                            .with_style(
-                                                theme.workspace.tab_bar.pane_button_container,
-                                            )
-                                            .flex(1., false)
-                                            .boxed(),
-                                    )
+                                    tab_row.add_child(self.render_tab_bar_buttons(&theme, cx))
                                 }
 
                                 tab_row
@@ -1440,14 +1370,39 @@ impl View for Pane {
                                     .flex(1., false)
                                     .named("tab bar")
                             })
-                            .with_child(ChildView::new(&self.toolbar, cx).expanded().boxed())
-                            .with_child(ChildView::new(active_item, cx).flex(1., true).boxed())
+                            .with_child({
+                                enum PaneContentTabDropTarget {}
+                                dragged_item_receiver::<PaneContentTabDropTarget, _>(
+                                    0,
+                                    self.active_item_index + 1,
+                                    false,
+                                    Some(100.),
+                                    cx,
+                                    {
+                                        let toolbar = self.toolbar.clone();
+                                        move |_, cx| {
+                                            Flex::column()
+                                                .with_child(
+                                                    ChildView::new(&toolbar, cx).expanded().boxed(),
+                                                )
+                                                .with_child(
+                                                    ChildView::new(active_item, cx)
+                                                        .flex(1., true)
+                                                        .boxed(),
+                                                )
+                                                .boxed()
+                                        }
+                                    },
+                                )
+                                .flex(1., true)
+                                .boxed()
+                            })
                             .boxed()
                     } else {
                         enum EmptyPane {}
                         let theme = cx.global::<Settings>().theme.clone();
 
-                        MouseEventHandler::<EmptyPane>::new(0, cx, |_, _| {
+                        dragged_item_receiver::<EmptyPane, _>(0, 0, false, None, cx, |_, _| {
                             Empty::new()
                                 .contained()
                                 .with_background_color(theme.workspace.background)
@@ -1456,10 +1411,6 @@ impl View for Pane {
                         .on_down(MouseButton::Left, |_, cx| {
                             cx.focus_parent_view();
                         })
-                        .on_up(MouseButton::Left, {
-                            let pane = this.clone();
-                            move |_, cx: &mut EventContext| Pane::handle_dropped_item(&pane, 0, cx)
-                        })
                         .boxed()
                     }
                 })

crates/workspace/src/pane/dragged_item_receiver.rs 🔗

@@ -0,0 +1,142 @@
+use drag_and_drop::DragAndDrop;
+use gpui::{
+    color::Color,
+    elements::{Canvas, MouseEventHandler, ParentElement, Stack},
+    geometry::{rect::RectF, vector::Vector2F},
+    scene::MouseUp,
+    AppContext, Element, ElementBox, EventContext, MouseButton, MouseState, Quad, RenderContext,
+    WeakViewHandle,
+};
+use settings::Settings;
+
+use crate::{MoveItem, Pane, SplitDirection, SplitWithItem, Workspace};
+
+use super::DraggedItem;
+
+pub fn dragged_item_receiver<Tag, F>(
+    region_id: usize,
+    drop_index: usize,
+    allow_same_pane: bool,
+    split_margin: Option<f32>,
+    cx: &mut RenderContext<Pane>,
+    render_child: F,
+) -> MouseEventHandler<Tag>
+where
+    Tag: 'static,
+    F: FnOnce(&mut MouseState, &mut RenderContext<Pane>) -> ElementBox,
+{
+    MouseEventHandler::<Tag>::above(region_id, cx, |state, cx| {
+        // Observing hovered will cause a render when the mouse enters regardless
+        // of if mouse position was accessed before
+        let hovered = state.hovered();
+        let drag_position = cx
+            .global::<DragAndDrop<Workspace>>()
+            .currently_dragged::<DraggedItem>(cx.window_id())
+            .filter(|_| hovered)
+            .map(|(drag_position, _)| drag_position);
+
+        Stack::new()
+            .with_child(render_child(state, cx))
+            .with_children(drag_position.map(|drag_position| {
+                Canvas::new(move |bounds, _, cx| {
+                    if bounds.contains_point(drag_position) {
+                        let overlay_region = split_margin
+                            .and_then(|split_margin| {
+                                drop_split_direction(drag_position, bounds, split_margin)
+                                    .map(|dir| (dir, split_margin))
+                            })
+                            .map(|(dir, margin)| dir.along_edge(bounds, margin))
+                            .unwrap_or(bounds);
+
+                        cx.paint_stacking_context(None, |cx| {
+                            cx.scene.push_quad(Quad {
+                                bounds: overlay_region,
+                                background: Some(overlay_color(cx)),
+                                border: Default::default(),
+                                corner_radius: 0.,
+                            });
+                        });
+                    }
+                })
+                .boxed()
+            }))
+            .boxed()
+    })
+    .on_up(MouseButton::Left, {
+        let pane = cx.handle();
+        move |event, cx| {
+            handle_dropped_item(event, &pane, drop_index, allow_same_pane, split_margin, cx);
+            cx.notify();
+        }
+    })
+    .on_move(|_, cx| {
+        if cx
+            .global::<DragAndDrop<Workspace>>()
+            .currently_dragged::<DraggedItem>(cx.window_id())
+            .is_some()
+        {
+            cx.notify();
+        }
+    })
+}
+
+pub fn handle_dropped_item(
+    event: MouseUp,
+    pane: &WeakViewHandle<Pane>,
+    index: usize,
+    allow_same_pane: bool,
+    split_margin: Option<f32>,
+    cx: &mut EventContext,
+) {
+    if let Some((_, dragged_item)) = cx
+        .global::<DragAndDrop<Workspace>>()
+        .currently_dragged::<DraggedItem>(cx.window_id)
+    {
+        if let Some(split_direction) = split_margin
+            .and_then(|margin| drop_split_direction(event.position, event.region, margin))
+        {
+            cx.dispatch_action(SplitWithItem {
+                from: dragged_item.pane.clone(),
+                item_id_to_move: dragged_item.item.id(),
+                pane_to_split: pane.clone(),
+                split_direction,
+            });
+        } else if pane != &dragged_item.pane || allow_same_pane {
+            // If no split margin or not close enough to the edge, just move the item
+            cx.dispatch_action(MoveItem {
+                item_id: dragged_item.item.id(),
+                from: dragged_item.pane.clone(),
+                to: pane.clone(),
+                destination_index: index,
+            })
+        }
+    } else {
+        cx.propagate_event();
+    }
+}
+
+fn drop_split_direction(
+    position: Vector2F,
+    region: RectF,
+    split_margin: f32,
+) -> Option<SplitDirection> {
+    let mut min_direction = None;
+    let mut min_distance = split_margin;
+    for direction in SplitDirection::all() {
+        let edge_distance = (direction.edge(region) - direction.axis().component(position)).abs();
+
+        if edge_distance < min_distance {
+            min_direction = Some(direction);
+            min_distance = edge_distance;
+        }
+    }
+
+    min_direction
+}
+
+fn overlay_color(cx: &AppContext) -> Color {
+    cx.global::<Settings>()
+        .theme
+        .workspace
+        .drop_target_overlay_color
+}

crates/workspace/src/pane_group.rs 🔗

@@ -2,7 +2,9 @@ use crate::{FollowerStatesByLeader, JoinProject, Pane, Workspace};
 use anyhow::{anyhow, Result};
 use call::{ActiveCall, ParticipantLocation};
 use gpui::{
-    elements::*, Axis, Border, CursorStyle, ModelHandle, MouseButton, RenderContext, ViewHandle,
+    elements::*,
+    geometry::{rect::RectF, vector::Vector2F},
+    Axis, Border, CursorStyle, ModelHandle, MouseButton, RenderContext, ViewHandle,
 };
 use project::Project;
 use serde::Deserialize;
@@ -263,9 +265,7 @@ impl PaneAxis {
         new_pane: &ViewHandle<Pane>,
         direction: SplitDirection,
     ) -> Result<()> {
-        use SplitDirection::*;
-
-        for (idx, member) in self.members.iter_mut().enumerate() {
+        for (mut idx, member) in self.members.iter_mut().enumerate() {
             match member {
                 Member::Axis(axis) => {
                     if axis.split(old_pane, new_pane, direction).is_ok() {
@@ -274,15 +274,12 @@ impl PaneAxis {
                 }
                 Member::Pane(pane) => {
                     if pane == old_pane {
-                        if direction.matches_axis(self.axis) {
-                            match direction {
-                                Up | Left => {
-                                    self.members.insert(idx, Member::Pane(new_pane.clone()));
-                                }
-                                Down | Right => {
-                                    self.members.insert(idx + 1, Member::Pane(new_pane.clone()));
-                                }
+                        if direction.axis() == self.axis {
+                            if direction.increasing() {
+                                idx += 1;
                             }
+
+                            self.members.insert(idx, Member::Pane(new_pane.clone()));
                         } else {
                             *member =
                                 Member::new_axis(old_pane.clone(), new_pane.clone(), direction);
@@ -374,187 +371,46 @@ pub enum SplitDirection {
 }
 
 impl SplitDirection {
-    fn matches_axis(self, orientation: Axis) -> bool {
-        use Axis::*;
-        use SplitDirection::*;
+    pub fn all() -> [Self; 4] {
+        [Self::Up, Self::Down, Self::Left, Self::Right]
+    }
 
+    pub fn edge(&self, rect: RectF) -> f32 {
         match self {
-            Up | Down => match orientation {
-                Vertical => true,
-                Horizontal => false,
-            },
-            Left | Right => match orientation {
-                Vertical => false,
-                Horizontal => true,
-            },
+            Self::Up => rect.min_y(),
+            Self::Down => rect.max_y(),
+            Self::Left => rect.min_x(),
+            Self::Right => rect.max_x(),
         }
     }
-}
-
-#[cfg(test)]
-mod tests {
-    // use super::*;
-    // use serde_json::json;
-
-    // #[test]
-    // fn test_split_and_remove() -> Result<()> {
-    //     let mut group = PaneGroup::new(1);
-    //     assert_eq!(
-    //         serde_json::to_value(&group)?,
-    //         json!({
-    //             "type": "pane",
-    //             "paneId": 1,
-    //         })
-    //     );
-
-    //     group.split(1, 2, SplitDirection::Right)?;
-    //     assert_eq!(
-    //         serde_json::to_value(&group)?,
-    //         json!({
-    //             "type": "axis",
-    //             "orientation": "horizontal",
-    //             "members": [
-    //                 {"type": "pane", "paneId": 1},
-    //                 {"type": "pane", "paneId": 2},
-    //             ]
-    //         })
-    //     );
-
-    //     group.split(2, 3, SplitDirection::Up)?;
-    //     assert_eq!(
-    //         serde_json::to_value(&group)?,
-    //         json!({
-    //             "type": "axis",
-    //             "orientation": "horizontal",
-    //             "members": [
-    //                 {"type": "pane", "paneId": 1},
-    //                 {
-    //                     "type": "axis",
-    //                     "orientation": "vertical",
-    //                     "members": [
-    //                         {"type": "pane", "paneId": 3},
-    //                         {"type": "pane", "paneId": 2},
-    //                     ]
-    //                 },
-    //             ]
-    //         })
-    //     );
-
-    //     group.split(1, 4, SplitDirection::Right)?;
-    //     assert_eq!(
-    //         serde_json::to_value(&group)?,
-    //         json!({
-    //             "type": "axis",
-    //             "orientation": "horizontal",
-    //             "members": [
-    //                 {"type": "pane", "paneId": 1},
-    //                 {"type": "pane", "paneId": 4},
-    //                 {
-    //                     "type": "axis",
-    //                     "orientation": "vertical",
-    //                     "members": [
-    //                         {"type": "pane", "paneId": 3},
-    //                         {"type": "pane", "paneId": 2},
-    //                     ]
-    //                 },
-    //             ]
-    //         })
-    //     );
 
-    //     group.split(2, 5, SplitDirection::Up)?;
-    //     assert_eq!(
-    //         serde_json::to_value(&group)?,
-    //         json!({
-    //             "type": "axis",
-    //             "orientation": "horizontal",
-    //             "members": [
-    //                 {"type": "pane", "paneId": 1},
-    //                 {"type": "pane", "paneId": 4},
-    //                 {
-    //                     "type": "axis",
-    //                     "orientation": "vertical",
-    //                     "members": [
-    //                         {"type": "pane", "paneId": 3},
-    //                         {"type": "pane", "paneId": 5},
-    //                         {"type": "pane", "paneId": 2},
-    //                     ]
-    //                 },
-    //             ]
-    //         })
-    //     );
-
-    //     assert_eq!(true, group.remove(5)?);
-    //     assert_eq!(
-    //         serde_json::to_value(&group)?,
-    //         json!({
-    //             "type": "axis",
-    //             "orientation": "horizontal",
-    //             "members": [
-    //                 {"type": "pane", "paneId": 1},
-    //                 {"type": "pane", "paneId": 4},
-    //                 {
-    //                     "type": "axis",
-    //                     "orientation": "vertical",
-    //                     "members": [
-    //                         {"type": "pane", "paneId": 3},
-    //                         {"type": "pane", "paneId": 2},
-    //                     ]
-    //                 },
-    //             ]
-    //         })
-    //     );
-
-    //     assert_eq!(true, group.remove(4)?);
-    //     assert_eq!(
-    //         serde_json::to_value(&group)?,
-    //         json!({
-    //             "type": "axis",
-    //             "orientation": "horizontal",
-    //             "members": [
-    //                 {"type": "pane", "paneId": 1},
-    //                 {
-    //                     "type": "axis",
-    //                     "orientation": "vertical",
-    //                     "members": [
-    //                         {"type": "pane", "paneId": 3},
-    //                         {"type": "pane", "paneId": 2},
-    //                     ]
-    //                 },
-    //             ]
-    //         })
-    //     );
-
-    //     assert_eq!(true, group.remove(3)?);
-    //     assert_eq!(
-    //         serde_json::to_value(&group)?,
-    //         json!({
-    //             "type": "axis",
-    //             "orientation": "horizontal",
-    //             "members": [
-    //                 {"type": "pane", "paneId": 1},
-    //                 {"type": "pane", "paneId": 2},
-    //             ]
-    //         })
-    //     );
-
-    //     assert_eq!(true, group.remove(2)?);
-    //     assert_eq!(
-    //         serde_json::to_value(&group)?,
-    //         json!({
-    //             "type": "pane",
-    //             "paneId": 1,
-    //         })
-    //     );
+    // Returns a new rectangle which shares an edge in SplitDirection and has `size` along SplitDirection
+    pub fn along_edge(&self, rect: RectF, size: f32) -> RectF {
+        match self {
+            Self::Up => RectF::new(rect.origin(), Vector2F::new(rect.width(), size)),
+            Self::Down => RectF::new(
+                rect.lower_left() - Vector2F::new(0., size),
+                Vector2F::new(rect.width(), size),
+            ),
+            Self::Left => RectF::new(rect.origin(), Vector2F::new(size, rect.height())),
+            Self::Right => RectF::new(
+                rect.upper_right() - Vector2F::new(size, 0.),
+                Vector2F::new(size, rect.height()),
+            ),
+        }
+    }
 
-    //     assert_eq!(false, group.remove(1)?);
-    //     assert_eq!(
-    //         serde_json::to_value(&group)?,
-    //         json!({
-    //             "type": "pane",
-    //             "paneId": 1,
-    //         })
-    //     );
+    pub fn axis(&self) -> Axis {
+        match self {
+            Self::Up | Self::Down => Axis::Vertical,
+            Self::Left | Self::Right => Axis::Horizontal,
+        }
+    }
 
-    //     Ok(())
-    // }
+    pub fn increasing(&self) -> bool {
+        match self {
+            Self::Left | Self::Up => false,
+            Self::Down | Self::Right => true,
+        }
+    }
 }

crates/workspace/src/workspace.rs 🔗

@@ -100,7 +100,7 @@ actions!(
         ToggleLeftSidebar,
         ToggleRightSidebar,
         NewTerminal,
-        NewSearch
+        NewSearch,
     ]
 );
 
@@ -126,6 +126,14 @@ pub struct OpenSharedScreen {
     pub peer_id: PeerId,
 }
 
+#[derive(Clone, PartialEq)]
+pub struct SplitWithItem {
+    from: WeakViewHandle<Pane>,
+    pane_to_split: WeakViewHandle<Pane>,
+    split_direction: SplitDirection,
+    item_id_to_move: usize,
+}
+
 impl_internal_actions!(
     workspace,
     [
@@ -133,7 +141,8 @@ impl_internal_actions!(
         ToggleFollow,
         JoinProject,
         OpenSharedScreen,
-        RemoveWorktreeFromProject
+        RemoveWorktreeFromProject,
+        SplitWithItem,
     ]
 );
 impl_actions!(workspace, [ActivatePane]);
@@ -206,6 +215,24 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
         workspace.toggle_sidebar(SidebarSide::Right, cx);
     });
     cx.add_action(Workspace::activate_pane_at_index);
+    cx.add_action(
+        |workspace: &mut Workspace,
+         SplitWithItem {
+             from,
+             pane_to_split,
+             item_id_to_move,
+             split_direction,
+         }: &_,
+         cx| {
+            workspace.split_pane_with_item(
+                from.clone(),
+                pane_to_split.clone(),
+                *item_id_to_move,
+                *split_direction,
+                cx,
+            )
+        },
+    );
 
     let client = &app_state.client;
     client.add_view_request_handler(Workspace::handle_follow);
@@ -1950,6 +1977,29 @@ impl Workspace {
         })
     }
 
+    pub fn split_pane_with_item(
+        &mut self,
+        from: WeakViewHandle<Pane>,
+        pane_to_split: WeakViewHandle<Pane>,
+        item_id_to_move: usize,
+        split_direction: SplitDirection,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if let Some((pane_to_split, from)) = pane_to_split.upgrade(cx).zip(from.upgrade(cx)) {
+            if &pane_to_split == self.dock_pane() {
+                warn!("Can't split dock pane.");
+                return;
+            }
+
+            let new_pane = self.add_pane(cx);
+            Pane::move_item(self, from.clone(), new_pane.clone(), item_id_to_move, 0, cx);
+            self.center
+                .split(&pane_to_split, &new_pane, split_direction)
+                .unwrap();
+            cx.notify();
+        }
+    }
+
     fn remove_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
         if self.center.remove(&pane).unwrap() {
             self.panes.retain(|p| p != &pane);
@@ -3175,7 +3225,7 @@ mod tests {
 
         cx.foreground().run_until_parked();
         pane.read_with(cx, |pane, _| {
-            assert_eq!(pane.items().count(), 4);
+            assert_eq!(pane.items_len(), 4);
             assert_eq!(pane.active_item().unwrap().id(), item1.id());
         });
 
@@ -3185,7 +3235,7 @@ mod tests {
             assert_eq!(item1.read(cx).save_count, 1);
             assert_eq!(item1.read(cx).save_as_count, 0);
             assert_eq!(item1.read(cx).reload_count, 0);
-            assert_eq!(pane.items().count(), 3);
+            assert_eq!(pane.items_len(), 3);
             assert_eq!(pane.active_item().unwrap().id(), item3.id());
         });
 
@@ -3195,7 +3245,7 @@ mod tests {
             assert_eq!(item3.read(cx).save_count, 0);
             assert_eq!(item3.read(cx).save_as_count, 0);
             assert_eq!(item3.read(cx).reload_count, 1);
-            assert_eq!(pane.items().count(), 2);
+            assert_eq!(pane.items_len(), 2);
             assert_eq!(pane.active_item().unwrap().id(), item4.id());
         });
 
@@ -3207,7 +3257,7 @@ mod tests {
             assert_eq!(item4.read(cx).save_count, 0);
             assert_eq!(item4.read(cx).save_as_count, 1);
             assert_eq!(item4.read(cx).reload_count, 0);
-            assert_eq!(pane.items().count(), 1);
+            assert_eq!(pane.items_len(), 1);
             assert_eq!(pane.active_item().unwrap().id(), item2.id());
         });
     }
@@ -3309,7 +3359,7 @@ mod tests {
         cx.foreground().run_until_parked();
         close.await.unwrap();
         left_pane.read_with(cx, |pane, _| {
-            assert_eq!(pane.items().count(), 0);
+            assert_eq!(pane.items_len(), 0);
         });
     }
 

crates/zed/src/zed.rs 🔗

@@ -811,7 +811,7 @@ mod tests {
                 pane.active_item().unwrap().project_path(cx),
                 Some(file1.clone())
             );
-            assert_eq!(pane.items().count(), 1);
+            assert_eq!(pane.items_len(), 1);
         });
 
         // Open the second entry
@@ -825,7 +825,7 @@ mod tests {
                 pane.active_item().unwrap().project_path(cx),
                 Some(file2.clone())
             );
-            assert_eq!(pane.items().count(), 2);
+            assert_eq!(pane.items_len(), 2);
         });
 
         // Open the first entry again. The existing pane item is activated.
@@ -841,7 +841,7 @@ mod tests {
                 pane.active_item().unwrap().project_path(cx),
                 Some(file1.clone())
             );
-            assert_eq!(pane.items().count(), 2);
+            assert_eq!(pane.items_len(), 2);
         });
 
         // Split the pane with the first entry, then open the second entry again.

styles/src/styleTree/tabBar.ts 🔗

@@ -75,10 +75,6 @@ export default function tabBar(colorScheme: ColorScheme) {
   return {
     height,
     background: background(layer),
-    dropTargetOverlayColor: withOpacity(
-      foreground(layer),
-      0.6
-    ),
     activePane: {
       activeTab: activePaneActiveTab,
       inactiveTab: tab,

styles/src/styleTree/workspace.ts 🔗

@@ -227,5 +227,9 @@ export default function workspace(colorScheme: ColorScheme) {
         shadow: colorScheme.modalShadow,
       },
     },
+    dropTargetOverlayColor: withOpacity(
+      foreground(layer, "variant"),
+      0.5
+    ),
   };
 }