#![doc = include_str!("../docs/api/testing.md")]
use std::any::{Any, TypeId};
use std::cell::RefCell;
use std::collections::HashMap;
use dioxus::prelude::*;
use crate::core::components::{
deliver_drop, drop_query, resolve_drag_hover, resolve_drag_target, DropCompletion, SettleRoute,
};
use crate::core::hooks::SettleFlag;
use crate::core::monitor::CancelReason;
use crate::core::world::{JoinedWindow, WorldHit, WorldMembership};
use crate::core::{
use_dnd, use_zone_registry, DndContext, DragCompletion, DropEffect, Point, Rect, WindowKey,
ZoneId, ZoneRegistry,
};
thread_local! {
static SIMS: RefCell<HashMap<TypeId, Box<dyn Any>>> = RefCell::new(HashMap::new());
}
#[component]
pub fn DragSimProbe<T: Clone + PartialEq + 'static>(
#[props(default)]
phantom: std::marker::PhantomData<T>,
) -> Element {
let _ = phantom;
let completions = use_signal(Vec::<bool>::new);
let completion = use_callback(move |dropped| {
let mut completions = completions;
completions.write().push(dropped);
});
let sim = DragSim {
dnd: use_dnd::<T>(),
registry: use_zone_registry::<T>(),
settle: try_use_context::<SettleFlag<T>>(),
membership: try_use_context::<WorldMembership<T>>().and_then(|m| m.0),
completion,
completions,
};
use_hook(move || {
SIMS.with_borrow_mut(|m| {
m.insert(TypeId::of::<T>(), Box::new(sim));
});
});
rsx! {}
}
pub fn drag_sim<T: Clone + PartialEq + 'static>() -> DragSim<T> {
SIMS.with_borrow(|m| {
m.get(&TypeId::of::<T>())
.and_then(|b| b.downcast_ref::<DragSim<T>>())
.copied()
})
.expect("no DragSim captured: mount DragSimProbe::<T> inside the provider and rebuild first")
}
pub struct DragSim<T: Clone + 'static> {
dnd: DndContext<T>,
registry: ZoneRegistry<T>,
settle: Option<SettleFlag<T>>,
membership: Option<JoinedWindow<T>>,
completion: Callback<bool>,
completions: Signal<Vec<bool>>,
}
impl<T: Clone + 'static> Copy for DragSim<T> {}
impl<T: Clone + 'static> Clone for DragSim<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T: Clone + PartialEq + 'static> DragSim<T> {
pub fn place(&self, dom: &VirtualDom, zone: ZoneId, rect: Rect) {
dom.in_runtime(|| {
assert!(
self.registry.contains(zone),
"place: no zone {} registered",
zone.0
);
let mut registry = self.registry;
registry.set_rect(zone, rect);
});
}
pub fn window_key(&self) -> Option<WindowKey> {
self.membership.map(|j| j.key)
}
pub fn place_in(&self, dom: &VirtualDom, window: WindowKey, zone: ZoneId, rect: Rect) {
let world = self
.membership
.expect("place_in: this provider joined no DndWorld")
.world;
dom.in_runtime(|| {
let rec = world
.record(window)
.unwrap_or_else(|| panic!("place_in: no window {} joined", window.0));
assert!(
rec.registry.contains(zone),
"place_in: no zone {} in window {}",
zone.0,
window.0
);
let mut registry = rec.registry;
registry.set_rect(zone, rect);
});
}
pub fn pick_up(&mut self, dom: &VirtualDom, payload: T) {
self.pick_up_from(dom, payload, None);
}
pub fn pick_up_from(&mut self, dom: &VirtualDom, payload: T, from: Option<ZoneId>) {
let mut dnd = self.dnd;
let membership = self.membership;
dom.in_runtime(|| {
let session = dnd.start_tracked(
payload,
from,
Point::default(),
Point::default(),
DropEffect::Move,
self.completion,
);
if dnd.is_session(session) {
if let Some(j) = membership {
j.world.begin_from(j.key);
}
}
});
}
pub fn move_to(&mut self, dom: &VirtualDom, point: Point) {
let mut dnd = self.dnd;
let registry = self.registry;
let membership = self.membership;
dom.in_runtime(|| {
let session = dnd.active_session();
dnd.update_pointer(point);
if session.is_some_and(|session| !dnd.is_session(session)) {
return;
}
let query = dnd
.payload()
.map(|payload| drop_query(&dnd, payload, dnd.effect()));
match membership {
Some(joined) => match query
.as_ref()
.map(|query| joined.zone_under_query(point, query))
.unwrap_or(WorldHit::Unresolved)
{
WorldHit::Zone(location) => joined.enter(location),
WorldHit::Window => joined.clear_hover(),
WorldHit::Unresolved => {
match resolve_drag_hover(registry, &dnd, point, dnd.effect()) {
Some(zone) => joined.enter(joined.location(zone)),
None => joined.clear_hover(),
}
}
},
None => match resolve_drag_hover(registry, &dnd, point, dnd.effect()) {
Some(zone) => dnd.enter(zone),
None => {
if let Some(over) = dnd.over() {
dnd.leave(over);
}
}
},
}
});
}
pub fn release(&mut self, dom: &VirtualDom) -> Option<ZoneId> {
self.release_as(dom, DropEffect::Move)
}
pub fn release_as(&mut self, dom: &VirtualDom, effect: DropEffect) -> Option<ZoneId> {
let mut dnd = self.dnd;
let registry = self.registry;
let settle = self.settle;
let membership = self.membership;
dom.in_runtime(|| {
let point = dnd.pointer();
let session = dnd.active_session();
if let Some(j) = membership {
let _ = j.zone_under(point);
if let Some((rec, local)) = j.foreign_window_under(point) {
let target = dnd.payload().and_then(|payload| {
let query = drop_query(&dnd, payload, effect);
rec.registry
.resolve(
&query,
local,
j.world.active_rect_in(rec, local),
rec.registry.release_policy().recovery_radius,
)
.map(|(zone, _)| zone)
});
let delivered = target
.filter(|t| {
deliver_drop(
rec.registry,
&mut dnd,
SettleRoute {
flag: Some(rec.settle),
owner: Some((&j.world, rec.key)),
},
DropCompletion::World {
world: &j.world,
session,
},
*t,
local,
effect,
)
})
.is_some();
if !delivered {
match session {
Some(session) => {
j.world.finish_session(
session,
DragCompletion::Cancelled(CancelReason::NoTarget),
);
}
None => j.world.finish_untracked(DragCompletion::Cancelled(
CancelReason::NoTarget,
)),
}
return None;
}
return target;
}
}
let target = resolve_drag_target(
registry,
&dnd,
point,
effect,
registry.release_policy().recovery_radius,
);
let delivered = target
.filter(|t| match membership {
Some(j) => deliver_drop(
registry,
&mut dnd,
SettleRoute {
flag: settle,
owner: Some((&j.world, j.key)),
},
DropCompletion::World {
world: &j.world,
session,
},
*t,
point,
effect,
),
None => deliver_drop(
registry,
&mut dnd,
SettleRoute {
flag: settle,
owner: None,
},
match session {
Some(session) => DropCompletion::Local(session),
None => DropCompletion::None,
},
*t,
point,
effect,
),
})
.is_some();
if !delivered {
match membership {
Some(j) => match session {
Some(session) => {
j.world.finish_session(
session,
DragCompletion::Cancelled(CancelReason::NoTarget),
);
}
None => j
.world
.finish_untracked(DragCompletion::Cancelled(CancelReason::NoTarget)),
},
None => match session {
Some(session) => {
dnd.cancel_session(session, CancelReason::NoTarget);
}
None => dnd.cancel_with_reason(CancelReason::NoTarget),
},
}
return None;
}
target
})
}
pub fn cancel(&mut self, dom: &VirtualDom) {
let mut dnd = self.dnd;
let membership = self.membership;
dom.in_runtime(|| {
let session = dnd.active_session();
match membership {
Some(j) => match session {
Some(session) => {
j.world
.finish_session(session, DragCompletion::Cancelled(CancelReason::User));
}
None => {
j.world
.finish_untracked(DragCompletion::Cancelled(CancelReason::User));
}
},
None => match session {
Some(session) => {
dnd.cancel_session(session, CancelReason::User);
}
None => dnd.cancel(),
},
}
});
}
pub fn completions(&self, dom: &VirtualDom) -> Vec<bool> {
dom.in_runtime(|| self.completions.read().clone())
}
pub fn over(&self, dom: &VirtualDom) -> Option<ZoneId> {
dom.in_runtime(|| self.dnd.over())
}
pub fn dragging(&self, dom: &VirtualDom) -> bool {
dom.in_runtime(|| self.dnd.dragging())
}
pub fn payload(&self, dom: &VirtualDom) -> Option<T> {
dom.in_runtime(|| self.dnd.payload())
}
pub fn announcement(&self, dom: &VirtualDom) -> String {
dom.in_runtime(|| self.dnd.announcement())
}
}
pub fn rerender(dom: &mut VirtualDom) {
dom.process_events();
dom.render_immediate(&mut dioxus::core::NoOpMutations);
}
pub fn simulate_drag<T: Clone + PartialEq + 'static>(
dom: &mut VirtualDom,
payload: T,
from: Option<ZoneId>,
path: &[Point],
) -> Option<ZoneId> {
let mut sim = drag_sim::<T>();
sim.pick_up_from(dom, payload, from);
rerender(dom);
for p in path {
sim.move_to(dom, *p);
rerender(dom);
}
let delivered = sim.release(dom);
rerender(dom);
delivered
}