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            PointerEventKind::Scroll
370            | PointerEventKind::Zoom
371            | PointerEventKind::RotaryScrollPre
372            | PointerEventKind::RotaryScroll
373            | PointerEventKind::Enter
374            | PointerEventKind::Exit => {}
375        }
376    }
377
378    fn on_move(
379        &mut self,
380        event: &PointerEvent,
381        source: &DragAndDropSource,
382        state: &DragAndDropState,
383        point: DragAndDropPoint,
384    ) {
385        let Some(press) = self.press else {
386            return;
387        };
388        if self.dragging {
389            state.record(DragAndDropEvent::Moved(point));
390            event.consume();
391            return;
392        }
393        let dx = event.global_position.x - press.x;
394        let dy = event.global_position.y - press.y;
395        if (dx * dx + dy * dy).sqrt() > DRAG_THRESHOLD {
396            self.dragging = true;
397            state.start(source.clone(), point);
398            event.consume();
399        }
400    }
401}
402
403type PointerHandler = Rc<dyn Fn(PointerEvent)>;
404
405fn source_handler(
406    source: Rc<RefCell<DragAndDropSource>>,
407    gesture: Rc<RefCell<SourceGesture>>,
408) -> PointerHandler {
409    Rc::new(move |event: PointerEvent| {
410        let Some(context) = current_app_context() else {
411            return;
412        };
413        let source = source.borrow().clone();
414        gesture
415            .borrow_mut()
416            .on_event(&event, &source, context.drag_and_drop());
417    })
418}
419
420/// Node that starts a transfer when a press on it becomes a drag.
421pub struct DragAndDropSourceNode {
422    source: Rc<RefCell<DragAndDropSource>>,
423    handler: PointerHandler,
424    state: NodeState,
425}
426
427impl fmt::Debug for DragAndDropSourceNode {
428    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
429        f.debug_struct("DragAndDropSourceNode")
430            .finish_non_exhaustive()
431    }
432}
433
434impl DelegatableNode for DragAndDropSourceNode {
435    fn node_state(&self) -> &NodeState {
436        &self.state
437    }
438}
439
440impl ModifierNode for DragAndDropSourceNode {
441    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
442        context.invalidate(InvalidationKind::PointerInput);
443    }
444
445    cranpose_foundation::impl_modifier_node!(pointer_input);
446}
447
448impl PointerInputNode for DragAndDropSourceNode {
449    fn on_pointer_event(
450        &mut self,
451        _context: &mut dyn ModifierNodeContext,
452        event: &PointerEvent,
453    ) -> bool {
454        (self.handler)(event.clone());
455        event.is_consumed()
456    }
457
458    fn pointer_input_handler(&self) -> Option<PointerHandler> {
459        Some(Rc::clone(&self.handler))
460    }
461}
462
463/// Element that creates and updates [`DragAndDropSourceNode`]s.
464#[derive(Clone, Debug)]
465pub struct DragAndDropSourceElement {
466    source: DragAndDropSource,
467}
468
469impl PartialEq for DragAndDropSourceElement {
470    fn eq(&self, _other: &Self) -> bool {
471        true
472    }
473}
474
475impl Hash for DragAndDropSourceElement {
476    fn hash<H: Hasher>(&self, state: &mut H) {
477        "dragAndDropSource".hash(state);
478    }
479}
480
481impl ModifierNodeElement for DragAndDropSourceElement {
482    type Node = DragAndDropSourceNode;
483
484    fn create(&self) -> Self::Node {
485        let source = Rc::new(RefCell::new(self.source.clone()));
486        let gesture = Rc::new(RefCell::new(SourceGesture::default()));
487        DragAndDropSourceNode {
488            handler: source_handler(Rc::clone(&source), gesture),
489            source,
490            state: NodeState::new(),
491        }
492    }
493
494    fn update(&self, node: &mut Self::Node) {
495        *node.source.borrow_mut() = self.source.clone();
496    }
497
498    fn always_update(&self) -> bool {
499        true
500    }
501
502    fn capabilities(&self) -> NodeCapabilities {
503        NodeCapabilities::POINTER_INPUT
504    }
505
506    fn inspector_name(&self) -> &'static str {
507        "dragAndDropSource"
508    }
509}
510
511/// Node that registers its layout node as a drop target while attached.
512/// It is a pointer target so the shell's hit test finds it under a
513/// transfer; it handles no pointer events itself.
514pub struct DragAndDropTargetNode {
515    target: DragAndDropTarget,
516    node_id: Cell<Option<NodeId>>,
517    owner: Cell<Option<AppContextId>>,
518    handler: PointerHandler,
519    state: NodeState,
520}
521
522impl fmt::Debug for DragAndDropTargetNode {
523    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
524        f.debug_struct("DragAndDropTargetNode")
525            .field("node_id", &self.node_id.get())
526            .finish()
527    }
528}
529
530impl DragAndDropTargetNode {
531    fn register(&self) {
532        let Some(node) = self.node_id.get() else {
533            return;
534        };
535        let Some(context) = current_app_context() else {
536            return;
537        };
538        self.owner.set(Some(context.id()));
539        context
540            .drag_and_drop()
541            .register_target(node, self.target.clone());
542    }
543
544    fn unregister(&self) {
545        let (Some(node), Some(owner)) = (self.node_id.get(), self.owner.take()) else {
546            return;
547        };
548        with_app_context_by_id(owner, |context| {
549            context.drag_and_drop().unregister_target(node);
550        });
551    }
552}
553
554impl DelegatableNode for DragAndDropTargetNode {
555    fn node_state(&self) -> &NodeState {
556        &self.state
557    }
558}
559
560impl ModifierNode for DragAndDropTargetNode {
561    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
562        self.node_id.set(context.node_id());
563        self.register();
564        context.invalidate(InvalidationKind::PointerInput);
565    }
566
567    fn on_detach(&mut self) {
568        self.unregister();
569    }
570
571    cranpose_foundation::impl_modifier_node!(pointer_input);
572}
573
574impl PointerInputNode for DragAndDropTargetNode {
575    fn pointer_input_handler(&self) -> Option<PointerHandler> {
576        Some(Rc::clone(&self.handler))
577    }
578}
579
580/// Element that creates and updates [`DragAndDropTargetNode`]s.
581#[derive(Clone, Debug)]
582pub struct DragAndDropTargetElement {
583    target: DragAndDropTarget,
584}
585
586impl PartialEq for DragAndDropTargetElement {
587    fn eq(&self, _other: &Self) -> bool {
588        true
589    }
590}
591
592impl Hash for DragAndDropTargetElement {
593    fn hash<H: Hasher>(&self, state: &mut H) {
594        "dragAndDropTarget".hash(state);
595    }
596}
597
598impl ModifierNodeElement for DragAndDropTargetElement {
599    type Node = DragAndDropTargetNode;
600
601    fn create(&self) -> Self::Node {
602        DragAndDropTargetNode {
603            target: self.target.clone(),
604            node_id: Cell::new(None),
605            owner: Cell::new(None),
606            handler: Rc::new(|_event: PointerEvent| {}),
607            state: NodeState::new(),
608        }
609    }
610
611    fn update(&self, node: &mut Self::Node) {
612        node.target = self.target.clone();
613        node.register();
614    }
615
616    fn always_update(&self) -> bool {
617        true
618    }
619
620    fn capabilities(&self) -> NodeCapabilities {
621        NodeCapabilities::POINTER_INPUT
622    }
623
624    fn inspector_name(&self) -> &'static str {
625        "dragAndDropTarget"
626    }
627}
628
629impl Modifier {
630    /// Lets a press on the node that moves past the drag threshold carry
631    /// `source`'s payload to a [`drag_and_drop_target`](Self::drag_and_drop_target)
632    /// anywhere in the app, in this window or another.
633    pub fn drag_and_drop_source(self, source: DragAndDropSource) -> Self {
634        self.then(Self::with_element(DragAndDropSourceElement { source }))
635    }
636
637    /// Lets the node receive a transfer started by a
638    /// [`drag_and_drop_source`](Self::drag_and_drop_source), hearing
639    /// `target`'s enter, move, exit and drop.
640    pub fn drag_and_drop_target(self, target: DragAndDropTarget) -> Self {
641        self.then(Self::with_element(DragAndDropTargetElement { target }))
642    }
643}
644
645#[cfg(test)]
646#[path = "tests/drag_and_drop_tests.rs"]
647mod tests;