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::types::{DragSessionId, Point};
9
10use super::geometry::WindowKey;
11use super::state::{DndWorld, WindowRecord, ZoneLocation};
12
13static NEXT_WORLD_DRAG_GENERATION: AtomicU64 = AtomicU64::new(1);
14
15#[derive(Debug, Clone, Copy, PartialEq)]
16pub(super) struct ActiveDrag {
17    pub(super) origin: WindowKey,
18    /// Fresh for every `begin_from`, including custom/untracked sources. Host
19    /// adapters bind observations to this rather than treating `session: None`
20    /// as an authority token that could attach to a successor drag.
21    pub(super) generation: u64,
22    pub(super) session: Option<DragSessionId>,
23    pub(super) origin_scale: f64,
24    pub(super) source_location: Option<ZoneLocation>,
25    pub(super) modifiers: Modifiers,
26}
27
28impl<T: Clone + 'static> DndWorld<T> {
29    /// Mark a drag as begun from `key` and reset stale presentation state.
30    /// `Draggable` calls this at pickup; call it from custom drag sources
31    /// so the world knows which window's client px `ctx.pointer()` is in.
32    pub fn begin_from(&self, key: WindowKey) {
33        let origin = self.record(key);
34        let active_drag = ActiveDrag {
35            origin: key,
36            generation: NEXT_WORLD_DRAG_GENERATION.fetch_add(1, Ordering::Relaxed),
37            // Receiver code may synchronously start an untracked drag while
38            // the old source result is committed but not yet finalized. Do
39            // not attach that old generation to the replacement.
40            session: self
41                .ctx
42                .active_session()
43                .filter(|session| self.ctx.session_result(*session).is_none()),
44            origin_scale: origin.map_or(1.0, |record| record.geometry.scale()),
45            source_location: self
46                .ctx
47                .source()
48                .map(|zone| ZoneLocation { window: key, zone }),
49            modifiers: Modifiers::empty(),
50        };
51        let mut active = self.active;
52        if *active.peek() != Some(active_drag) {
53            active.set(Some(active_drag));
54        }
55        let mut settle_claim = self.settle_claim;
56        if settle_claim.peek().is_some() {
57            settle_claim.set(None);
58        }
59        let mut global_pointer = self.global_pointer;
60        let initial_global =
61            origin.and_then(|record| record.geometry.to_global(self.ctx.pointer()));
62        if *global_pointer.peek() != initial_global {
63            global_pointer.set(initial_global);
64        }
65        let mut over_location = self.over_location;
66        if over_location.peek().is_some() {
67            over_location.set(None);
68        }
69    }
70
71    /// The record of the window the in-flight drag started in.
72    pub fn active_record(&self) -> Option<WindowRecord<T>> {
73        let origin = self.active.peek().as_ref()?.origin;
74        self.record(origin)
75    }
76
77    pub(super) fn active_drag(&self) -> Option<ActiveDrag> {
78        *self.active.peek()
79    }
80
81    /// The in-flight pointer in global physical px. `None` until a world
82    /// pointer can be resolved or after the world drag finishes.
83    pub fn global_pointer(&self) -> Option<Point> {
84        *self.global_pointer.read()
85    }
86
87    /// Window-qualified source and hover locations for the active world
88    /// drag. The legacy `DndContext` id accessors remain unchanged.
89    pub fn source_location(&self) -> Option<ZoneLocation> {
90        self.active
91            .read()
92            .as_ref()
93            .and_then(|active| active.source_location)
94    }
95
96    pub fn over_location(&self) -> Option<ZoneLocation> {
97        *self.over_location.read()
98    }
99
100    /// Current tracked pointer-drag generation, if this world owns one.
101    pub fn drag_session(&self) -> Option<DragSessionId> {
102        self.active.peek().as_ref()?.session
103    }
104
105    /// Private host-adapter token for the current world drag. The generation
106    /// is mandatory; the optional source session adds exactly-once completion
107    /// ownership for built-in tracked sources.
108    #[cfg_attr(not(feature = "desktop"), allow(dead_code))]
109    pub(crate) fn drag_generation(&self) -> Option<(u64, Option<DragSessionId>)> {
110        let active = self.active.read();
111        let active = active.as_ref()?;
112        Some((active.generation, active.session))
113    }
114
115    /// Non-subscribing generation read for imperative host event handlers.
116    /// Async resources use [`Self::drag_generation`] so `begin_from` wakes a
117    /// new run even when all other drag gates retain the same values.
118    #[cfg_attr(not(feature = "desktop"), allow(dead_code))]
119    pub(crate) fn drag_generation_peek(&self) -> Option<(u64, Option<DragSessionId>)> {
120        let active = self.active_drag()?;
121        Some((active.generation, active.session))
122    }
123
124    /// Whether both halves of a captured host token still name the active
125    /// drag. For untracked custom sources, `None` is valid only alongside the
126    /// matching mandatory world generation.
127    #[cfg_attr(not(feature = "desktop"), allow(dead_code))]
128    pub(crate) fn is_drag_generation(
129        &self,
130        generation: u64,
131        session: Option<DragSessionId>,
132    ) -> bool {
133        let Some(active) = self.active_drag() else {
134            return false;
135        };
136        if !self.ctx.dragging() || active.generation != generation || active.session != session {
137            return false;
138        }
139        session.is_none_or(|session| self.ctx.is_session(session))
140    }
141
142    pub(crate) fn is_drag_session(&self, session: DragSessionId) -> bool {
143        self.drag_session() == Some(session) && self.ctx.is_session(session)
144    }
145
146    pub(crate) fn commit_session(&self, session: DragSessionId, dropped: bool) -> bool {
147        if !self.is_drag_session(session) {
148            return false;
149        }
150        let mut ctx = self.ctx;
151        ctx.commit_source(session, dropped)
152    }
153
154    pub(crate) fn finalize_session(&self, session: DragSessionId) -> bool {
155        let Some(result) = self.ctx.session_result(session) else {
156            return false;
157        };
158        self.finish_session(session, result)
159    }
160
161    pub(crate) fn finish_session(&self, session: DragSessionId, dropped: bool) -> bool {
162        let mut ctx = self.ctx;
163        if !ctx.is_session(session) {
164            return false;
165        }
166        let owns_metadata = self.drag_session() == Some(session);
167        let result = ctx.session_result(session).unwrap_or(dropped);
168        let finished = if ctx.session_result(session).is_some() {
169            ctx.finalize_source(session)
170        } else if dropped {
171            ctx.finish_source(session, true)
172        } else {
173            ctx.cancel_session(session)
174        };
175        if !finished {
176            return false;
177        }
178        if !owns_metadata || self.drag_session() != Some(session) {
179            return true;
180        }
181        // Source notification is user code and may synchronously begin a
182        // replacement. Its new begin_from call owns the metadata now.
183        if ctx.dragging() {
184            return true;
185        }
186        if result && ctx.settling().is_some() {
187            let mut active = self.active;
188            let current = *active.peek();
189            if let Some(mut current) = current {
190                current.session = None;
191                active.set(Some(current));
192            }
193            self.clear_hover();
194        } else {
195            self.clear_world_state();
196        }
197        true
198    }
199
200    pub(crate) fn finish_untracked(&self, dropped: bool) {
201        let mut ctx = self.ctx;
202        if !dropped && ctx.dragging() {
203            ctx.cancel();
204        }
205        if ctx.dragging() {
206            return;
207        }
208        if dropped && ctx.settling().is_some() {
209            self.clear_hover();
210        } else {
211            self.clear_world_state();
212        }
213    }
214
215    pub(super) fn clear_world_state(&self) {
216        let mut active = self.active;
217        active.set(None);
218        let mut global_pointer = self.global_pointer;
219        global_pointer.set(None);
220        let mut over_location = self.over_location;
221        over_location.set(None);
222        let mut settle_claim = self.settle_claim;
223        settle_claim.set(None);
224    }
225
226    pub(super) fn enter_location(&self, location: ZoneLocation) {
227        let mut over_location = self.over_location;
228        if *over_location.peek() != Some(location) {
229            over_location.set(Some(location));
230        }
231        let mut ctx = self.ctx;
232        ctx.enter(location.zone);
233    }
234
235    pub(super) fn clear_hover(&self) {
236        let mut ctx = self.ctx;
237        if let Some(over) = ctx.over() {
238            ctx.leave(over);
239        }
240        let mut over_location = self.over_location;
241        if over_location.peek().is_some() {
242            over_location.set(None);
243        }
244    }
245}