use dioxus::html::MountedData;
use dioxus::prelude::*;
use std::rc::Rc;
use super::hooks::{use_dnd, use_dnd_provider, use_zone_id, use_zone_registry};
use super::registry::ZoneRecord;
use super::{platform, transition, GestureEffect, GestureEvent, GesturePhase};
#[derive(Clone, Copy, PartialEq)]
pub struct ParentZone(pub ZoneId);
#[derive(Debug, Clone, Copy, PartialEq)]
enum NavKey {
Next,
Prev,
Descend,
Ascend,
}
use super::types::{
effective_effect, Direction, DragMode, DropEffect, DropOutcome, Point, Rect, ZoneId,
};
fn nav_key(key: &Key, dir: Direction) -> Option<NavKey> {
Some(match (key, dir) {
(Key::ArrowDown, _) => NavKey::Next,
(Key::ArrowUp, _) => NavKey::Prev,
(Key::ArrowRight, Direction::Ltr) | (Key::ArrowLeft, Direction::Rtl) => NavKey::Descend,
(Key::ArrowLeft, Direction::Ltr) | (Key::ArrowRight, Direction::Rtl) => NavKey::Ascend,
_ => return None,
})
}
pub(crate) fn merge_style(attributes: &mut Vec<Attribute>, functional: &str) -> String {
let user = attributes
.iter()
.position(|a| a.name == "style")
.map(|i| attributes.remove(i));
match user.map(|a| a.value) {
Some(dioxus::core::AttributeValue::Text(s)) => format!("{functional} {s}"),
_ => functional.to_string(),
}
}
fn keyboard_drop_points(rect: Option<Rect>) -> (Point, Point) {
match rect {
Some(r) => {
let client = r.center();
(client, client - r.origin())
}
None => (Point::default(), Point::default()),
}
}
#[component]
pub fn DndProvider<T: Clone + PartialEq + 'static>(
#[props(default)]
phantom: std::marker::PhantomData<T>,
#[props(default)]
dir: Direction,
children: Element,
) -> Element {
let _ = phantom;
use_dnd_provider::<T>();
use_zone_registry::<T>().set_direction(dir);
rsx! {
{children}
}
}
fn pointer_client(evt: &PointerEvent) -> Point {
let c = evt.client_coordinates();
Point::new(c.x, c.y)
}
#[component]
pub fn Draggable<T: Clone + PartialEq + 'static>(
payload: T,
#[props(default)]
zone: Option<ZoneId>,
#[props(default)]
effect: DropEffect,
#[props(default)]
disabled: bool,
#[props(default = 8.0)]
threshold: f64,
#[props(default)]
label: Option<String>,
#[props(default)]
on_drag_start: Option<EventHandler<()>>,
#[props(default)]
on_drag_end: Option<EventHandler<bool>>,
#[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
children: Element,
) -> Element {
let mut dnd = use_dnd::<T>();
let registry = use_zone_registry::<T>();
let kb_payload = payload.clone();
let pointer_payload = payload.clone();
let kb_label = label.clone();
let attr_payload = payload.clone();
let mut phase = use_signal(|| GesturePhase::Idle);
let mut step = move |event: GestureEvent, threshold: f64| -> GestureEffect {
let (next, fx) = transition(*phase.peek(), event, threshold);
phase.set(next);
fx
};
let mut node = use_signal(|| None::<Rc<MountedData>>);
let mut press_offset = use_signal(Point::default);
let mut mods = use_signal(Modifiers::empty);
let mut attributes = attributes;
let style = merge_style(&mut attributes, "touch-action: none;");
let mut deliver_to = move |target: ZoneId, point: Point, effect: DropEffect| -> bool {
let Some(record) = registry.get(target) else {
return false;
};
let Some(p) = dnd.payload() else {
return false;
};
if !record.accepts_payload(&p) {
return false;
}
let origin = (*record.rect.peek())
.map(|r| r.origin())
.unwrap_or_default();
let mode = dnd.mode();
let grab = dnd.grab();
if let Some((p, from)) = dnd.take() {
record.on_drop.call(DropOutcome {
payload: p,
from,
to: target,
effect,
mode,
client: point,
element: point - origin,
grab,
});
return true;
}
false
};
let mut finish_drop = move |point: Point| {
let effect = effective_effect(effect, *mods.peek());
if let Some(target) = registry.hit_test(point) {
if deliver_to(target, point, effect) {
if let Some(h) = &on_drag_end {
h.call(true);
}
return;
}
}
spawn(async move {
registry.measure_all().await;
let target = dnd
.payload()
.and_then(|p| registry.hit_test_closest(point, &p, 48.0));
let dropped = match target {
Some(t) => deliver_to(t, point, effect),
None => false,
};
if !dropped {
dnd.cancel();
}
if let Some(h) = &on_drag_end {
h.call(dropped);
}
});
};
rsx! {
div {
style: style,
"data-dragging": if dnd.dragging() && dnd.payload().as_ref() == Some(&attr_payload) { "true" },
"data-disabled": if disabled { "true" },
onmounted: move |evt: Event<MountedData>| node.set(Some(evt.data())),
onpointerdown: move |evt: PointerEvent| {
if disabled || !evt.is_primary() {
return;
}
evt.stop_propagation();
if let Some(n) = node.peek().clone() {
platform::capture_pointer(&n, evt.pointer_id());
}
let o = evt.element_coordinates();
press_offset.set(Point::new(o.x, o.y));
let _ = step(
GestureEvent::Down { at: pointer_client(&evt), pointer_id: evt.pointer_id() },
threshold,
);
},
onpointermove: move |evt: PointerEvent| {
let at = pointer_client(&evt);
mods.set(evt.modifiers());
let event = if matches!(*phase.peek(), GesturePhase::Dragging { .. })
&& evt.held_buttons().is_empty()
{
if let Some(n) = node.peek().clone() {
platform::release_pointer(&n, evt.pointer_id());
}
GestureEvent::Up { at, pointer_id: evt.pointer_id() }
} else {
GestureEvent::Move { at, pointer_id: evt.pointer_id() }
};
match step(event, threshold) {
GestureEffect::Begin { at, .. } => {
dnd.start(
pointer_payload.clone(),
zone,
at,
*press_offset.peek(),
effect,
DragMode::Pointer,
);
registry.refresh_rects();
if let Some(h) = &on_drag_start {
h.call(());
}
}
GestureEffect::Track { at } => {
dnd.update_pointer(at);
match registry.hit_test(at) {
Some(z) => dnd.enter(z),
None => {
if let Some(over) = dnd.over() {
dnd.leave(over);
}
}
}
}
GestureEffect::Drop { at: point } => finish_drop(point),
_ => {}
}
},
onpointerup: move |evt: PointerEvent| {
if let Some(n) = node.peek().clone() {
platform::release_pointer(&n, evt.pointer_id());
}
mods.set(evt.modifiers());
let GestureEffect::Drop { at: point } = step(
GestureEvent::Up { at: pointer_client(&evt), pointer_id: evt.pointer_id() },
threshold,
) else {
return;
};
finish_drop(point);
},
onpointercancel: move |evt: PointerEvent| {
if let Some(n) = node.peek().clone() {
platform::release_pointer(&n, evt.pointer_id());
}
if step(GestureEvent::Cancel, threshold) == GestureEffect::Abort {
dnd.cancel();
if let Some(h) = &on_drag_end {
h.call(false);
}
}
},
onlostpointercapture: move |_| {
if step(GestureEvent::Cancel, threshold) == GestureEffect::Abort {
dnd.cancel();
if let Some(h) = &on_drag_end {
h.call(false);
}
}
},
tabindex: if disabled { -1_i64 } else { 0 },
role: "button",
aria_roledescription: "draggable",
onkeydown: move |evt: KeyboardEvent| {
if disabled {
return;
}
let registry = registry;
let key = evt.key();
let is_activate = matches!(key, Key::Enter)
|| matches!(&key, Key::Character(c) if c == " ");
let kb_drag = dnd.dragging() && dnd.mode() == DragMode::Keyboard;
if !dnd.dragging() && is_activate {
evt.prevent_default();
dnd.start(
kb_payload.clone(),
zone,
Point::default(),
Point::default(),
effect,
DragMode::Keyboard,
);
registry.refresh_rects();
let name = kb_label.clone().unwrap_or_else(|| "item".to_string());
dnd.announce(format!(
"Picked up {name}. Use arrow keys to choose a drop target, Enter to drop, Escape to cancel."
));
if let Some(h) = &on_drag_start {
h.call(());
}
return;
}
if !kb_drag {
return;
}
let nav = nav_key(&key, registry.direction());
if let (Some(nav), Some(p)) = (nav, dnd.payload()) {
evt.prevent_default();
let over = dnd.over();
let next = match nav {
NavKey::Next => registry.step_sibling(over, &p, 1),
NavKey::Prev => registry.step_sibling(over, &p, -1),
NavKey::Descend => over
.and_then(|z| registry.first_child(z, &p))
.or_else(|| registry.step_sibling(over, &p, 1)),
NavKey::Ascend => over
.and_then(|z| registry.ascend(z))
.or_else(|| registry.step_sibling(over, &p, -1)),
};
if let Some(next) = next {
dnd.enter(next);
let record = registry.get(next);
let name = record
.as_ref()
.and_then(|z| z.label.clone())
.unwrap_or_else(|| format!("zone {}", next.0));
let inside = record
.as_ref()
.and_then(|z| z.parent)
.and_then(|pid| registry.get(pid))
.and_then(|pz| pz.label);
match inside {
Some(parent) => dnd.announce(format!("Over {name}, inside {parent}.")),
None => dnd.announce(format!("Over {name}.")),
}
} else {
dnd.announce("No drop targets available.");
}
return;
}
if is_activate {
evt.prevent_default();
let target = dnd.over().filter(|z| registry.contains(*z)).or_else(|| {
dnd.payload().and_then(|p| registry.step_zone(None, &p, 1))
});
let Some(target) = target else {
dnd.announce("No drop target selected.");
return;
};
if let Some(record) = registry.get(target) {
if let Some((p, from)) = dnd.take() {
let (client, element) = keyboard_drop_points(*record.rect.peek());
record.on_drop.call(DropOutcome {
payload: p,
from,
to: target,
effect,
mode: DragMode::Keyboard,
client,
element,
grab: Point::default(),
});
let name = record
.label
.unwrap_or_else(|| format!("zone {}", target.0));
dnd.announce(format!("Dropped in {name}."));
if let Some(h) = &on_drag_end {
h.call(true);
}
}
}
return;
}
if matches!(key, Key::Escape) {
evt.prevent_default();
dnd.cancel();
dnd.announce("Drag cancelled.");
if let Some(h) = &on_drag_end {
h.call(false);
}
}
},
..attributes,
{children}
}
}
}
#[component]
pub fn DropZone<T: Clone + PartialEq + 'static>(
#[props(default)]
id: Option<ZoneId>,
#[props(default)]
label: Option<String>,
#[props(default)]
accepts: Option<Callback<T, bool>>,
on_drop: EventHandler<DropOutcome<T>>,
#[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
children: Element,
) -> Element {
let dnd = use_dnd::<T>();
let mut registry = use_zone_registry::<T>();
let auto_id = use_zone_id();
let zone_id = id.unwrap_or(auto_id);
let parent = try_use_context::<ParentZone>().map(|p| p.0);
use_context_provider(|| ParentZone(zone_id));
let mounted = use_signal(|| None::<Rc<MountedData>>);
let rect = use_signal(|| None::<super::types::Rect>);
use_hook(|| {
registry.register(ZoneRecord {
id: zone_id,
parent,
label: label.clone(),
on_drop: Callback::new(move |o| on_drop.call(o)),
accepts,
mounted,
rect,
});
});
use_drop(move || {
registry.unregister(zone_id);
});
registry.sync_label(zone_id, label.clone());
let acceptable = move || -> bool {
match dnd.payload() {
Some(p) => accepts.map(|cb| cb.call(p)).unwrap_or(true),
None => false,
}
};
rsx! {
div {
"data-active": if dnd.dragging() && acceptable() { "true" },
"data-over": if dnd.over() == Some(zone_id) && acceptable() { "true" },
onmounted: move |evt: Event<MountedData>| {
let mut mounted = mounted;
mounted.set(Some(evt.data()));
},
..attributes,
{children}
}
}
}
pub(crate) fn overlay_style(pos: Point) -> String {
format!(
"position: fixed; left: {}px; top: {}px; pointer-events: none; z-index: 9999;",
pos.x, pos.y
)
}
#[component]
pub fn DragOverlay<T: Clone + PartialEq + 'static>(
#[props(default)]
phantom: std::marker::PhantomData<T>,
#[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
children: Element,
) -> Element {
let _ = phantom;
let dnd = use_dnd::<T>();
if !dnd.dragging() {
return rsx! {};
}
let mut attributes = attributes;
let style = merge_style(&mut attributes, &overlay_style(dnd.pointer() - dnd.grab()));
rsx! {
div {
style: style,
..attributes,
{children}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn nav_keys_mirror_under_rtl() {
for dir in [Direction::Ltr, Direction::Rtl] {
assert_eq!(nav_key(&Key::ArrowDown, dir), Some(NavKey::Next));
assert_eq!(nav_key(&Key::ArrowUp, dir), Some(NavKey::Prev));
assert_eq!(nav_key(&Key::Enter, dir), None);
}
assert_eq!(nav_key(&Key::ArrowRight, Direction::Ltr), Some(NavKey::Descend));
assert_eq!(nav_key(&Key::ArrowLeft, Direction::Ltr), Some(NavKey::Ascend));
assert_eq!(nav_key(&Key::ArrowRight, Direction::Rtl), Some(NavKey::Ascend));
assert_eq!(nav_key(&Key::ArrowLeft, Direction::Rtl), Some(NavKey::Descend));
}
#[test]
fn keyboard_drop_points_use_zone_center_and_element_offset() {
let rect = Rect::new(40.0, 80.0, 200.0, 100.0);
let (client, element) = keyboard_drop_points(Some(rect));
assert_eq!(client, Point::new(140.0, 130.0));
assert_eq!(element, Point::new(100.0, 50.0));
}
#[test]
fn keyboard_drop_points_fall_back_to_origin_without_rect() {
let (client, element) = keyboard_drop_points(None);
assert_eq!(client, Point::default());
assert_eq!(element, Point::default());
}
}