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