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