Skip to main content

guise/panegroup/
drag.rs

1//! Tab drag-and-drop for [`PaneGroup`](super::PaneGroup): the drag payload +
2//! ghost, the edge-zone geometry that turns a cursor over a pane into a drop
3//! edge (mirroring Zed), and the drop overlay.
4
5use gpui::{
6    div, px, relative, Bounds, Context, EntityId, IntoElement, ParentElement, Pixels, Point,
7    Render, SharedString, Styled, Window,
8};
9
10use crate::SplitDirection;
11
12use super::{ItemId, PaneId};
13
14/// Which edge of a pane a drop targets — or the center (add as a tab, no split).
15#[derive(Clone, Copy, PartialEq, Eq, Debug)]
16pub enum DropEdge {
17    Left,
18    Right,
19    Top,
20    Bottom,
21}
22
23impl DropEdge {
24    /// The split axis and whether the dropped item's pane goes first (left/top).
25    pub fn split(self) -> (SplitDirection, bool) {
26        match self {
27            DropEdge::Left => (SplitDirection::Horizontal, true),
28            DropEdge::Right => (SplitDirection::Horizontal, false),
29            DropEdge::Top => (SplitDirection::Vertical, true),
30            DropEdge::Bottom => (SplitDirection::Vertical, false),
31        }
32    }
33}
34
35/// The payload carried while dragging a tab, and its own ghost renderer.
36#[derive(Clone)]
37pub struct TabDrag {
38    pub group: EntityId,
39    pub item: ItemId,
40    pub from_pane: PaneId,
41    pub label: SharedString,
42}
43
44impl Render for TabDrag {
45    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
46        div()
47            .px(px(10.0))
48            .py(px(4.0))
49            .rounded(px(6.0))
50            .bg(gpui::rgb(0x2a2a2e))
51            .text_color(gpui::rgb(0xf2f2f7))
52            .text_size(px(12.0))
53            .child(self.label.clone())
54    }
55}
56
57/// Fraction of a pane's short side that counts as an edge zone.
58const EDGE: f32 = 0.28;
59
60/// Cursor within `bounds` → the nearest edge when in that edge's zone, or `None`
61/// for the center (add as a tab) or outside the pane.
62pub fn drop_edge(bounds: Bounds<Pixels>, cursor: Point<Pixels>) -> Option<DropEdge> {
63    let w = f32::from(bounds.size.width);
64    let h = f32::from(bounds.size.height);
65    if w <= 0.0 || h <= 0.0 {
66        return None;
67    }
68    let x = f32::from(cursor.x - bounds.origin.x);
69    let y = f32::from(cursor.y - bounds.origin.y);
70    if x < 0.0 || y < 0.0 || x > w || y > h {
71        return None;
72    }
73    let edge = w.min(h) * EDGE;
74    if x >= edge && x <= w - edge && y >= edge && y <= h - edge {
75        return None;
76    }
77    let (left, right, top, bottom) = (x, w - x, y, h - y);
78    let min = left.min(right).min(top).min(bottom);
79    Some(if min == left {
80        DropEdge::Left
81    } else if min == right {
82        DropEdge::Right
83    } else if min == top {
84        DropEdge::Top
85    } else {
86        DropEdge::Bottom
87    })
88}
89
90/// A translucent highlight over the half of the pane the split would occupy (or
91/// the whole pane for a center/tab drop), shown while a tab is dragged over it.
92pub fn drop_overlay(edge: Option<DropEdge>) -> impl IntoElement {
93    let fill = gpui::rgba(0x4a9eff33);
94    let border = gpui::rgb(0x4a9eff);
95    let base = div().absolute().bg(fill).border_2().border_color(border);
96    match edge {
97        None => base.top_0().left_0().size_full(),
98        Some(DropEdge::Left) => base.top_0().left_0().bottom_0().w(relative(0.5)),
99        Some(DropEdge::Right) => base.top_0().right_0().bottom_0().w(relative(0.5)),
100        Some(DropEdge::Top) => base.top_0().left_0().right_0().h(relative(0.5)),
101        Some(DropEdge::Bottom) => base.bottom_0().left_0().right_0().h(relative(0.5)),
102    }
103}