Skip to main content

ui/
drag.rs

1//! Drag and drop reordering over gpui's active drag.
2//!
3//! The gesture is gpui's: a [`Handle`] puts `on_drag` on the host's element,
4//! and the payload is a [`Carry`]. Any element can take part in the same drag
5//! with `on_drag_move::<Carry<I>>` and `on_drop::<Carry<I>>` —
6//! [`crate::docking`] does.
7//!
8//! A [`Domain`] adds landing, displacement and edge scrolling for the
9//! [`Region`]s it builds. The host lays out, scrolls and virtualizes each
10//! region's content; a region measures the handles painted inside it in the
11//! frame being drawn, and nothing else. A landing names its neighbours by id,
12//! so a list with rows that were not painted still gets an exact place.
13//!
14//! Keep one domain on the view that renders all of its regions.
15
16use std::{cell::RefCell, rc::Rc};
17
18use gpui::{
19    AnyElement, App, Axis, ElementId, IntoElement, Pixels, Point, Render,
20    StatefulInteractiveElement, Window, prelude::*,
21};
22use motion::Painter;
23
24mod element;
25mod state;
26
27pub use element::{Handle, Region};
28pub(crate) use state::Gesture;
29use state::State;
30
31/// gpui's drag payload for an item of a [`Domain`].
32pub struct Carry<I> {
33    item: I,
34    pub(crate) gesture: Rc<Gesture>,
35}
36
37impl<I> Carry<I> {
38    pub fn item(&self) -> &I {
39        &self.item
40    }
41
42    /// Whether a region of the source domain holds the pointer. A target
43    /// outside the domain takes the item only while this is false.
44    pub fn claimed(&self) -> bool {
45        self.gesture.claimed.get()
46    }
47
48    /// A compact frame for a ghost at `pointer`, keeping the grab point.
49    pub fn ghost_bounds(&self, pointer: Point<Pixels>) -> gpui::Bounds<Pixels> {
50        self.gesture.ghost_bounds(pointer)
51    }
52
53    /// Hides the in-place item while unclaimed. For a target that draws its
54    /// own ghost at `origin`; called on every frame the target shows it.
55    pub fn host(&self, origin: Point<Pixels>) {
56        self.gesture.host(origin);
57    }
58}
59
60/// Where a released item goes. `after` and `before` are its neighbours among
61/// the items painted in `region` at release; either is `None` at an end of
62/// what was painted.
63#[derive(Clone, Debug, PartialEq, Eq)]
64pub struct Drop<R, I> {
65    pub item: I,
66    pub from: R,
67    pub region: R,
68    pub after: Option<I>,
69    pub before: Option<I>,
70}
71
72/// A release over no region that accepts the item. Nothing is committed.
73#[derive(Clone, Debug)]
74pub struct Outside<I> {
75    pub item: I,
76    pub position: Point<Pixels>,
77}
78
79/// How a region shows where the item would land.
80#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
81pub enum Feedback {
82    /// Neighbours slide aside to open a gap.
83    #[default]
84    Displace,
85    /// A line in the gap; nothing moves.
86    Indicator,
87}
88
89type Ghost<I> = dyn Fn(&I, &mut Window, &mut App) -> AnyElement;
90type Paint = dyn Fn(&mut Window, &mut App) -> AnyElement;
91
92/// Gesture and animation state for the regions items move between.
93pub struct Domain<R, I>(Rc<RefCell<State<R, I>>>);
94
95impl<R, I> Clone for Domain<R, I> {
96    fn clone(&self) -> Self {
97        Self(self.0.clone())
98    }
99}
100
101impl<R: Clone + PartialEq + 'static, I: Clone + PartialEq + 'static> Domain<R, I> {
102    /// The carried item is painted in place, following the pointer.
103    pub fn new(painter: Painter) -> Self {
104        Self(Rc::new(RefCell::new(State::new(painter, None))))
105    }
106
107    /// The carried item's own element stays hidden in its slot and gpui paints
108    /// `ghost` under the pointer instead. For virtualized regions, where the
109    /// item's element can scroll out of the painted range.
110    pub fn with_ghost(
111        painter: Painter,
112        ghost: impl Fn(&I, &mut Window, &mut App) -> AnyElement + 'static,
113    ) -> Self {
114        Self(Rc::new(RefCell::new(State::new(
115            painter,
116            Some(Rc::new(ghost)),
117        ))))
118    }
119
120    /// A drop target laid out by the host. `child` holds this region's handles.
121    pub fn region(
122        &self,
123        element_id: impl Into<ElementId>,
124        id: R,
125        axis: Axis,
126        child: impl IntoElement,
127    ) -> Region<R, I> {
128        self.0.borrow_mut().begin_frame();
129        Region::new(
130            self.clone(),
131            element_id.into(),
132            id,
133            axis,
134            child.into_any_element(),
135        )
136    }
137
138    /// Makes `element` pickable as `item`. Item ids are unique within a domain.
139    /// Custom controls inside it stop mouse-down propagation to keep their
140    /// presses from starting a drag.
141    pub fn handle<E>(&self, item: I, element: E) -> Handle<R, I>
142    where
143        E: StatefulInteractiveElement + IntoElement + 'static,
144    {
145        let domain = self.clone();
146        let carry = Carry {
147            item: item.clone(),
148            gesture: Rc::new(Gesture::default()),
149        };
150        let element = element.on_drag(carry, move |carry, grab, window, cx| {
151            domain.start(carry, grab, window, cx)
152        });
153        Handle::new(self.clone(), item, element.into_any_element())
154    }
155
156    /// An item that is measured and moves aside like a handle but cannot be
157    /// picked up, such as a heading between sections.
158    pub fn fixed(&self, item: I, element: impl IntoElement) -> Handle<R, I> {
159        Handle::new(self.clone(), item, element.into_any_element())
160    }
161
162    /// Whether `item` is being carried.
163    pub fn carries(&self, item: &I) -> bool {
164        self.0
165            .borrow()
166            .live
167            .as_ref()
168            .is_some_and(|live| &live.item == item)
169    }
170
171    /// Ends the gesture without a drop.
172    pub fn cancel(&self, window: &mut Window, cx: &mut App) {
173        let live = self.0.borrow_mut().end(window, cx);
174        if live.is_some() {
175            cx.stop_active_drag(window);
176        }
177    }
178
179    /// The item being carried.
180    pub fn carried(&self) -> Option<I> {
181        self.0.borrow().live.as_ref().map(|live| live.item.clone())
182    }
183
184    /// Ends the gesture at the next render that has a window.
185    pub(crate) fn invalidate(&self) {
186        self.0.borrow_mut().stale = true;
187    }
188
189    fn start(
190        &self,
191        carry: &Carry<I>,
192        cursor: Point<Pixels>,
193        window: &mut Window,
194        cx: &mut App,
195    ) -> gpui::Entity<Ghosted> {
196        let (grab, ghost) = {
197            let mut state = self.0.borrow_mut();
198            let grab = state.start(&carry.item, carry.gesture.clone(), cursor, window, cx);
199            (grab, state.ghost.clone())
200        };
201        let item = carry.item.clone();
202        let gesture = carry.gesture.clone();
203        cx.new(|_| Ghosted {
204            gesture,
205            // gpui places the view at the pointer less `cursor`.
206            shift: cursor - grab,
207            render: ghost.map(|ghost| {
208                Rc::new(move |window: &mut Window, cx: &mut App| ghost(&item, window, cx))
209                    as Rc<Paint>
210            }),
211        })
212    }
213}
214
215/// The view gpui paints under the pointer: the domain's ghost, or nothing.
216pub struct Ghosted {
217    gesture: Rc<Gesture>,
218    shift: Point<Pixels>,
219    render: Option<Rc<Paint>>,
220}
221
222impl Render for Ghosted {
223    fn render(&mut self, window: &mut Window, cx: &mut gpui::Context<Self>) -> impl IntoElement {
224        let hosted = self.gesture.hosted.get() && !self.gesture.claimed.get();
225        match self.render.as_ref().filter(|_| !hosted) {
226            Some(render) => {
227                let gesture = self.gesture.clone();
228                // gpui lays the drag view out as a root, which drops its own
229                // insets and margins, so the shift sits one level down.
230                gpui::div()
231                    .child(
232                        gpui::div()
233                            .relative()
234                            .left(self.shift.x)
235                            .top(self.shift.y)
236                            .child(render(window, cx))
237                            .child(
238                                gpui::canvas(
239                                    move |bounds, _, _| gesture.shown.set(Some(bounds.origin)),
240                                    |_, _, _, _| {},
241                                )
242                                .absolute()
243                                .top_0()
244                                .left_0()
245                                .size_full(),
246                            ),
247                    )
248                    .into_any_element()
249            }
250            None => gpui::Empty.into_any_element(),
251        }
252    }
253}