use dioxus::html::MountedData;
use dioxus::prelude::*;
use std::rc::Rc;
use super::hooks::{
client_point, element_point, use_dnd, use_dnd_provider, use_zone_id, use_zone_registry,
};
use super::registry::ZoneRecord;
#[derive(Clone, Copy, PartialEq)]
pub struct ParentZone(pub ZoneId);
#[derive(Clone, Copy)]
enum NavKey {
Next,
Prev,
Descend,
Ascend,
}
use super::types::{effective_effect, DragMode, DropEffect, DropOutcome, Point, ZoneId};
#[component]
pub fn DndProvider<T: Clone + PartialEq + 'static>(
#[props(default)]
phantom: std::marker::PhantomData<T>,
children: Element,
) -> Element {
let _ = phantom;
use_dnd_provider::<T>();
rsx! {
{children}
}
}
#[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)]
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 kb_label = label.clone();
rsx! {
div {
draggable: !disabled,
ondragstart: move |evt: DragEvent| {
if disabled {
return;
}
evt.stop_propagation();
let dt = evt.data_transfer();
let _ = dt.set_data("text/plain", "dioxus-dnd");
dt.set_effect_allowed(effect.as_str());
dnd.start(
payload.clone(),
zone,
client_point(&evt),
element_point(&evt),
effect,
DragMode::Pointer,
);
if let Some(h) = &on_drag_start {
h.call(());
}
},
ondrag: move |evt: DragEvent| {
dnd.update_pointer(client_point(&evt));
},
ondragend: move |_| {
let dropped = !dnd.dragging();
dnd.cancel();
if let Some(h) = &on_drag_end {
h.call(dropped);
}
},
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 = match key {
Key::ArrowDown => Some(NavKey::Next),
Key::ArrowUp => Some(NavKey::Prev),
Key::ArrowRight => Some(NavKey::Descend),
Key::ArrowLeft => Some(NavKey::Ascend),
_ => None,
};
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.parent_of(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().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 center = (*record.rect.peek())
.map(|r| r.center())
.unwrap_or_default();
record.on_drop.call(DropOutcome {
payload: p,
from,
to: target,
effect,
client: center,
element: 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(default)]
on_enter: Option<EventHandler<T>>,
#[props(default)]
on_leave: Option<EventHandler<()>>,
#[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
children: Element,
) -> Element {
let mut 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 mut depth = use_signal(|| 0u32);
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 {
onmounted: move |evt: Event<MountedData>| {
let mut mounted = mounted;
mounted.set(Some(evt.data()));
},
ondragover: move |evt: DragEvent| {
if acceptable() {
evt.prevent_default();
let eff = effective_effect(dnd.effect(), evt.modifiers());
evt.data_transfer().set_drop_effect(eff.as_str());
}
},
ondragenter: move |evt: DragEvent| {
if !acceptable() {
return;
}
evt.prevent_default();
let d = depth() + 1;
depth.set(d);
if d == 1 {
dnd.enter(zone_id);
if let (Some(h), Some(p)) = (&on_enter, dnd.payload()) {
h.call(p);
}
}
},
ondragleave: move |_| {
let d = depth().saturating_sub(1);
depth.set(d);
if d == 0 {
dnd.leave(zone_id);
if let Some(h) = &on_leave {
h.call(());
}
}
},
ondrop: move |evt: DragEvent| {
evt.prevent_default();
depth.set(0);
if !acceptable() {
return;
}
let client = client_point(&evt);
let element = element_point(&evt);
let effect = effective_effect(dnd.effect(), evt.modifiers());
if let Some((payload, from)) = dnd.take() {
on_drop.call(DropOutcome {
payload,
from,
to: zone_id,
effect,
client,
element,
});
if let Some(h) = &on_leave {
h.call(());
}
}
},
..attributes,
{children}
}
}
}
#[component]
pub fn DragOverlay<T: Clone + PartialEq + 'static>(
#[props(default)]
phantom: std::marker::PhantomData<T>,
children: Element,
) -> Element {
let _ = phantom;
let dnd = use_dnd::<T>();
if !dnd.dragging() {
return rsx! {};
}
let p = dnd.pointer() - dnd.grab();
rsx! {
div {
style: "position: fixed; left: {p.x}px; top: {p.y}px; pointer-events: none; z-index: 9999;",
{children}
}
}
}