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