Skip to main content

cranpose_ui/modifier/
drag_and_drop.rs

1//! Drag and drop between nodes, across windows. A `drag_and_drop_source`
2//! starts a transfer once a press on it moves past the drag threshold; the
3//! shell then routes the transfer by screen position to the
4//! `drag_and_drop_target` under the pointer in whichever surface draws it,
5//! and the target hears enter, move, exit and drop while the source hears
6//! how the transfer ended. The nodes know only their own handlers; the
7//! state here is what the shell drives with [`DragAndDropState::route`].
8
9use std::{
10    any::Any,
11    cell::{Cell, RefCell},
12    fmt,
13    hash::{Hash, Hasher},
14    rc::Rc,
15};
16
17use cranpose_core::{NodeId, collections::map::HashMap};
18use cranpose_foundation::{
19    DelegatableNode, InvalidationKind, ModifierNode, ModifierNodeContext, ModifierNodeElement,
20    NodeCapabilities, NodeState, PointerEvent, PointerEventKind, PointerInputNode,
21    gesture_constants::DRAG_THRESHOLD,
22};
23
24use super::{Modifier, Point};
25use crate::render_state::{AppContextId, current_app_context, with_app_context_by_id};
26
27/// What a transfer carries: any value the source chose, which a target
28/// downcasts to the type it expects.
29pub type DragAndDropPayload = Rc<dyn Any>;
30
31type PayloadHandler = Rc<dyn Fn(&DragAndDropPayload)>;
32type PayloadAtHandler = Rc<dyn Fn(&DragAndDropPayload, Point)>;
33type DropHandler = Rc<dyn Fn(&DragAndDropPayload, Point) -> bool>;
34type PointHandler = Rc<dyn Fn(DragAndDropPoint)>;
35
36/// How a transfer ended, as the source hears it.
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38pub enum DragAndDropOutcome {
39    /// A target accepted the drop.
40    Dropped,
41    /// The pointer was released over no target, or the target declined.
42    Missed,
43    /// The gesture was cancelled before a release.
44    Cancelled,
45}
46
47/// Where a transfer is: on the screen when the platform knows window
48/// positions, and in the source's surface always.
49#[derive(Clone, Copy, Debug, PartialEq)]
50pub struct DragAndDropPoint {
51    /// The pointer on the screen, in logical pixels, when known.
52    pub screen: Option<Point>,
53    /// The pointer in the surface that holds the press.
54    pub local: Point,
55}
56
57/// A payload a node offers to be dragged out of it, and what the node
58/// wants to hear about the transfer.
59#[derive(Clone)]
60pub struct DragAndDropSource {
61    payload: DragAndDropPayload,
62    on_started: Option<PointHandler>,
63    on_moved: Option<PointHandler>,
64    on_ended: Option<Rc<dyn Fn(DragAndDropOutcome)>>,
65}
66
67impl DragAndDropSource {
68    /// Offers `payload` from the node; a target downcasts it.
69    pub fn new(payload: impl Any) -> Self {
70        Self {
71            payload: Rc::new(payload),
72            on_started: None,
73            on_moved: None,
74            on_ended: None,
75        }
76    }
77
78    /// Called once the press has moved past the drag threshold.
79    pub fn on_started(mut self, handler: impl Fn(DragAndDropPoint) + 'static) -> Self {
80        self.on_started = Some(Rc::new(handler));
81        self
82    }
83
84    /// Called with every pointer sample while the transfer runs.
85    pub fn on_moved(mut self, handler: impl Fn(DragAndDropPoint) + 'static) -> Self {
86        self.on_moved = Some(Rc::new(handler));
87        self
88    }
89
90    /// Called when the transfer ends, with how it ended.
91    pub fn on_ended(mut self, handler: impl Fn(DragAndDropOutcome) + 'static) -> Self {
92        self.on_ended = Some(Rc::new(handler));
93        self
94    }
95}
96
97impl fmt::Debug for DragAndDropSource {
98    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99        f.debug_struct("DragAndDropSource").finish_non_exhaustive()
100    }
101}
102
103/// What a node does with a transfer that reaches it. Positions are in the
104/// surface that draws the target.
105#[derive(Clone, Default)]
106pub struct DragAndDropTarget {
107    on_entered: Option<PayloadHandler>,
108    on_moved: Option<PayloadAtHandler>,
109    on_exited: Option<PayloadHandler>,
110    on_drop: Option<DropHandler>,
111}
112
113impl DragAndDropTarget {
114    /// A target with no handlers yet.
115    pub fn new() -> Self {
116        Self::default()
117    }
118
119    /// Called when a transfer arrives over the node.
120    pub fn on_entered(mut self, handler: impl Fn(&DragAndDropPayload) + 'static) -> Self {
121        self.on_entered = Some(Rc::new(handler));
122        self
123    }
124
125    /// Called with every pointer sample over the node.
126    pub fn on_moved(mut self, handler: impl Fn(&DragAndDropPayload, Point) + 'static) -> Self {
127        self.on_moved = Some(Rc::new(handler));
128        self
129    }
130
131    /// Called when a transfer leaves the node without dropping.
132    pub fn on_exited(mut self, handler: impl Fn(&DragAndDropPayload) + 'static) -> Self {
133        self.on_exited = Some(Rc::new(handler));
134        self
135    }
136
137    /// Called on release over the node; returns whether the drop was
138    /// accepted, which the source hears as [`DragAndDropOutcome::Dropped`].
139    pub fn on_drop(
140        mut self,
141        handler: impl Fn(&DragAndDropPayload, Point) -> bool + 'static,
142    ) -> Self {
143        self.on_drop = Some(Rc::new(handler));
144        self
145    }
146}
147
148impl fmt::Debug for DragAndDropTarget {
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        f.debug_struct("DragAndDropTarget").finish_non_exhaustive()
151    }
152}
153
154/// One step of a transfer the source recorded for the shell to route.
155#[derive(Clone, Copy, Debug, PartialEq)]
156pub enum DragAndDropEvent {
157    /// The press moved past the drag threshold.
158    Started(DragAndDropPoint),
159    /// The pointer moved while the transfer runs.
160    Moved(DragAndDropPoint),
161    /// The pointer was released.
162    Dropped(DragAndDropPoint),
163    /// The gesture was cancelled.
164    Cancelled,
165}
166
167struct Transfer {
168    source: DragAndDropSource,
169    over: Option<NodeId>,
170}
171
172/// The one transfer in flight in an app context, the targets attached in
173/// it, and the steps the source recorded since the shell last routed.
174#[derive(Default)]
175pub struct DragAndDropState {
176    targets: RefCell<HashMap<NodeId, DragAndDropTarget>>,
177    transfer: RefCell<Option<Transfer>>,
178    pending: RefCell<Vec<DragAndDropEvent>>,
179}
180
181impl DragAndDropState {
182    fn register_target(&self, node: NodeId, target: DragAndDropTarget) {
183        self.targets.borrow_mut().insert(node, target);
184    }
185
186    fn unregister_target(&self, node: NodeId) {
187        self.targets.borrow_mut().remove(&node);
188        let mut transfer = self.transfer.borrow_mut();
189        if let Some(transfer) = transfer.as_mut()
190            && transfer.over == Some(node)
191        {
192            transfer.over = None;
193        }
194    }
195
196    /// Whether `node` carries a `drag_and_drop_target` right now.
197    pub fn is_target(&self, node: NodeId) -> bool {
198        self.targets.borrow().contains_key(&node)
199    }
200
201    /// Whether a transfer is in flight.
202    pub fn is_active(&self) -> bool {
203        self.transfer.borrow().is_some()
204    }
205
206    fn start(&self, source: DragAndDropSource, point: DragAndDropPoint) {
207        *self.transfer.borrow_mut() = Some(Transfer { source, over: None });
208        self.pending
209            .borrow_mut()
210            .push(DragAndDropEvent::Started(point));
211    }
212
213    fn record(&self, event: DragAndDropEvent) {
214        if self.transfer.borrow().is_some() {
215            self.pending.borrow_mut().push(event);
216        }
217    }
218
219    /// Delivers every step recorded since the last call. `find_target`
220    /// names the target node under a point and the point in that node's
221    /// surface; the shell answers it from its surfaces and their positions
222    /// on the screen. Returns whether anything was delivered.
223    pub fn route(
224        &self,
225        mut find_target: impl FnMut(DragAndDropPoint) -> Option<(NodeId, Point)>,
226    ) -> bool {
227        let events = std::mem::take(&mut *self.pending.borrow_mut());
228        let routed = !events.is_empty();
229        for event in events {
230            match event {
231                DragAndDropEvent::Started(point) => {
232                    self.tell_source(|source| source.on_started.clone(), point);
233                    self.hover(find_target(point));
234                }
235                DragAndDropEvent::Moved(point) => {
236                    self.tell_source(|source| source.on_moved.clone(), point);
237                    self.hover(find_target(point));
238                }
239                DragAndDropEvent::Dropped(point) => {
240                    let hit = find_target(point);
241                    self.hover(hit);
242                    let accepted = hit.is_some_and(|(node, local)| self.drop_on(node, local));
243                    self.finish(if accepted {
244                        DragAndDropOutcome::Dropped
245                    } else {
246                        DragAndDropOutcome::Missed
247                    });
248                }
249                DragAndDropEvent::Cancelled => {
250                    self.hover(None);
251                    self.finish(DragAndDropOutcome::Cancelled);
252                }
253            }
254        }
255        routed
256    }
257
258    fn tell_source(
259        &self,
260        handler: impl Fn(&DragAndDropSource) -> Option<PointHandler>,
261        point: DragAndDropPoint,
262    ) {
263        let handler = self
264            .transfer
265            .borrow()
266            .as_ref()
267            .and_then(|transfer| handler(&transfer.source));
268        if let Some(handler) = handler {
269            handler(point);
270        }
271    }
272
273    fn payload(&self) -> Option<DragAndDropPayload> {
274        self.transfer
275            .borrow()
276            .as_ref()
277            .map(|transfer| Rc::clone(&transfer.source.payload))
278    }
279
280    fn target(&self, node: NodeId) -> Option<DragAndDropTarget> {
281        self.targets.borrow().get(&node).cloned()
282    }
283
284    fn hover(&self, hit: Option<(NodeId, Point)>) {
285        let Some(payload) = self.payload() else {
286            return;
287        };
288        let previous = self
289            .transfer
290            .borrow()
291            .as_ref()
292            .and_then(|transfer| transfer.over);
293        let current = hit.map(|(node, _)| node);
294        if previous != current {
295            if let Some(exited) = previous.and_then(|node| self.target(node))
296                && let Some(on_exited) = exited.on_exited
297            {
298                on_exited(&payload);
299            }
300            if let Some(entered) = current.and_then(|node| self.target(node))
301                && let Some(on_entered) = entered.on_entered
302            {
303                on_entered(&payload);
304            }
305            if let Some(transfer) = self.transfer.borrow_mut().as_mut() {
306                transfer.over = current;
307            }
308        }
309        if let Some((node, local)) = hit
310            && let Some(on_moved) = self.target(node).and_then(|target| target.on_moved)
311        {
312            on_moved(&payload, local);
313        }
314    }
315
316    fn drop_on(&self, node: NodeId, local: Point) -> bool {
317        let Some(payload) = self.payload() else {
318            return false;
319        };
320        self.target(node)
321            .and_then(|target| target.on_drop)
322            .is_some_and(|on_drop| on_drop(&payload, local))
323    }
324
325    fn finish(&self, outcome: DragAndDropOutcome) {
326        let transfer = self.transfer.borrow_mut().take();
327        if let Some(on_ended) = transfer.and_then(|transfer| transfer.source.on_ended) {
328            on_ended(outcome);
329        }
330    }
331}
332
333#[derive(Default)]
334struct SourceGesture {
335    press: Option<Point>,
336    dragging: bool,
337}
338
339impl SourceGesture {
340    fn on_event(
341        &mut self,
342        event: &PointerEvent,
343        source: &DragAndDropSource,
344        state: &DragAndDropState,
345    ) {
346        let point = DragAndDropPoint {
347            screen: event.screen_position,
348            local: event.global_position,
349        };
350        match event.kind {
351            PointerEventKind::Down => {
352                self.press = Some(event.global_position);
353                self.dragging = false;
354            }
355            PointerEventKind::Move => self.on_move(event, source, state, point),
356            PointerEventKind::Up => {
357                if self.dragging {
358                    state.record(DragAndDropEvent::Dropped(point));
359                    event.consume();
360                }
361                *self = Self::default();
362            }
363            PointerEventKind::Cancel => {
364                if self.dragging {
365                    state.record(DragAndDropEvent::Cancelled);
366                }
367                *self = Self::default();
368            }
369            _ => {}
370        }
371    }
372
373    fn on_move(
374        &mut self,
375        event: &PointerEvent,
376        source: &DragAndDropSource,
377        state: &DragAndDropState,
378        point: DragAndDropPoint,
379    ) {
380        let Some(press) = self.press else {
381            return;
382        };
383        if self.dragging {
384            state.record(DragAndDropEvent::Moved(point));
385            event.consume();
386            return;
387        }
388        let dx = event.global_position.x - press.x;
389        let dy = event.global_position.y - press.y;
390        if (dx * dx + dy * dy).sqrt() > DRAG_THRESHOLD {
391            self.dragging = true;
392            state.start(source.clone(), point);
393            event.consume();
394        }
395    }
396}
397
398type PointerHandler = Rc<dyn Fn(PointerEvent)>;
399
400fn source_handler(
401    source: Rc<RefCell<DragAndDropSource>>,
402    gesture: Rc<RefCell<SourceGesture>>,
403) -> PointerHandler {
404    Rc::new(move |event: PointerEvent| {
405        let Some(context) = current_app_context() else {
406            return;
407        };
408        let source = source.borrow().clone();
409        gesture
410            .borrow_mut()
411            .on_event(&event, &source, context.drag_and_drop());
412    })
413}
414
415/// Node that starts a transfer when a press on it becomes a drag.
416pub struct DragAndDropSourceNode {
417    source: Rc<RefCell<DragAndDropSource>>,
418    handler: PointerHandler,
419    state: NodeState,
420}
421
422impl fmt::Debug for DragAndDropSourceNode {
423    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
424        f.debug_struct("DragAndDropSourceNode")
425            .finish_non_exhaustive()
426    }
427}
428
429impl DelegatableNode for DragAndDropSourceNode {
430    fn node_state(&self) -> &NodeState {
431        &self.state
432    }
433}
434
435impl ModifierNode for DragAndDropSourceNode {
436    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
437        context.invalidate(InvalidationKind::PointerInput);
438    }
439
440    cranpose_foundation::impl_modifier_node!(pointer_input);
441}
442
443impl PointerInputNode for DragAndDropSourceNode {
444    fn on_pointer_event(
445        &mut self,
446        _context: &mut dyn ModifierNodeContext,
447        event: &PointerEvent,
448    ) -> bool {
449        (self.handler)(event.clone());
450        event.is_consumed()
451    }
452
453    fn pointer_input_handler(&self) -> Option<PointerHandler> {
454        Some(Rc::clone(&self.handler))
455    }
456}
457
458/// Element that creates and updates [`DragAndDropSourceNode`]s.
459#[derive(Clone, Debug)]
460pub struct DragAndDropSourceElement {
461    source: DragAndDropSource,
462}
463
464impl PartialEq for DragAndDropSourceElement {
465    fn eq(&self, _other: &Self) -> bool {
466        true
467    }
468}
469
470impl Hash for DragAndDropSourceElement {
471    fn hash<H: Hasher>(&self, state: &mut H) {
472        "dragAndDropSource".hash(state);
473    }
474}
475
476impl ModifierNodeElement for DragAndDropSourceElement {
477    type Node = DragAndDropSourceNode;
478
479    fn create(&self) -> Self::Node {
480        let source = Rc::new(RefCell::new(self.source.clone()));
481        let gesture = Rc::new(RefCell::new(SourceGesture::default()));
482        DragAndDropSourceNode {
483            handler: source_handler(Rc::clone(&source), gesture),
484            source,
485            state: NodeState::new(),
486        }
487    }
488
489    fn update(&self, node: &mut Self::Node) {
490        *node.source.borrow_mut() = self.source.clone();
491    }
492
493    fn always_update(&self) -> bool {
494        true
495    }
496
497    fn capabilities(&self) -> NodeCapabilities {
498        NodeCapabilities::POINTER_INPUT
499    }
500
501    fn inspector_name(&self) -> &'static str {
502        "dragAndDropSource"
503    }
504}
505
506/// Node that registers its layout node as a drop target while attached.
507/// It is a pointer target so the shell's hit test finds it under a
508/// transfer; it handles no pointer events itself.
509pub struct DragAndDropTargetNode {
510    target: DragAndDropTarget,
511    node_id: Cell<Option<NodeId>>,
512    owner: Cell<Option<AppContextId>>,
513    handler: PointerHandler,
514    state: NodeState,
515}
516
517impl fmt::Debug for DragAndDropTargetNode {
518    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
519        f.debug_struct("DragAndDropTargetNode")
520            .field("node_id", &self.node_id.get())
521            .finish()
522    }
523}
524
525impl DragAndDropTargetNode {
526    fn register(&self) {
527        let Some(node) = self.node_id.get() else {
528            return;
529        };
530        let Some(context) = current_app_context() else {
531            return;
532        };
533        self.owner.set(Some(context.id()));
534        context
535            .drag_and_drop()
536            .register_target(node, self.target.clone());
537    }
538
539    fn unregister(&self) {
540        let (Some(node), Some(owner)) = (self.node_id.get(), self.owner.take()) else {
541            return;
542        };
543        with_app_context_by_id(owner, |context| {
544            context.drag_and_drop().unregister_target(node);
545        });
546    }
547}
548
549impl DelegatableNode for DragAndDropTargetNode {
550    fn node_state(&self) -> &NodeState {
551        &self.state
552    }
553}
554
555impl ModifierNode for DragAndDropTargetNode {
556    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
557        self.node_id.set(context.node_id());
558        self.register();
559        context.invalidate(InvalidationKind::PointerInput);
560    }
561
562    fn on_detach(&mut self) {
563        self.unregister();
564    }
565
566    cranpose_foundation::impl_modifier_node!(pointer_input);
567}
568
569impl PointerInputNode for DragAndDropTargetNode {
570    fn pointer_input_handler(&self) -> Option<PointerHandler> {
571        Some(Rc::clone(&self.handler))
572    }
573}
574
575/// Element that creates and updates [`DragAndDropTargetNode`]s.
576#[derive(Clone, Debug)]
577pub struct DragAndDropTargetElement {
578    target: DragAndDropTarget,
579}
580
581impl PartialEq for DragAndDropTargetElement {
582    fn eq(&self, _other: &Self) -> bool {
583        true
584    }
585}
586
587impl Hash for DragAndDropTargetElement {
588    fn hash<H: Hasher>(&self, state: &mut H) {
589        "dragAndDropTarget".hash(state);
590    }
591}
592
593impl ModifierNodeElement for DragAndDropTargetElement {
594    type Node = DragAndDropTargetNode;
595
596    fn create(&self) -> Self::Node {
597        DragAndDropTargetNode {
598            target: self.target.clone(),
599            node_id: Cell::new(None),
600            owner: Cell::new(None),
601            handler: Rc::new(|_event: PointerEvent| {}),
602            state: NodeState::new(),
603        }
604    }
605
606    fn update(&self, node: &mut Self::Node) {
607        node.target = self.target.clone();
608        node.register();
609    }
610
611    fn always_update(&self) -> bool {
612        true
613    }
614
615    fn capabilities(&self) -> NodeCapabilities {
616        NodeCapabilities::POINTER_INPUT
617    }
618
619    fn inspector_name(&self) -> &'static str {
620        "dragAndDropTarget"
621    }
622}
623
624impl Modifier {
625    /// Lets a press on the node that moves past the drag threshold carry
626    /// `source`'s payload to a [`drag_and_drop_target`](Self::drag_and_drop_target)
627    /// anywhere in the app, in this window or another.
628    pub fn drag_and_drop_source(self, source: DragAndDropSource) -> Self {
629        self.then(Self::with_element(DragAndDropSourceElement { source }))
630    }
631
632    /// Lets the node receive a transfer started by a
633    /// [`drag_and_drop_source`](Self::drag_and_drop_source), hearing
634    /// `target`'s enter, move, exit and drop.
635    pub fn drag_and_drop_target(self, target: DragAndDropTarget) -> Self {
636        self.then(Self::with_element(DragAndDropTargetElement { target }))
637    }
638}
639
640#[cfg(test)]
641#[path = "tests/drag_and_drop_tests.rs"]
642mod tests;