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, Bounds, Element, ElementId, GlobalElementId, InspectorElementId,
20    IntoElement, LayoutId, Pixels, Point, Render, StatefulInteractiveElement, Window, point,
21    prelude::*,
22};
23use motion::Painter;
24use theme::Theme;
25
26use crate::surface::{Surface, Surfaced};
27
28/// A dragged item's floating copy.
29pub trait Carried: Styled + IntoElement + Sized {
30    /// A [`Domain::with_ghost`] or docking copy, on
31    /// [`Theme::carried_surface`]. It keeps the element's own rounding.
32    fn ghost(self, theme: &Theme) -> Surface {
33        self.surface(theme, theme.carried_surface)
34    }
35}
36
37impl<E: Styled + IntoElement> Carried for E {}
38
39mod element;
40mod state;
41
42pub use element::{Handle, Region};
43pub(crate) use state::Gesture;
44use state::State;
45
46/// gpui's drag payload for an item of a [`Domain`].
47pub struct Carry<I> {
48    item: I,
49    pub(crate) gesture: Rc<Gesture>,
50}
51
52impl<I> Carry<I> {
53    pub fn item(&self) -> &I {
54        &self.item
55    }
56
57    /// Whether a region of the source domain holds the pointer. A target
58    /// outside the domain takes the item only while this is false.
59    pub fn claimed(&self) -> bool {
60        self.gesture.claimed.get()
61    }
62
63    /// A compact frame for a ghost at `pointer`, keeping the grab point.
64    pub fn ghost_bounds(&self, pointer: Point<Pixels>) -> gpui::Bounds<Pixels> {
65        self.gesture.ghost_bounds(pointer)
66    }
67
68    /// Hides the in-place item while unclaimed. For a target that draws its
69    /// own ghost at `origin`; called on every frame the target shows it.
70    pub fn host(&self, origin: Point<Pixels>) {
71        self.gesture.host(origin);
72    }
73}
74
75/// Where a released item goes. `after` and `before` are its neighbours among
76/// the items painted in `region` at release; either is `None` at an end of
77/// what was painted.
78#[derive(Clone, Debug, PartialEq, Eq)]
79pub struct Drop<R, I> {
80    pub item: I,
81    pub from: R,
82    pub region: R,
83    pub after: Option<I>,
84    pub before: Option<I>,
85}
86
87/// A release over no region that accepts the item. Nothing is committed.
88#[derive(Clone, Debug)]
89pub struct Outside<I> {
90    pub item: I,
91    pub position: Point<Pixels>,
92}
93
94/// How a region shows where the item would land.
95#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
96pub enum Feedback {
97    /// Neighbours slide aside to open a gap.
98    #[default]
99    Displace,
100    /// A line in the gap; nothing moves.
101    Indicator,
102}
103
104type Ghost<I> = dyn Fn(&I, &mut Window, &mut App) -> AnyElement;
105type Paint = dyn Fn(&mut Window, &mut App) -> AnyElement;
106
107/// Gesture and animation state for the regions items move between.
108pub struct Domain<R, I>(Rc<RefCell<State<R, I>>>);
109
110impl<R, I> Clone for Domain<R, I> {
111    fn clone(&self) -> Self {
112        Self(self.0.clone())
113    }
114}
115
116impl<R: Clone + PartialEq + 'static, I: Clone + PartialEq + 'static> Domain<R, I> {
117    /// The carried item is painted in place, following the pointer.
118    pub fn new(painter: Painter) -> Self {
119        Self(Rc::new(RefCell::new(State::new(painter, None))))
120    }
121
122    /// The carried item's own element stays hidden in its slot and gpui paints
123    /// `ghost` under the pointer instead. For virtualized regions, where the
124    /// item's element can scroll out of the painted range.
125    pub fn with_ghost(
126        painter: Painter,
127        ghost: impl Fn(&I, &mut Window, &mut App) -> AnyElement + 'static,
128    ) -> Self {
129        Self(Rc::new(RefCell::new(State::new(
130            painter,
131            Some(Rc::new(ghost)),
132        ))))
133    }
134
135    /// A drop target laid out by the host. `child` holds this region's handles.
136    pub fn region(
137        &self,
138        element_id: impl Into<ElementId>,
139        id: R,
140        axis: Axis,
141        child: impl IntoElement,
142    ) -> Region<R, I> {
143        self.0.borrow_mut().begin_frame();
144        Region::new(
145            self.clone(),
146            element_id.into(),
147            id,
148            axis,
149            child.into_any_element(),
150        )
151    }
152
153    /// Makes `element` pickable as `item`. Item ids are unique within a domain.
154    /// Custom controls inside it stop mouse-down propagation to keep their
155    /// presses from starting a drag.
156    pub fn handle<E>(&self, item: I, element: E) -> Handle<R, I>
157    where
158        E: StatefulInteractiveElement + IntoElement + 'static,
159    {
160        let domain = self.clone();
161        let carry = Carry {
162            item: item.clone(),
163            gesture: Rc::new(Gesture::default()),
164        };
165        let element = element.on_drag(carry, move |carry, grab, window, cx| {
166            domain.start(carry, grab, window, cx)
167        });
168        Handle::new(self.clone(), item, element.into_any_element())
169    }
170
171    /// An item that is measured and moves aside like a handle but cannot be
172    /// picked up, such as a heading between sections.
173    pub fn fixed(&self, item: I, element: impl IntoElement) -> Handle<R, I> {
174        Handle::new(self.clone(), item, element.into_any_element())
175    }
176
177    /// Whether `item` is being carried.
178    pub fn carries(&self, item: &I) -> bool {
179        self.0
180            .borrow()
181            .live
182            .as_ref()
183            .is_some_and(|live| &live.item == item)
184    }
185
186    /// Ends the gesture without a drop.
187    pub fn cancel(&self, window: &mut Window, cx: &mut App) {
188        let live = self.0.borrow_mut().end(window, cx);
189        if live.is_some() {
190            cx.stop_active_drag(window);
191        }
192    }
193
194    /// The item being carried.
195    pub fn carried(&self) -> Option<I> {
196        self.0.borrow().live.as_ref().map(|live| live.item.clone())
197    }
198
199    /// Ends the gesture at the next render that has a window.
200    pub(crate) fn invalidate(&self) {
201        self.0.borrow_mut().stale = true;
202    }
203
204    fn start(
205        &self,
206        carry: &Carry<I>,
207        cursor: Point<Pixels>,
208        window: &mut Window,
209        cx: &mut App,
210    ) -> gpui::Entity<Ghosted> {
211        let (grab, ghost) = {
212            let mut state = self.0.borrow_mut();
213            let grab = state.start(&carry.item, carry.gesture.clone(), cursor, window, cx);
214            (grab, state.ghost.clone())
215        };
216        carry.gesture.ghosted.set(ghost.is_some());
217        let item = carry.item.clone();
218        let gesture = carry.gesture.clone();
219        cx.new(|_| Ghosted {
220            gesture,
221            cursor,
222            grab,
223            render: ghost.map(|ghost| {
224                Rc::new(move |window: &mut Window, cx: &mut App| ghost(&item, window, cx))
225                    as Rc<Paint>
226            }),
227        })
228    }
229}
230
231/// The view gpui paints under the pointer: the domain's ghost, or nothing.
232pub struct Ghosted {
233    gesture: Rc<Gesture>,
234    /// Where gpui places the view: at the pointer less this.
235    cursor: Point<Pixels>,
236    /// Where the item was pressed, within the item.
237    grab: Point<Pixels>,
238    render: Option<Rc<Paint>>,
239}
240
241impl Render for Ghosted {
242    fn render(&mut self, window: &mut Window, cx: &mut gpui::Context<Self>) -> impl IntoElement {
243        let hosted = self.gesture.hosted.get() && !self.gesture.claimed.get();
244        match self.render.as_ref().filter(|_| !hosted) {
245            Some(render) => {
246                let gesture = self.gesture.clone();
247                // gpui lays the drag view out as a root, which drops its own
248                // insets and margins, so the shift sits one level down.
249                gpui::div()
250                    .child(AtGrab {
251                        cursor: self.cursor,
252                        grab: self.grab,
253                        child: gpui::div()
254                            .child(render(window, cx))
255                            .child(
256                                gpui::canvas(
257                                    move |bounds, _, _| gesture.shown.set(Some(bounds.origin)),
258                                    |_, _, _, _| {},
259                                )
260                                .absolute()
261                                .top_0()
262                                .left_0()
263                                .size_full(),
264                            )
265                            .into_any_element(),
266                    })
267                    .into_any_element()
268            }
269            None => gpui::Empty.into_any_element(),
270        }
271    }
272}
273
274/// The ghost, shifted so the pointer holds it where the item was grabbed —
275/// clamped to the ghost's own laid-out size, which only exists after layout.
276struct AtGrab {
277    cursor: Point<Pixels>,
278    grab: Point<Pixels>,
279    child: AnyElement,
280}
281
282impl IntoElement for AtGrab {
283    type Element = Self;
284
285    fn into_element(self) -> Self {
286        self
287    }
288}
289
290impl Element for AtGrab {
291    type RequestLayoutState = ();
292    type PrepaintState = ();
293
294    fn id(&self) -> Option<ElementId> {
295        None
296    }
297
298    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
299        None
300    }
301
302    fn request_layout(
303        &mut self,
304        _id: Option<&GlobalElementId>,
305        _inspector_id: Option<&InspectorElementId>,
306        window: &mut Window,
307        cx: &mut App,
308    ) -> (LayoutId, ()) {
309        (self.child.request_layout(window, cx), ())
310    }
311
312    fn prepaint(
313        &mut self,
314        _id: Option<&GlobalElementId>,
315        _inspector_id: Option<&InspectorElementId>,
316        bounds: Bounds<Pixels>,
317        _request_layout: &mut (),
318        window: &mut Window,
319        cx: &mut App,
320    ) {
321        let grab = point(
322            self.grab.x.clamp(Pixels::ZERO, bounds.size.width),
323            self.grab.y.clamp(Pixels::ZERO, bounds.size.height),
324        );
325        window.with_element_offset(self.cursor - grab, |window| self.child.prepaint(window, cx));
326    }
327
328    fn paint(
329        &mut self,
330        _id: Option<&GlobalElementId>,
331        _inspector_id: Option<&InspectorElementId>,
332        _bounds: Bounds<Pixels>,
333        _request_layout: &mut (),
334        _prepaint: &mut (),
335        window: &mut Window,
336        cx: &mut App,
337    ) {
338        self.child.paint(window, cx);
339    }
340}