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