Allowing dragging tabs onto panes and pane edges (#3641)

Max Brunsfeld created

Change summary

crates/gpui2/src/elements/div.rs | 110 +++++++++++++++++----------------
crates/workspace2/src/pane.rs    |  77 +++++++++++++++++++++++
2 files changed, 134 insertions(+), 53 deletions(-)

Detailed changes

crates/gpui2/src/elements/div.rs 🔗

@@ -740,7 +740,7 @@ impl Interactivity {
         if style
             .background
             .as_ref()
-            .is_some_and(|fill| fill.color().is_some_and(|color| !color.is_transparent()))
+            .is_some_and(|fill| fill.color().is_some())
         {
             cx.with_z_index(style.z_index.unwrap_or(0), |cx| cx.add_opaque_layer(bounds))
         }
@@ -1120,78 +1120,84 @@ impl Interactivity {
         let mut style = Style::default();
         style.refine(&self.base_style);
 
-        if let Some(focus_handle) = self.tracked_focus_handle.as_ref() {
-            if let Some(in_focus_style) = self.in_focus_style.as_ref() {
-                if focus_handle.within_focused(cx) {
-                    style.refine(in_focus_style);
+        cx.with_z_index(style.z_index.unwrap_or(0), |cx| {
+            if let Some(focus_handle) = self.tracked_focus_handle.as_ref() {
+                if let Some(in_focus_style) = self.in_focus_style.as_ref() {
+                    if focus_handle.within_focused(cx) {
+                        style.refine(in_focus_style);
+                    }
                 }
-            }
 
-            if let Some(focus_style) = self.focus_style.as_ref() {
-                if focus_handle.is_focused(cx) {
-                    style.refine(focus_style);
+                if let Some(focus_style) = self.focus_style.as_ref() {
+                    if focus_handle.is_focused(cx) {
+                        style.refine(focus_style);
+                    }
                 }
             }
-        }
 
-        if let Some(bounds) = bounds {
-            let mouse_position = cx.mouse_position();
-            if let Some(group_hover) = self.group_hover_style.as_ref() {
-                if let Some(group_bounds) = GroupBounds::get(&group_hover.group, cx) {
-                    if group_bounds.contains(&mouse_position)
+            if let Some(bounds) = bounds {
+                let mouse_position = cx.mouse_position();
+                if let Some(group_hover) = self.group_hover_style.as_ref() {
+                    if let Some(group_bounds) = GroupBounds::get(&group_hover.group, cx) {
+                        if group_bounds.contains(&mouse_position)
+                            && cx.was_top_layer(&mouse_position, cx.stacking_order())
+                        {
+                            style.refine(&group_hover.style);
+                        }
+                    }
+                }
+                if let Some(hover_style) = self.hover_style.as_ref() {
+                    if bounds
+                        .intersect(&cx.content_mask().bounds)
+                        .contains(&mouse_position)
                         && cx.was_top_layer(&mouse_position, cx.stacking_order())
                     {
-                        style.refine(&group_hover.style);
+                        style.refine(hover_style);
                     }
                 }
-            }
-            if let Some(hover_style) = self.hover_style.as_ref() {
-                if bounds
-                    .intersect(&cx.content_mask().bounds)
-                    .contains(&mouse_position)
-                    && cx.was_top_layer(&mouse_position, cx.stacking_order())
-                {
-                    style.refine(hover_style);
-                }
-            }
 
-            if let Some(drag) = cx.active_drag.take() {
-                for (state_type, group_drag_style) in &self.group_drag_over_styles {
-                    if let Some(group_bounds) = GroupBounds::get(&group_drag_style.group, cx) {
+                if let Some(drag) = cx.active_drag.take() {
+                    for (state_type, group_drag_style) in &self.group_drag_over_styles {
+                        if let Some(group_bounds) = GroupBounds::get(&group_drag_style.group, cx) {
+                            if *state_type == drag.view.entity_type()
+                                && group_bounds.contains(&mouse_position)
+                            {
+                                style.refine(&group_drag_style.style);
+                            }
+                        }
+                    }
+
+                    for (state_type, drag_over_style) in &self.drag_over_styles {
                         if *state_type == drag.view.entity_type()
-                            && group_bounds.contains(&mouse_position)
+                            && bounds
+                                .intersect(&cx.content_mask().bounds)
+                                .contains(&mouse_position)
+                            && cx.was_top_layer_under_active_drag(
+                                &mouse_position,
+                                cx.stacking_order(),
+                            )
                         {
-                            style.refine(&group_drag_style.style);
+                            style.refine(drag_over_style);
                         }
                     }
-                }
 
-                for (state_type, drag_over_style) in &self.drag_over_styles {
-                    if *state_type == drag.view.entity_type()
-                        && bounds
-                            .intersect(&cx.content_mask().bounds)
-                            .contains(&mouse_position)
-                    {
-                        style.refine(drag_over_style);
-                    }
+                    cx.active_drag = Some(drag);
                 }
-
-                cx.active_drag = Some(drag);
             }
-        }
 
-        let clicked_state = element_state.clicked_state.borrow();
-        if clicked_state.group {
-            if let Some(group) = self.group_active_style.as_ref() {
-                style.refine(&group.style)
+            let clicked_state = element_state.clicked_state.borrow();
+            if clicked_state.group {
+                if let Some(group) = self.group_active_style.as_ref() {
+                    style.refine(&group.style)
+                }
             }
-        }
 
-        if let Some(active_style) = self.active_style.as_ref() {
-            if clicked_state.element {
-                style.refine(active_style)
+            if let Some(active_style) = self.active_style.as_ref() {
+                if clicked_state.element {
+                    style.refine(active_style)
+                }
             }
-        }
+        });
 
         style
     }

crates/workspace2/src/pane.rs 🔗

@@ -1759,6 +1759,34 @@ impl Pane {
             })
             .log_err();
     }
+
+    fn handle_split_tab_drop(
+        &mut self,
+        dragged_tab: &View<DraggedTab>,
+        split_direction: SplitDirection,
+        cx: &mut ViewContext<'_, Pane>,
+    ) {
+        let dragged_tab = dragged_tab.read(cx);
+        let item_id = dragged_tab.item_id;
+        let from_pane = dragged_tab.pane.clone();
+        let to_pane = cx.view().clone();
+        self.workspace
+            .update(cx, |workspace, cx| {
+                cx.defer(move |workspace, cx| {
+                    let item = from_pane
+                        .read(cx)
+                        .items()
+                        .find(|item| item.item_id() == item_id)
+                        .map(|item| item.boxed_clone());
+                    if let Some(item) = item {
+                        if let Some(item) = item.clone_on_split(workspace.database_id(), cx) {
+                            workspace.split_item(split_direction, item, cx);
+                        }
+                    }
+                });
+            })
+            .log_err();
+    }
 }
 
 impl FocusableView for Pane {
@@ -1852,7 +1880,54 @@ impl Render for Pane {
             .child(self.render_tab_bar(cx))
             .child(self.toolbar.clone())
             .child(if let Some(item) = self.active_item() {
-                div().flex().flex_1().child(item.to_any())
+                let mut drag_target_color = cx.theme().colors().text;
+                drag_target_color.a = 0.5;
+
+                div()
+                    .flex()
+                    .flex_1()
+                    .relative()
+                    .child(item.to_any())
+                    .child(
+                        div()
+                            .absolute()
+                            .full()
+                            .z_index(1)
+                            .drag_over::<DraggedTab>(|style| style.bg(drag_target_color))
+                            .on_drop(cx.listener(
+                                move |this, dragged_tab: &View<DraggedTab>, cx| {
+                                    this.handle_tab_drop(dragged_tab, this.active_item_index(), cx)
+                                },
+                            )),
+                    )
+                    .children(
+                        [
+                            (SplitDirection::Up, 2),
+                            (SplitDirection::Down, 2),
+                            (SplitDirection::Left, 3),
+                            (SplitDirection::Right, 3),
+                        ]
+                        .into_iter()
+                        .map(|(direction, z_index)| {
+                            let div = div()
+                                .absolute()
+                                .z_index(z_index)
+                                .invisible()
+                                .bg(drag_target_color)
+                                .drag_over::<DraggedTab>(|style| style.visible())
+                                .on_drop(cx.listener(
+                                    move |this, dragged_tab: &View<DraggedTab>, cx| {
+                                        this.handle_split_tab_drop(dragged_tab, direction, cx)
+                                    },
+                                ));
+                            match direction {
+                                SplitDirection::Up => div.top_0().left_0().right_0().h_32(),
+                                SplitDirection::Down => div.left_0().bottom_0().right_0().h_32(),
+                                SplitDirection::Left => div.top_0().left_0().bottom_0().w_32(),
+                                SplitDirection::Right => div.top_0().bottom_0().right_0().w_32(),
+                            }
+                        }),
+                    )
             } else {
                 h_stack()
                     .items_center()