Add pane splitting by dragged item. Works, but the overlay doesn't clear quite right

K Simmons created

Change summary

crates/gpui/src/app.rs                          |  18 +
crates/gpui/src/app/test_app_context.rs         |   1 
crates/gpui/src/elements/mouse_event_handler.rs |  12 
crates/gpui/src/presenter.rs                    |  28 ++
crates/gpui/src/scene/mouse_region.rs           |   7 
crates/theme/src/theme.rs                       |   2 
crates/workspace/src/dock.rs                    |   4 
crates/workspace/src/pane.rs                    | 214 +++++++++++------
crates/workspace/src/pane_group.rs              | 232 +++---------------
crates/workspace/src/workspace.rs               |  56 ++++
styles/src/styleTree/tabBar.ts                  |   4 
styles/src/styleTree/workspace.ts               |   4 
12 files changed, 305 insertions(+), 277 deletions(-)

Detailed changes

crates/gpui/src/app.rs 🔗

@@ -21,6 +21,7 @@ use std::{
 use anyhow::{anyhow, Context, Result};
 use lazy_static::lazy_static;
 use parking_lot::Mutex;
+use pathfinder_geometry::vector::Vector2F;
 use postage::oneshot;
 use smallvec::SmallVec;
 use smol::prelude::*;
@@ -939,6 +940,7 @@ impl MutableAppContext {
                         window_id,
                         view_id,
                         titlebar_height,
+                        mouse_position: Default::default(),
                         hovered_region_ids: Default::default(),
                         clicked_region_ids: None,
                         refreshing: false,
@@ -3895,6 +3897,7 @@ pub struct RenderParams {
     pub window_id: usize,
     pub view_id: usize,
     pub titlebar_height: f32,
+    pub mouse_position: Vector2F,
     pub hovered_region_ids: HashSet<MouseRegionId>,
     pub clicked_region_ids: Option<(HashSet<MouseRegionId>, MouseButton)>,
     pub refreshing: bool,
@@ -3905,6 +3908,7 @@ pub struct RenderContext<'a, T: View> {
     pub(crate) window_id: usize,
     pub(crate) view_id: usize,
     pub(crate) view_type: PhantomData<T>,
+    pub(crate) mouse_position: Vector2F,
     pub(crate) hovered_region_ids: HashSet<MouseRegionId>,
     pub(crate) clicked_region_ids: Option<(HashSet<MouseRegionId>, MouseButton)>,
     pub app: &'a mut MutableAppContext,
@@ -3916,12 +3920,19 @@ pub struct RenderContext<'a, T: View> {
 #[derive(Clone, Default)]
 pub struct MouseState {
     hovered: bool,
+    mouse_position: Vector2F,
     clicked: Option<MouseButton>,
+    accessed_mouse_position: bool,
     accessed_hovered: bool,
     accessed_clicked: bool,
 }
 
 impl MouseState {
+    pub fn mouse_position(&mut self) -> Vector2F {
+        self.accessed_mouse_position = true;
+        self.mouse_position
+    }
+
     pub fn hovered(&mut self) -> bool {
         self.accessed_hovered = true;
         self.hovered
@@ -3932,6 +3943,10 @@ impl MouseState {
         self.clicked
     }
 
+    pub fn accessed_mouse_position(&self) -> bool {
+        self.accessed_mouse_position
+    }
+
     pub fn accessed_hovered(&self) -> bool {
         self.accessed_hovered
     }
@@ -3949,6 +3964,7 @@ impl<'a, V: View> RenderContext<'a, V> {
             view_id: params.view_id,
             view_type: PhantomData,
             titlebar_height: params.titlebar_height,
+            mouse_position: params.mouse_position,
             hovered_region_ids: params.hovered_region_ids.clone(),
             clicked_region_ids: params.clicked_region_ids.clone(),
             refreshing: params.refreshing,
@@ -3971,6 +3987,7 @@ impl<'a, V: View> RenderContext<'a, V> {
     pub fn mouse_state<Tag: 'static>(&self, region_id: usize) -> MouseState {
         let region_id = MouseRegionId::new::<Tag>(self.view_id, region_id);
         MouseState {
+            mouse_position: self.mouse_position.clone(),
             hovered: self.hovered_region_ids.contains(&region_id),
             clicked: self.clicked_region_ids.as_ref().and_then(|(ids, button)| {
                 if ids.contains(&region_id) {
@@ -3979,6 +3996,7 @@ impl<'a, V: View> RenderContext<'a, V> {
                     None
                 }
             }),
+            accessed_mouse_position: false,
             accessed_hovered: false,
             accessed_clicked: false,
         }

crates/gpui/src/app/test_app_context.rs 🔗

@@ -186,6 +186,7 @@ impl TestAppContext {
                 view_id: handle.id(),
                 view_type: PhantomData,
                 titlebar_height: 0.,
+                mouse_position: Default::default(),
                 hovered_region_ids: Default::default(),
                 clicked_region_ids: None,
                 refreshing: false,

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

@@ -21,6 +21,7 @@ pub struct MouseEventHandler<Tag: 'static> {
     cursor_style: Option<CursorStyle>,
     handlers: HandlerSet,
     hoverable: bool,
+    notify_on_move: bool,
     notify_on_hover: bool,
     notify_on_click: bool,
     above: bool,
@@ -28,9 +29,8 @@ pub struct MouseEventHandler<Tag: 'static> {
     _tag: PhantomData<Tag>,
 }
 
-// MouseEventHandler::new
-// MouseEventHandler::above
-
+/// 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
@@ -39,6 +39,7 @@ impl<Tag> MouseEventHandler<Tag> {
     {
         let mut mouse_state = cx.mouse_state::<Tag>(region_id);
         let child = render_child(&mut mouse_state, cx);
+        let notify_on_move = mouse_state.accessed_mouse_position();
         let notify_on_hover = mouse_state.accessed_hovered();
         let notify_on_click = mouse_state.accessed_clicked();
         Self {
@@ -46,6 +47,7 @@ impl<Tag> MouseEventHandler<Tag> {
             region_id,
             cursor_style: None,
             handlers: Default::default(),
+            notify_on_move,
             notify_on_hover,
             notify_on_click,
             hoverable: true,
@@ -55,6 +57,9 @@ impl<Tag> MouseEventHandler<Tag> {
         }
     }
 
+    /// 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,
@@ -183,6 +188,7 @@ impl<Tag> MouseEventHandler<Tag> {
                 self.handlers.clone(),
             )
             .with_hoverable(self.hoverable)
+            .with_notify_on_move(self.notify_on_move)
             .with_notify_on_hover(self.notify_on_hover)
             .with_notify_on_click(self.notify_on_click),
         );

crates/gpui/src/presenter.rs 🔗

@@ -90,6 +90,7 @@ impl Presenter {
                     window_id: self.window_id,
                     view_id: *view_id,
                     titlebar_height: self.titlebar_height,
+                    mouse_position: self.mouse_position.clone(),
                     hovered_region_ids: self.hovered_region_ids.clone(),
                     clicked_region_ids: self
                         .clicked_button
@@ -116,6 +117,7 @@ impl Presenter {
                         window_id: self.window_id,
                         view_id: *view_id,
                         titlebar_height: self.titlebar_height,
+                        mouse_position: self.mouse_position.clone(),
                         hovered_region_ids: self.hovered_region_ids.clone(),
                         clicked_region_ids: self
                             .clicked_button
@@ -183,6 +185,7 @@ impl Presenter {
             asset_cache: &self.asset_cache,
             view_stack: Vec::new(),
             refreshing,
+            mouse_position: self.mouse_position.clone(),
             hovered_region_ids: self.hovered_region_ids.clone(),
             clicked_region_ids: self
                 .clicked_button
@@ -231,6 +234,10 @@ impl Presenter {
         let mut mouse_events = SmallVec::<[_; 2]>::new();
         let mut notified_views: HashSet<usize> = Default::default();
 
+        if let Some(mouse_position) = event.position() {
+            self.mouse_position = mouse_position;
+        }
+
         // 1. Handle platform event. Keyboard events get dispatched immediately, while mouse events
         //    get mapped into the mouse-specific MouseEvent type.
         //  -> These are usually small: [Mouse Down] or [Mouse up, Click] or [Mouse Moved, Mouse Dragged?]
@@ -402,10 +409,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());
                         }
                     }
                 }
@@ -447,6 +454,16 @@ impl Presenter {
                         }
                     }
                 }
+                MouseEvent::Move(_) => {
+                    for (mouse_region, _) in self.mouse_regions.iter().rev() {
+                        if mouse_region.bounds.contains_point(self.mouse_position) {
+                            valid_regions.push(mouse_region.clone());
+                            if mouse_region.notify_on_move {
+                                notified_views.insert(mouse_region.id().view_id());
+                            }
+                        }
+                    }
+                }
                 _ => {
                     for (mouse_region, _) in self.mouse_regions.iter().rev() {
                         // Contains
@@ -551,6 +568,7 @@ pub struct LayoutContext<'a> {
     pub window_size: Vector2F,
     titlebar_height: f32,
     appearance: Appearance,
+    mouse_position: Vector2F,
     hovered_region_ids: HashSet<MouseRegionId>,
     clicked_region_ids: Option<(HashSet<MouseRegionId>, MouseButton)>,
 }
@@ -622,6 +640,7 @@ impl<'a> LayoutContext<'a> {
                 view_id: handle.id(),
                 view_type: PhantomData,
                 titlebar_height: self.titlebar_height,
+                mouse_position: self.mouse_position.clone(),
                 hovered_region_ids: self.hovered_region_ids.clone(),
                 clicked_region_ids: self.clicked_region_ids.clone(),
                 refreshing: self.refreshing,
@@ -861,6 +880,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/gpui/src/scene/mouse_region.rs 🔗

@@ -20,6 +20,7 @@ pub struct MouseRegion {
     pub bounds: RectF,
     pub handlers: HandlerSet,
     pub hoverable: bool,
+    pub notify_on_move: bool,
     pub notify_on_hover: bool,
     pub notify_on_click: bool,
 }
@@ -54,6 +55,7 @@ impl MouseRegion {
             bounds,
             handlers,
             hoverable: true,
+            notify_on_move: false,
             notify_on_hover: false,
             notify_on_click: false,
         }
@@ -136,6 +138,11 @@ impl MouseRegion {
         self
     }
 
+    pub fn with_notify_on_move(mut self, notify: bool) -> Self {
+        self.notify_on_move = notify;
+        self
+    }
+
     pub fn with_notify_on_hover(mut self, notify: bool) -> Self {
         self.notify_on_hover = notify;
         self

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 🔗

@@ -397,10 +397,10 @@ impl View for ToggleDockButton {
             }
         })
         .with_cursor_style(CursorStyle::PointingHand)
-        .on_up(MouseButton::Left, move |_, cx| {
+        .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;
-            Pane::handle_dropped_item(&dock_pane.downgrade(), drop_index, false, cx);
+            Pane::handle_dropped_item(event, &dock_pane.downgrade(), drop_index, false, None, cx);
         });
 
         if dock_position.is_visible() {

crates/workspace/src/pane.rs 🔗

@@ -2,7 +2,7 @@ use super::{ItemHandle, SplitDirection};
 use crate::{
     dock::{icon_for_dock_anchor, AnchorDockBottom, AnchorDockRight, ExpandDock, HideDock},
     toolbar::Toolbar,
-    Item, NewFile, NewSearch, NewTerminal, WeakItemHandle, Workspace,
+    Item, NewFile, NewSearch, NewTerminal, SplitWithItem, WeakItemHandle, Workspace,
 };
 use anyhow::Result;
 use collections::{HashMap, HashSet, VecDeque};
@@ -19,6 +19,7 @@ use gpui::{
     },
     impl_actions, impl_internal_actions,
     platform::{CursorStyle, NavigationDirection},
+    scene::MouseUp,
     Action, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, EventContext,
     ModelHandle, MouseButton, MutableAppContext, PromptLevel, Quad, RenderContext, Task, View,
     ViewContext, ViewHandle, WeakViewHandle,
@@ -98,7 +99,7 @@ impl_internal_actions!(
         DeploySplitMenu,
         DeployNewMenu,
         DeployDockMenu,
-        MoveItem
+        MoveItem,
     ]
 );
 
@@ -1097,7 +1098,7 @@ impl Pane {
                             ix == 0,
                             detail,
                             hovered,
-                            Self::tab_overlay_color(hovered, theme.as_ref(), cx),
+                            Self::tab_overlay_color(hovered, cx),
                             tab_style,
                             cx,
                         )
@@ -1124,7 +1125,7 @@ impl Pane {
                 })
                 .on_up(MouseButton::Left, {
                     let pane = pane.clone();
-                    move |_, cx: &mut EventContext| Pane::handle_dropped_item(&pane, ix, true, cx)
+                    move |event, cx| Pane::handle_dropped_item(event, &pane, ix, true, None, cx)
                 })
                 .as_draggable(
                     DraggedItem {
@@ -1164,14 +1165,14 @@ impl Pane {
                     .with_style(filler_style.container)
                     .with_border(filler_style.container.border);
 
-                if let Some(overlay) = Self::tab_overlay_color(mouse_state.hovered(), &theme, cx) {
+                if let Some(overlay) = Self::tab_overlay_color(mouse_state.hovered(), cx) {
                     filler = filler.with_overlay_color(overlay);
                 }
 
                 filler.boxed()
             })
-            .on_up(MouseButton::Left, move |_, cx| {
-                Pane::handle_dropped_item(&pane, filler_index, true, cx)
+            .on_up(MouseButton::Left, move |event, cx| {
+                Pane::handle_dropped_item(event, &pane, filler_index, true, None, cx)
             })
             .flex(1., true)
             .named("filler"),
@@ -1320,17 +1321,64 @@ impl Pane {
         tab.constrained().with_height(tab_style.height).boxed()
     }
 
+    fn render_tab_bar_buttons(
+        &mut self,
+        theme: &Theme,
+        cx: &mut RenderContext<Self>,
+    ) -> 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()
+    }
+
     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 pane != &dragged_item.pane || allow_same_pane {
+            if let Some(split_direction) = split_margin
+                .and_then(|margin| Self::drop_split_direction(event.position, event.region, margin))
+            {
+                cx.dispatch_action(SplitWithItem {
+                    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(),
@@ -1343,18 +1391,39 @@ impl Pane {
         }
     }
 
-    fn tab_overlay_color(
-        hovered: bool,
-        theme: &Theme,
-        cx: &mut RenderContext<Self>,
-    ) -> Option<Color> {
+    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 tab_overlay_color(hovered: bool, 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)
+            Some(
+                cx.global::<Settings>()
+                    .theme
+                    .workspace
+                    .drop_target_overlay_color,
+            )
         } else {
             None
         }
@@ -1389,55 +1458,7 @@ impl View for Pane {
                                 // 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
@@ -1453,25 +1474,66 @@ impl View for Pane {
                                 MouseEventHandler::<PaneContentTabDropTarget>::above(
                                     0,
                                     cx,
-                                    |_, cx| {
-                                        Flex::column()
+                                    |state, cx| {
+                                        let overlay_color = Self::tab_overlay_color(true, cx);
+                                        let drag_position = cx
+                                            .global::<DragAndDrop<Workspace>>()
+                                            .currently_dragged::<DraggedItem>(cx.window_id())
+                                            .map(|_| state.mouse_position());
+
+                                        Stack::new()
                                             .with_child(
-                                                ChildView::new(&self.toolbar, cx)
-                                                    .expanded()
-                                                    .boxed(),
-                                            )
-                                            .with_child(
-                                                ChildView::new(active_item, cx)
-                                                    .flex(1., true)
+                                                Flex::column()
+                                                    .with_child(
+                                                        ChildView::new(&self.toolbar, cx)
+                                                            .expanded()
+                                                            .boxed(),
+                                                    )
+                                                    .with_child(
+                                                        ChildView::new(active_item, cx)
+                                                            .flex(1., true)
+                                                            .boxed(),
+                                                    )
                                                     .boxed(),
                                             )
+                                            .with_children(drag_position.map(|drag_position| {
+                                                Canvas::new(move |region, _, cx| {
+                                                    let overlay_region =
+                                                        if let Some(split_direction) =
+                                                            Self::drop_split_direction(
+                                                                drag_position,
+                                                                region,
+                                                                100., /* Replace with theme value */
+                                                            )
+                                                        {
+                                                            split_direction.along_edge(region, 100.)
+                                                        } else {
+                                                            region
+                                                        };
+
+                                                    cx.scene.push_quad(Quad {
+                                                        bounds: overlay_region,
+                                                        background: overlay_color,
+                                                        border: Default::default(),
+                                                        corner_radius: 0.,
+                                                    });
+                                                })
+                                                .boxed()
+                                            }))
                                             .boxed()
                                     },
                                 )
                                 .on_up(MouseButton::Left, {
                                     let pane = cx.handle();
-                                    move |_, cx: &mut EventContext| {
-                                        Pane::handle_dropped_item(&pane, drop_index, false, cx)
+                                    move |event, cx| {
+                                        Pane::handle_dropped_item(
+                                            event,
+                                            &pane,
+                                            drop_index,
+                                            false,
+                                            Some(100.), /* Use theme value */
+                                            cx,
+                                        )
                                     }
                                 })
                                 .flex(1., true)
@@ -1493,8 +1555,8 @@ impl View for Pane {
                         })
                         .on_up(MouseButton::Left, {
                             let pane = this.clone();
-                            move |_, cx: &mut EventContext| {
-                                Pane::handle_dropped_item(&pane, 0, true, cx)
+                            move |event, cx| {
+                                Pane::handle_dropped_item(event, &pane, 0, true, None, cx)
                             }
                         })
                         .boxed()

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,12 @@ pub struct OpenSharedScreen {
     pub peer_id: PeerId,
 }
 
+pub struct SplitWithItem {
+    pane_to_split: WeakViewHandle<Pane>,
+    split_direction: SplitDirection,
+    item_id_to_move: usize,
+}
+
 impl_internal_actions!(
     workspace,
     [
@@ -133,7 +139,8 @@ impl_internal_actions!(
         ToggleFollow,
         JoinProject,
         OpenSharedScreen,
-        RemoveWorktreeFromProject
+        RemoveWorktreeFromProject,
+        SplitWithItem,
     ]
 );
 impl_actions!(workspace, [ActivatePane]);
@@ -206,6 +213,22 @@ 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 {
+             pane_to_split,
+             item_id_to_move,
+             split_direction,
+         }: &_,
+         cx| {
+            workspace.split_pane_with_item(
+                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 +1973,35 @@ impl Workspace {
         })
     }
 
+    pub fn split_pane_with_item(
+        &mut self,
+        pane_to_split: WeakViewHandle<Pane>,
+        item_id_to_move: usize,
+        split_direction: SplitDirection,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if let Some(pane_to_split) = pane_to_split.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,
+                pane_to_split.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);

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),
+      0.6
+    ),
   };
 }