dioxus_dnd/core/world/
drag.rs1use 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
15static 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 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 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 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 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 pub fn global_pointer(&self) -> Option<Point> {
89 *self.global_pointer.read()
90 }
91
92 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 pub fn drag_session(&self) -> Option<DragSessionId> {
107 self.active.peek().as_ref()?.session
108 }
109
110 #[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 #[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 #[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 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}