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::{deliver_drop, DropCompletion, SettleRoute};
10use crate::core::hooks::SettleFlag;
11use crate::core::world::{JoinedWindow, WorldHit, WorldMembership};
12use crate::core::{
13 use_dnd, use_zone_registry, DndContext, DropEffect, Point, Rect, WindowKey, ZoneId,
14 ZoneRegistry,
15};
16
17thread_local! {
18 static SIMS: RefCell<HashMap<TypeId, Box<dyn Any>>> = RefCell::new(HashMap::new());
22}
23
24#[component]
28pub fn DragSimProbe<T: Clone + PartialEq + 'static>(
29 #[props(default)]
31 phantom: std::marker::PhantomData<T>,
32) -> Element {
33 let _ = phantom;
34 let completions = use_signal(Vec::<bool>::new);
35 let completion = use_callback(move |dropped| {
36 let mut completions = completions;
37 completions.write().push(dropped);
38 });
39 let sim = DragSim {
40 dnd: use_dnd::<T>(),
41 registry: use_zone_registry::<T>(),
42 settle: try_use_context::<SettleFlag<T>>(),
43 membership: try_use_context::<WorldMembership<T>>().and_then(|m| m.0),
44 completion,
45 completions,
46 };
47 use_hook(move || {
48 SIMS.with_borrow_mut(|m| {
49 m.insert(TypeId::of::<T>(), Box::new(sim));
50 });
51 });
52 rsx! {}
53}
54
55pub fn drag_sim<T: Clone + PartialEq + 'static>() -> DragSim<T> {
61 SIMS.with_borrow(|m| {
62 m.get(&TypeId::of::<T>())
63 .and_then(|b| b.downcast_ref::<DragSim<T>>())
64 .copied()
65 })
66 .expect("no DragSim captured: mount DragSimProbe::<T> inside the provider and rebuild first")
67}
68
69pub struct DragSim<T: Clone + 'static> {
73 dnd: DndContext<T>,
74 registry: ZoneRegistry<T>,
75 settle: Option<SettleFlag<T>>,
76 membership: Option<JoinedWindow<T>>,
79 completion: Callback<bool>,
80 completions: Signal<Vec<bool>>,
81}
82
83impl<T: Clone + 'static> Copy for DragSim<T> {}
84impl<T: Clone + 'static> Clone for DragSim<T> {
85 fn clone(&self) -> Self {
86 *self
87 }
88}
89
90impl<T: Clone + PartialEq + 'static> DragSim<T> {
91 pub fn place(&self, dom: &VirtualDom, zone: ZoneId, rect: Rect) {
96 dom.in_runtime(|| {
97 assert!(
98 self.registry.contains(zone),
99 "place: no zone {} registered",
100 zone.0
101 );
102 let mut registry = self.registry;
103 registry.set_rect(zone, rect);
104 });
105 }
106
107 pub fn window_key(&self) -> Option<WindowKey> {
109 self.membership.map(|j| j.key)
110 }
111
112 pub fn place_in(&self, dom: &VirtualDom, window: WindowKey, zone: ZoneId, rect: Rect) {
119 let world = self
120 .membership
121 .expect("place_in: this provider joined no DndWorld")
122 .world;
123 dom.in_runtime(|| {
124 let rec = world
125 .record(window)
126 .unwrap_or_else(|| panic!("place_in: no window {} joined", window.0));
127 assert!(
128 rec.registry.contains(zone),
129 "place_in: no zone {} in window {}",
130 zone.0,
131 window.0
132 );
133 let mut registry = rec.registry;
134 registry.set_rect(zone, rect);
135 });
136 }
137
138 pub fn pick_up(&mut self, dom: &VirtualDom, payload: T) {
140 self.pick_up_from(dom, payload, None);
141 }
142
143 pub fn pick_up_from(&mut self, dom: &VirtualDom, payload: T, from: Option<ZoneId>) {
146 let mut dnd = self.dnd;
147 let membership = self.membership;
148 dom.in_runtime(|| {
149 dnd.start_tracked(
150 payload,
151 from,
152 Point::default(),
153 Point::default(),
154 DropEffect::Move,
155 self.completion,
156 );
157 if let Some(j) = membership {
159 j.world.begin_from(j.key);
160 }
161 });
162 }
163
164 pub fn move_to(&mut self, dom: &VirtualDom, point: Point) {
168 let mut dnd = self.dnd;
169 let registry = self.registry;
170 let membership = self.membership;
171 dom.in_runtime(|| {
172 dnd.update_pointer(point);
173 match membership {
177 Some(joined) => match joined.zone_under(point) {
178 WorldHit::Zone(location) => joined.enter(location),
179 WorldHit::Window => joined.clear_hover(),
180 WorldHit::Unresolved => match registry.hit_test(point) {
181 Some(zone) => joined.enter(joined.location(zone)),
182 None => joined.clear_hover(),
183 },
184 },
185 None => match registry.hit_test(point) {
186 Some(zone) => dnd.enter(zone),
187 None => {
188 if let Some(over) = dnd.over() {
189 dnd.leave(over);
190 }
191 }
192 },
193 }
194 });
195 }
196
197 pub fn release(&mut self, dom: &VirtualDom) -> Option<ZoneId> {
202 self.release_as(dom, DropEffect::Move)
203 }
204
205 pub fn release_as(&mut self, dom: &VirtualDom, effect: DropEffect) -> Option<ZoneId> {
208 let mut dnd = self.dnd;
209 let registry = self.registry;
210 let settle = self.settle;
211 let membership = self.membership;
212 dom.in_runtime(|| {
213 let point = dnd.pointer();
214 let session = dnd.active_session();
215 if let Some(j) = membership {
220 let _ = j.zone_under(point);
221 if let Some((rec, local)) = j.foreign_window_under(point) {
222 let target = rec.registry.hit_test(local).or_else(|| {
223 dnd.payload()
224 .and_then(|p| rec.registry.hit_test_closest(local, &p, 48.0))
225 });
226 let delivered = target
227 .filter(|t| {
228 deliver_drop(
229 rec.registry,
230 &mut dnd,
231 SettleRoute {
232 flag: Some(rec.settle),
233 owner: Some((&j.world, rec.key)),
234 },
235 DropCompletion::World {
236 world: &j.world,
237 session,
238 },
239 *t,
240 local,
241 effect,
242 )
243 })
244 .is_some();
245 if !delivered {
246 match session {
247 Some(session) => {
248 j.world.finish_session(session, false);
249 }
250 None => j.world.finish_untracked(false),
251 }
252 return None;
253 }
254 return target;
255 }
256 }
257 let target = registry.hit_test(point).or_else(|| {
258 dnd.payload()
259 .and_then(|p| registry.hit_test_closest(point, &p, 48.0))
260 });
261 let delivered = target
262 .filter(|t| match membership {
263 Some(j) => deliver_drop(
264 registry,
265 &mut dnd,
266 SettleRoute {
267 flag: settle,
268 owner: Some((&j.world, j.key)),
269 },
270 DropCompletion::World {
271 world: &j.world,
272 session,
273 },
274 *t,
275 point,
276 effect,
277 ),
278 None => deliver_drop(
279 registry,
280 &mut dnd,
281 SettleRoute {
282 flag: settle,
283 owner: None,
284 },
285 match session {
286 Some(session) => DropCompletion::Local(session),
287 None => DropCompletion::None,
288 },
289 *t,
290 point,
291 effect,
292 ),
293 })
294 .is_some();
295 if !delivered {
296 match membership {
297 Some(j) => match session {
298 Some(session) => {
299 j.world.finish_session(session, false);
300 }
301 None => j.world.finish_untracked(false),
302 },
303 None => match session {
304 Some(session) => {
305 dnd.cancel_session(session);
306 }
307 None => dnd.cancel(),
308 },
309 }
310 return None;
311 }
312 target
313 })
314 }
315
316 pub fn cancel(&mut self, dom: &VirtualDom) {
318 let mut dnd = self.dnd;
319 let membership = self.membership;
320 dom.in_runtime(|| {
321 let session = dnd.active_session();
322 match membership {
323 Some(j) => match session {
324 Some(session) => {
325 j.world.finish_session(session, false);
326 }
327 None => j.world.finish_untracked(false),
328 },
329 None => match session {
330 Some(session) => {
331 dnd.cancel_session(session);
332 }
333 None => dnd.cancel(),
334 },
335 }
336 });
337 }
338
339 pub fn completions(&self, dom: &VirtualDom) -> Vec<bool> {
342 dom.in_runtime(|| self.completions.read().clone())
343 }
344
345 pub fn over(&self, dom: &VirtualDom) -> Option<ZoneId> {
347 dom.in_runtime(|| self.dnd.over())
348 }
349
350 pub fn dragging(&self, dom: &VirtualDom) -> bool {
352 dom.in_runtime(|| self.dnd.dragging())
353 }
354
355 pub fn payload(&self, dom: &VirtualDom) -> Option<T> {
357 dom.in_runtime(|| self.dnd.payload())
358 }
359
360 pub fn announcement(&self, dom: &VirtualDom) -> String {
362 dom.in_runtime(|| self.dnd.announcement())
363 }
364}
365
366pub fn rerender(dom: &mut VirtualDom) {
370 dom.process_events();
371 dom.render_immediate(&mut dioxus::core::NoOpMutations);
372}
373
374pub fn simulate_drag<T: Clone + PartialEq + 'static>(
380 dom: &mut VirtualDom,
381 payload: T,
382 from: Option<ZoneId>,
383 path: &[Point],
384) -> Option<ZoneId> {
385 let mut sim = drag_sim::<T>();
386 sim.pick_up_from(dom, payload, from);
387 rerender(dom);
388 for p in path {
389 sim.move_to(dom, *p);
390 rerender(dom);
391 }
392 let delivered = sim.release(dom);
393 rerender(dom);
394 delivered
395}