Skip to main content

dioxus_dnd/core/world/
drag.rs

1//! Drag-session anchoring: which window the in-flight drag started in,
2//! and the origin-window conversion behind the global pointer.
3
4use std::sync::atomic::{AtomicU64, Ordering};
5
6use dioxus::prelude::*;
7
8use crate::core::monitor::CancelReason;
9use crate::core::session::DragCompletion;
10use crate::core::types::{DragSessionId, Point, Rect};
11
12use super::geometry::WindowKey;
13use super::state::{DndWorld, WindowRecord, ZoneLocation};
14
15// Identity freshness only: Relaxed is sufficient because the counter carries
16// no synchronization. Correctness assumes this process-lifetime u64 never
17// wraps; do not narrow it.
18static NEXT_WORLD_DRAG_GENERATION: AtomicU64 = AtomicU64::new(1);
19
20#[derive(Debug, Clone, Copy, PartialEq)]
21pub(super) struct ActiveDrag {
22    pub(super) origin: WindowKey,
23    /// Fresh for every `begin_from`, including custom/untracked sources. Host
24    /// adapters bind observations to this rather than treating `session: None`
25    /// as an authority token that could attach to a successor drag.
26    pub(super) generation: u64,
27    pub(super) session: Option<DragSessionId>,
28    pub(super) origin_scale: f64,
29    pub(super) source_location: Option<ZoneLocation>,
30    pub(super) modifiers: Modifiers,
31}
32
33impl<T: Clone + 'static> DndWorld<T> {
34    /// Mark a drag as begun from `key` and reset stale presentation state.
35    /// `Draggable` calls this at pickup; call it from custom drag sources
36    /// so the world knows which window's client px `ctx.pointer()` is in.
37    pub fn begin_from(&self, key: WindowKey) {
38        let origin = self.record(key);
39        let active_drag = ActiveDrag {
40            origin: key,
41            generation: NEXT_WORLD_DRAG_GENERATION.fetch_add(1, Ordering::Relaxed),
42            // Receiver code may synchronously start an untracked drag while
43            // the old source result is committed but not yet finalized. Do
44            // not attach that old generation to the replacement.
45            session: self
46                .ctx
47                .active_session()
48                .filter(|session| self.ctx.session_result(*session).is_none()),
49            origin_scale: origin.map_or(1.0, |record| record.geometry.scale()),
50            source_location: self
51                .ctx
52                .source()
53                .map(|zone| ZoneLocation { window: key, zone }),
54            modifiers: Modifiers::empty(),
55        };
56        let mut active = self.active;
57        if *active.peek() != Some(active_drag) {
58            active.set(Some(active_drag));
59        }
60        let mut settle_claim = self.settle_claim;
61        if settle_claim.peek().is_some() {
62            settle_claim.set(None);
63        }
64        let mut global_pointer = self.global_pointer;
65        let initial_global =
66            origin.and_then(|record| record.geometry.to_global(self.ctx.pointer()));
67        if *global_pointer.peek() != initial_global {
68            global_pointer.set(initial_global);
69        }
70        let mut over_location = self.over_location;
71        if over_location.peek().is_some() {
72            over_location.set(None);
73        }
74    }
75
76    /// The record of the window the in-flight drag started in.
77    pub fn active_record(&self) -> Option<WindowRecord<T>> {
78        let origin = self.active.peek().as_ref()?.origin;
79        self.record(origin)
80    }
81
82    pub(super) fn active_drag(&self) -> Option<ActiveDrag> {
83        *self.active.peek()
84    }
85
86    /// The in-flight pointer in global physical px. `None` until a world
87    /// pointer can be resolved or after the world drag finishes.
88    pub fn global_pointer(&self) -> Option<Point> {
89        *self.global_pointer.read()
90    }
91
92    /// Window-qualified source and hover locations for the active world
93    /// drag. The legacy `DndContext` id accessors remain unchanged.
94    pub fn source_location(&self) -> Option<ZoneLocation> {
95        self.active
96            .read()
97            .as_ref()
98            .and_then(|active| active.source_location)
99    }
100
101    pub fn over_location(&self) -> Option<ZoneLocation> {
102        *self.over_location.read()
103    }
104
105    /// Current tracked pointer-drag generation, if this world owns one.
106    pub fn drag_session(&self) -> Option<DragSessionId> {
107        self.active.peek().as_ref()?.session
108    }
109
110    /// Private host-adapter token for the current world drag. The generation
111    /// is mandatory; the optional source session adds exactly-once completion
112    /// ownership for built-in tracked sources.
113    #[cfg_attr(not(feature = "desktop"), allow(dead_code))]
114    pub(crate) fn drag_generation(&self) -> Option<(u64, Option<DragSessionId>)> {
115        let active = self.active.read();
116        let active = active.as_ref()?;
117        Some((active.generation, active.session))
118    }
119
120    /// Non-subscribing generation read for imperative host event handlers.
121    /// Async resources use [`Self::drag_generation`] so `begin_from` wakes a
122    /// new run even when all other drag gates retain the same values.
123    #[cfg_attr(not(feature = "desktop"), allow(dead_code))]
124    pub(crate) fn drag_generation_peek(&self) -> Option<(u64, Option<DragSessionId>)> {
125        let active = self.active_drag()?;
126        Some((active.generation, active.session))
127    }
128
129    /// Whether both halves of a captured host token still name the active
130    /// drag. For untracked custom sources, `None` is valid only alongside the
131    /// matching mandatory world generation.
132    #[cfg_attr(not(feature = "desktop"), allow(dead_code))]
133    pub(crate) fn is_drag_generation(
134        &self,
135        generation: u64,
136        session: Option<DragSessionId>,
137    ) -> bool {
138        let Some(active) = self.active_drag() else {
139            return false;
140        };
141        if !self.ctx.dragging() || active.generation != generation || active.session != session {
142            return false;
143        }
144        session.is_none_or(|session| self.ctx.is_session(session))
145    }
146
147    pub(crate) fn is_drag_session(&self, session: DragSessionId) -> bool {
148        self.drag_session() == Some(session) && self.ctx.is_session(session)
149    }
150
151    pub(crate) fn commit_session(&self, session: DragSessionId, dropped: bool) -> bool {
152        if !self.is_drag_session(session) {
153            return false;
154        }
155        let mut ctx = self.ctx;
156        ctx.commit_source(session, dropped)
157    }
158
159    pub(crate) fn finalize_session(&self, session: DragSessionId) -> bool {
160        let Some(result) = self.ctx.session_result(session) else {
161            return false;
162        };
163        let completion = if result {
164            DragCompletion::Dropped
165        } else {
166            DragCompletion::Cancelled(CancelReason::User)
167        };
168        self.finish_session(session, completion)
169    }
170
171    pub(crate) fn finish_session(
172        &self,
173        session: DragSessionId,
174        completion: DragCompletion,
175    ) -> bool {
176        let mut ctx = self.ctx;
177        if !ctx.is_session(session) {
178            return false;
179        }
180        let owns_metadata = self.drag_session() == Some(session);
181        let result = ctx
182            .session_result(session)
183            .unwrap_or_else(|| completion.dropped());
184        let finished = if ctx.session_result(session).is_some() {
185            ctx.finalize_source(session)
186        } else if completion.dropped() {
187            ctx.finish_source(session, true)
188        } else {
189            let DragCompletion::Cancelled(reason) = completion else {
190                unreachable!("dropped completion handled above")
191            };
192            ctx.cancel_session(session, reason)
193        };
194        if !finished {
195            return false;
196        }
197        if !owns_metadata || self.drag_session() != Some(session) {
198            return true;
199        }
200        // Source notification is user code and may synchronously begin a
201        // replacement. Its new begin_from call owns the metadata now.
202        if ctx.dragging() {
203            return true;
204        }
205        if result && ctx.settling().is_some() {
206            let mut active = self.active;
207            let current = *active.peek();
208            if let Some(mut current) = current {
209                current.session = None;
210                active.set(Some(current));
211            }
212            self.clear_hover();
213        } else {
214            self.clear_world_state();
215        }
216        true
217    }
218
219    pub(crate) fn finish_untracked(&self, completion: DragCompletion) {
220        let mut ctx = self.ctx;
221        if let DragCompletion::Cancelled(reason) = completion {
222            if ctx.dragging() {
223                ctx.cancel_with_reason(reason);
224            }
225        }
226        if ctx.dragging() {
227            return;
228        }
229        if completion.dropped() && ctx.settling().is_some() {
230            self.clear_hover();
231        } else {
232            self.clear_world_state();
233        }
234    }
235
236    pub(crate) fn active_rect_in(
237        &self,
238        destination: WindowRecord<T>,
239        pointer: Point,
240    ) -> Option<Rect> {
241        let source = self.ctx.source_rect()?;
242        let active = self.active_drag()?;
243        let destination_scale = destination.geometry.scale();
244        let scale = if active.origin_scale > 0.0 && destination_scale > 0.0 {
245            active.origin_scale / destination_scale
246        } else {
247            1.0
248        };
249        let grab = self.ctx.grab();
250        Some(Rect::new(
251            pointer.x - grab.x * scale,
252            pointer.y - grab.y * scale,
253            source.width * scale,
254            source.height * scale,
255        ))
256    }
257
258    pub(super) fn clear_world_state(&self) {
259        let mut active = self.active;
260        active.set(None);
261        let mut global_pointer = self.global_pointer;
262        global_pointer.set(None);
263        let mut over_location = self.over_location;
264        over_location.set(None);
265        let mut settle_claim = self.settle_claim;
266        settle_claim.set(None);
267    }
268
269    pub(super) fn enter_location(&self, location: ZoneLocation) {
270        let mut over_location = self.over_location;
271        if *over_location.peek() != Some(location) {
272            over_location.set(Some(location));
273        }
274        let mut ctx = self.ctx;
275        ctx.enter(location.zone);
276    }
277
278    pub(super) fn clear_hover(&self) {
279        let mut ctx = self.ctx;
280        if let Some(over) = ctx.over() {
281            ctx.leave(over);
282        }
283        let mut over_location = self.over_location;
284        if over_location.peek().is_some() {
285            over_location.set(None);
286        }
287    }
288}
289
290#[cfg(test)]
291mod tests {
292    use std::cell::RefCell;
293
294    use super::*;
295    use crate::core::types::{DragMode, DropEffect};
296
297    thread_local! {
298        static WORLD: RefCell<Option<DndWorld<String>>> = const { RefCell::new(None) };
299        static COMPLETION: RefCell<Option<Callback<bool>>> = const { RefCell::new(None) };
300        static REPLACEMENT_ORIGIN: RefCell<Option<WindowKey>> = const { RefCell::new(None) };
301    }
302
303    fn test_app() -> Element {
304        let world = use_hook(DndWorld::<String>::new);
305        let completion = use_callback(move |dropped: bool| {
306            assert!(dropped);
307            let replacement =
308                REPLACEMENT_ORIGIN.with_borrow(|key| key.expect("replacement origin"));
309            let mut ctx = world.context();
310            ctx.start(
311                "replacement".to_string(),
312                None,
313                Point::new(20.0, 30.0),
314                Point::default(),
315                DropEffect::Move,
316                DragMode::Pointer,
317            );
318            world.begin_from(replacement);
319        });
320        WORLD.with_borrow_mut(|slot| *slot = Some(world));
321        COMPLETION.with_borrow_mut(|slot| *slot = Some(completion));
322        rsx! {}
323    }
324
325    #[test]
326    fn source_completion_started_drag_owns_replacement_metadata() {
327        let mut dom = VirtualDom::new(test_app);
328        dom.rebuild_in_place();
329        let world = WORLD.with_borrow(|slot| slot.expect("test world"));
330        let completion = COMPLETION.with_borrow(|slot| slot.expect("completion callback"));
331        dom.in_runtime(|| {
332            let original = WindowKey::auto();
333            let replacement = WindowKey::auto();
334            REPLACEMENT_ORIGIN.with_borrow_mut(|key| *key = Some(replacement));
335            let mut ctx = world.context();
336            let session = ctx.start_tracked(
337                "original".to_string(),
338                None,
339                Point::new(10.0, 10.0),
340                Point::default(),
341                DropEffect::Move,
342                completion,
343            );
344            world.begin_from(original);
345            assert_eq!(world.drag_session(), Some(session));
346
347            assert!(ctx.take().is_some());
348            assert!(world.finish_session(session, DragCompletion::Dropped));
349
350            assert!(ctx.dragging());
351            assert_eq!(ctx.payload().as_deref(), Some("replacement"));
352            assert_eq!(
353                world.active_drag().map(|drag| drag.origin),
354                Some(replacement)
355            );
356            assert_eq!(world.drag_session(), None);
357            world.finish_untracked(DragCompletion::Cancelled(CancelReason::User));
358        });
359    }
360}