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, SettleFlag};
use super::registry::{ZoneRecord, ZoneRegistry};
use super::state::DndContext;
use super::strings::use_dnd_strings;
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::{
edge_of, effective_effect, Direction, DragMode, DropEffect, DropOutcome, EdgeSet, 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)
}
pub(crate) fn deliver_drop<T: Clone + PartialEq + 'static>(
registry: ZoneRegistry<T>,
dnd: &mut DndContext<T>,
settle_flag: Option<SettleFlag<T>>,
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();
let settle_to = match settle_flag {
Some(f) if mode == DragMode::Pointer && *f.armed.peek() => *record.rect.peek(),
_ => None,
};
let taken = match settle_to {
Some(to) => dnd.take_settling(to),
None => dnd.take(),
};
if let Some((p, from)) = taken {
record.on_drop.call(DropOutcome {
payload: p,
from,
to: target,
effect,
mode,
client: point,
element: point - origin,
grab,
edge: None,
});
return true;
}
false
}
#[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 settle_flag = try_use_context::<SettleFlag<T>>();
let strings = use_dnd_strings();
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 {
deliver_drop(registry, &mut dnd, settle_flag, target, point, effect)
};
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(|| (strings.item)());
dnd.announce((strings.picked_up)(&name));
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(|| (strings.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((strings.over_inside)(&name, &parent)),
None => dnd.announce((strings.over)(&name)),
}
} else {
dnd.announce((strings.no_targets)());
}
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((strings.no_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(),
edge: None,
});
let name = record
.label
.unwrap_or_else(|| (strings.zone)(target.0));
dnd.announce((strings.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((strings.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>>,
#[props(default)]
edge: Option<EdgeSet>,
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 |mut o: DropOutcome<T>| {
if let Some(set) = edge {
if o.mode == DragMode::Pointer {
if let Some(r) = *rect.peek() {
o.edge = Some(edge_of(o.client, r, set));
}
}
}
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,
}
};
let live_edge = move || -> Option<&'static str> {
let set = edge?;
if dnd.over() != Some(zone_id) || dnd.mode() != DragMode::Pointer || !acceptable() {
return None;
}
let r = (*rect.peek())?;
Some(edge_of(dnd.pointer(), r, set).as_str())
};
rsx! {
div {
"data-active": if dnd.dragging() && acceptable() { "true" },
"data-over": if dnd.over() == Some(zone_id) && acceptable() { "true" },
"data-edge": live_edge(),
onmounted: move |evt: Event<MountedData>| {
let m: Rc<MountedData> = evt.data();
let mut mounted = mounted;
let mut rect = rect;
mounted.set(Some(m.clone()));
spawn(async move {
if let Ok(r) = m.get_client_rect().await {
rect.set(Some(Rect::new(
r.origin.x,
r.origin.y,
r.size.width,
r.size.height,
)));
}
});
},
..attributes,
{children}
}
}
}
#[component]
pub fn BridgeDropZone<A: Clone + PartialEq + 'static, B: Clone + PartialEq + 'static>(
#[props(default)]
id: Option<ZoneId>,
#[props(default)]
label: Option<String>,
#[props(default)]
accepts_a: Option<Callback<A, bool>>,
#[props(default)]
accepts_b: Option<Callback<B, bool>>,
on_drop_a: EventHandler<DropOutcome<A>>,
on_drop_b: EventHandler<DropOutcome<B>>,
#[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
children: Element,
) -> Element {
let dnd_a = use_dnd::<A>();
let dnd_b = use_dnd::<B>();
let mut reg_a = use_zone_registry::<A>();
let mut reg_b = use_zone_registry::<B>();
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(|| {
reg_a.register(ZoneRecord {
id: zone_id,
parent,
label: label.clone(),
on_drop: Callback::new(move |o| on_drop_a.call(o)),
accepts: accepts_a,
mounted,
rect,
});
reg_b.register(ZoneRecord {
id: zone_id,
parent,
label: label.clone(),
on_drop: Callback::new(move |o| on_drop_b.call(o)),
accepts: accepts_b,
mounted,
rect,
});
});
use_drop(move || {
reg_a.unregister(zone_id);
reg_b.unregister(zone_id);
});
reg_a.sync_label(zone_id, label.clone());
reg_b.sync_label(zone_id, label);
let acceptable_a = move || -> bool {
match dnd_a.payload() {
Some(p) => accepts_a.map(|cb| cb.call(p)).unwrap_or(true),
None => false,
}
};
let acceptable_b = move || -> bool {
match dnd_b.payload() {
Some(p) => accepts_b.map(|cb| cb.call(p)).unwrap_or(true),
None => false,
}
};
rsx! {
div {
"data-active": if (dnd_a.dragging() && acceptable_a()) || (dnd_b.dragging() && acceptable_b()) { "true" },
"data-over": if (dnd_a.over() == Some(zone_id) && acceptable_a())
|| (dnd_b.over() == Some(zone_id) && acceptable_b()) { "true" },
onmounted: move |evt: Event<MountedData>| {
let m: Rc<MountedData> = evt.data();
let mut mounted = mounted;
let mut rect = rect;
mounted.set(Some(m.clone()));
spawn(async move {
if let Ok(r) = m.get_client_rect().await {
rect.set(Some(Rect::new(
r.origin.x,
r.origin.y,
r.size.width,
r.size.height,
)));
}
});
},
..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(default)]
settle: bool,
#[props(default = 200.0)]
duration: f64,
#[props(default = "ease".to_string())]
easing: String,
#[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
children: Element,
) -> Element {
let _ = phantom;
let mut dnd = use_dnd::<T>();
let flag = try_use_context::<SettleFlag<T>>();
use_hook(move || {
if settle {
if let Some(mut f) = flag {
f.armed.set(true);
}
}
});
use_drop(move || {
if settle {
if let Some(mut f) = flag {
f.armed.set(false);
}
dnd.finish_settle();
}
});
let mut node = use_signal(|| None::<Rc<MountedData>>);
let mut glide = use_signal(|| None::<Point>);
let reduced_motion_css = crate::a11y::use_reduced_motion_css_if(settle);
use_effect(move || {
match dnd.settling() {
Some(to) if settle => {
if glide.peek().is_some() {
return;
}
let Some(m) = node.peek().clone() else {
dnd.finish_settle();
return;
};
spawn(async move {
let Ok(r) = m.get_client_rect().await else {
dnd.finish_settle();
return;
};
let from = Rect::new(r.origin.x, r.origin.y, r.size.width, r.size.height);
let d = to.center() - from.center();
if d.x.abs() < 1.0 && d.y.abs() < 1.0 {
dnd.finish_settle();
} else {
glide.set(Some(d));
}
});
}
_ => {
if glide.peek().is_some() {
glide.set(None);
}
}
}
});
let settling = settle && dnd.settling().is_some();
if !dnd.dragging() && !settling {
return rsx! {};
}
let functional = if settling {
let transform = match glide() {
Some(d) => format!("translate({}px, {}px)", d.x, d.y),
None => "none".to_string(),
};
format!(
"{} transform: {transform}; transition: transform {duration}ms {easing};",
overlay_style(dnd.pointer() - dnd.grab()),
)
} else {
overlay_style(dnd.pointer() - dnd.grab())
};
let mut attributes = attributes;
let style = merge_style(&mut attributes, &functional);
rsx! {
{reduced_motion_css}
div {
style: style,
"data-dnd-motion": if settle { "true" },
onmounted: move |evt: Event<MountedData>| node.set(Some(evt.data())),
ontransitionend: move |_| {
if settling && glide.peek().is_some() {
dnd.finish_settle();
}
},
..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());
}
}