1#![doc = include_str!("../docs/api/testing.md")]
2
3use std::any::{Any, TypeId};
4use std::cell::RefCell;
5use std::collections::HashMap;
6
7use dioxus::prelude::*;
8
9use crate::core::components::{
10 deliver_drop, drop_query, resolve_drag_hover, resolve_drag_target, DropCompletion, SettleRoute,
11};
12use crate::core::hooks::SettleFlag;
13use crate::core::monitor::CancelReason;
14use crate::core::world::{JoinedWindow, WorldHit, WorldMembership};
15use crate::core::{
16 use_dnd, use_zone_registry, DndContext, DragCompletion, DropEffect, Point, Rect, WindowKey,
17 ZoneId, ZoneRegistry,
18};
19
20thread_local! {
21 static SIMS: RefCell<HashMap<TypeId, Box<dyn Any>>> = RefCell::new(HashMap::new());
25}
26
27#[component]
31pub fn DragSimProbe<T: Clone + PartialEq + 'static>(
32 #[props(default)]
34 phantom: std::marker::PhantomData<T>,
35) -> Element {
36 let _ = phantom;
37 let completions = use_signal(Vec::<bool>::new);
38 let completion = use_callback(move |dropped| {
39 let mut completions = completions;
40 completions.write().push(dropped);
41 });
42 let sim = DragSim {
43 dnd: use_dnd::<T>(),
44 registry: use_zone_registry::<T>(),
45 settle: try_use_context::<SettleFlag<T>>(),
46 membership: try_use_context::<WorldMembership<T>>().and_then(|m| m.0),
47 completion,
48 completions,
49 };
50 use_hook(move || {
51 SIMS.with_borrow_mut(|m| {
52 m.insert(TypeId::of::<T>(), Box::new(sim));
53 });
54 });
55 rsx! {}
56}
57
58pub fn drag_sim<T: Clone + PartialEq + 'static>() -> DragSim<T> {
64 SIMS.with_borrow(|m| {
65 m.get(&TypeId::of::<T>())
66 .and_then(|b| b.downcast_ref::<DragSim<T>>())
67 .copied()
68 })
69 .expect("no DragSim captured: mount DragSimProbe::<T> inside the provider and rebuild first")
70}
71
72pub struct DragSim<T: Clone + 'static> {
76 dnd: DndContext<T>,
77 registry: ZoneRegistry<T>,
78 settle: Option<SettleFlag<T>>,
79 membership: Option<JoinedWindow<T>>,
82 completion: Callback<bool>,
83 completions: Signal<Vec<bool>>,
84}
85
86impl<T: Clone + 'static> Copy for DragSim<T> {}
87impl<T: Clone + 'static> Clone for DragSim<T> {
88 fn clone(&self) -> Self {
89 *self
90 }
91}
92
93impl<T: Clone + PartialEq + 'static> DragSim<T> {
94 pub fn place(&self, dom: &VirtualDom, zone: ZoneId, rect: Rect) {
99 dom.in_runtime(|| {
100 assert!(
101 self.registry.contains(zone),
102 "place: no zone {} registered",
103 zone.0
104 );
105 let mut registry = self.registry;
106 registry.set_rect(zone, rect);
107 });
108 }
109
110 pub fn window_key(&self) -> Option<WindowKey> {
112 self.membership.map(|j| j.key)
113 }
114
115 pub fn place_in(&self, dom: &VirtualDom, window: WindowKey, zone: ZoneId, rect: Rect) {
122 let world = self
123 .membership
124 .expect("place_in: this provider joined no DndWorld")
125 .world;
126 dom.in_runtime(|| {
127 let rec = world
128 .record(window)
129 .unwrap_or_else(|| panic!("place_in: no window {} joined", window.0));
130 assert!(
131 rec.registry.contains(zone),
132 "place_in: no zone {} in window {}",
133 zone.0,
134 window.0
135 );
136 let mut registry = rec.registry;
137 registry.set_rect(zone, rect);
138 });
139 }
140
141 pub fn pick_up(&mut self, dom: &VirtualDom, payload: T) {
143 self.pick_up_from(dom, payload, None);
144 }
145
146 pub fn pick_up_from(&mut self, dom: &VirtualDom, payload: T, from: Option<ZoneId>) {
149 let mut dnd = self.dnd;
150 let membership = self.membership;
151 dom.in_runtime(|| {
152 let session = dnd.start_tracked(
153 payload,
154 from,
155 Point::default(),
156 Point::default(),
157 DropEffect::Move,
158 self.completion,
159 );
160 if dnd.is_session(session) {
162 if let Some(j) = membership {
163 j.world.begin_from(j.key);
164 }
165 }
166 });
167 }
168
169 pub fn move_to(&mut self, dom: &VirtualDom, point: Point) {
173 let mut dnd = self.dnd;
174 let registry = self.registry;
175 let membership = self.membership;
176 dom.in_runtime(|| {
177 let session = dnd.active_session();
178 dnd.update_pointer(point);
179 if session.is_some_and(|session| !dnd.is_session(session)) {
180 return;
181 }
182 let query = dnd
183 .payload()
184 .map(|payload| drop_query(&dnd, payload, dnd.effect()));
185 match membership {
189 Some(joined) => match query
190 .as_ref()
191 .map(|query| joined.zone_under_query(point, query))
192 .unwrap_or(WorldHit::Unresolved)
193 {
194 WorldHit::Zone(location) => joined.enter(location),
195 WorldHit::Window => joined.clear_hover(),
196 WorldHit::Unresolved => {
197 match resolve_drag_hover(registry, &dnd, point, dnd.effect()) {
198 Some(zone) => joined.enter(joined.location(zone)),
199 None => joined.clear_hover(),
200 }
201 }
202 },
203 None => match resolve_drag_hover(registry, &dnd, point, dnd.effect()) {
204 Some(zone) => dnd.enter(zone),
205 None => {
206 if let Some(over) = dnd.over() {
207 dnd.leave(over);
208 }
209 }
210 },
211 }
212 });
213 }
214
215 pub fn release(&mut self, dom: &VirtualDom) -> Option<ZoneId> {
220 self.release_as(dom, DropEffect::Move)
221 }
222
223 pub fn release_as(&mut self, dom: &VirtualDom, effect: DropEffect) -> Option<ZoneId> {
226 let mut dnd = self.dnd;
227 let registry = self.registry;
228 let settle = self.settle;
229 let membership = self.membership;
230 dom.in_runtime(|| {
231 let point = dnd.pointer();
232 let session = dnd.active_session();
233 if let Some(j) = membership {
238 let _ = j.zone_under(point);
239 if let Some((rec, local)) = j.foreign_window_under(point) {
240 let target = dnd.payload().and_then(|payload| {
241 let query = drop_query(&dnd, payload, effect);
242 rec.registry
243 .resolve(
244 &query,
245 local,
246 j.world.active_rect_in(rec, local),
247 rec.registry.release_policy().recovery_radius,
248 )
249 .map(|(zone, _)| zone)
250 });
251 let delivered = target
252 .filter(|t| {
253 deliver_drop(
254 rec.registry,
255 &mut dnd,
256 SettleRoute {
257 flag: Some(rec.settle),
258 owner: Some((&j.world, rec.key)),
259 },
260 DropCompletion::World {
261 world: &j.world,
262 session,
263 },
264 *t,
265 local,
266 effect,
267 )
268 })
269 .is_some();
270 if !delivered {
271 match session {
272 Some(session) => {
273 j.world.finish_session(
274 session,
275 DragCompletion::Cancelled(CancelReason::NoTarget),
276 );
277 }
278 None => j.world.finish_untracked(DragCompletion::Cancelled(
279 CancelReason::NoTarget,
280 )),
281 }
282 return None;
283 }
284 return target;
285 }
286 }
287 let target = resolve_drag_target(
288 registry,
289 &dnd,
290 point,
291 effect,
292 registry.release_policy().recovery_radius,
293 );
294 let delivered = target
295 .filter(|t| match membership {
296 Some(j) => deliver_drop(
297 registry,
298 &mut dnd,
299 SettleRoute {
300 flag: settle,
301 owner: Some((&j.world, j.key)),
302 },
303 DropCompletion::World {
304 world: &j.world,
305 session,
306 },
307 *t,
308 point,
309 effect,
310 ),
311 None => deliver_drop(
312 registry,
313 &mut dnd,
314 SettleRoute {
315 flag: settle,
316 owner: None,
317 },
318 match session {
319 Some(session) => DropCompletion::Local(session),
320 None => DropCompletion::None,
321 },
322 *t,
323 point,
324 effect,
325 ),
326 })
327 .is_some();
328 if !delivered {
329 match membership {
330 Some(j) => match session {
331 Some(session) => {
332 j.world.finish_session(
333 session,
334 DragCompletion::Cancelled(CancelReason::NoTarget),
335 );
336 }
337 None => j
338 .world
339 .finish_untracked(DragCompletion::Cancelled(CancelReason::NoTarget)),
340 },
341 None => match session {
342 Some(session) => {
343 dnd.cancel_session(session, CancelReason::NoTarget);
344 }
345 None => dnd.cancel_with_reason(CancelReason::NoTarget),
346 },
347 }
348 return None;
349 }
350 target
351 })
352 }
353
354 pub fn cancel(&mut self, dom: &VirtualDom) {
356 let mut dnd = self.dnd;
357 let membership = self.membership;
358 dom.in_runtime(|| {
359 let session = dnd.active_session();
360 match membership {
361 Some(j) => match session {
362 Some(session) => {
363 j.world
364 .finish_session(session, DragCompletion::Cancelled(CancelReason::User));
365 }
366 None => {
367 j.world
368 .finish_untracked(DragCompletion::Cancelled(CancelReason::User));
369 }
370 },
371 None => match session {
372 Some(session) => {
373 dnd.cancel_session(session, CancelReason::User);
374 }
375 None => dnd.cancel(),
376 },
377 }
378 });
379 }
380
381 pub fn completions(&self, dom: &VirtualDom) -> Vec<bool> {
384 dom.in_runtime(|| self.completions.read().clone())
385 }
386
387 pub fn over(&self, dom: &VirtualDom) -> Option<ZoneId> {
389 dom.in_runtime(|| self.dnd.over())
390 }
391
392 pub fn dragging(&self, dom: &VirtualDom) -> bool {
394 dom.in_runtime(|| self.dnd.dragging())
395 }
396
397 pub fn payload(&self, dom: &VirtualDom) -> Option<T> {
399 dom.in_runtime(|| self.dnd.payload())
400 }
401
402 pub fn announcement(&self, dom: &VirtualDom) -> String {
404 dom.in_runtime(|| self.dnd.announcement())
405 }
406}
407
408pub fn rerender(dom: &mut VirtualDom) {
412 dom.process_events();
413 dom.render_immediate(&mut dioxus::core::NoOpMutations);
414}
415
416pub fn simulate_drag<T: Clone + PartialEq + 'static>(
422 dom: &mut VirtualDom,
423 payload: T,
424 from: Option<ZoneId>,
425 path: &[Point],
426) -> Option<ZoneId> {
427 let mut sim = drag_sim::<T>();
428 sim.pick_up_from(dom, payload, from);
429 rerender(dom);
430 for p in path {
431 sim.move_to(dom, *p);
432 rerender(dom);
433 }
434 let delivered = sim.release(dom);
435 rerender(dom);
436 delivered
437}