1use dioxus::html::MountedData;
16use dioxus::prelude::*;
17
18use std::rc::Rc;
19
20use super::hooks::{use_dnd, use_dnd_provider, use_zone_id, use_zone_registry};
21use super::registry::ZoneRecord;
22use super::{platform, transition, GestureEffect, GestureEvent, GesturePhase};
23
24#[derive(Clone, Copy, PartialEq)]
28pub struct ParentZone(pub ZoneId);
29
30#[derive(Debug, Clone, Copy, PartialEq)]
32enum NavKey {
33 Next,
34 Prev,
35 Descend,
36 Ascend,
37}
38use super::types::{
39 effective_effect, Direction, DragMode, DropEffect, DropOutcome, Point, Rect, ZoneId,
40};
41
42fn nav_key(key: &Key, dir: Direction) -> Option<NavKey> {
47 Some(match (key, dir) {
48 (Key::ArrowDown, _) => NavKey::Next,
49 (Key::ArrowUp, _) => NavKey::Prev,
50 (Key::ArrowRight, Direction::Ltr) | (Key::ArrowLeft, Direction::Rtl) => NavKey::Descend,
51 (Key::ArrowLeft, Direction::Ltr) | (Key::ArrowRight, Direction::Rtl) => NavKey::Ascend,
52 _ => return None,
53 })
54}
55
56pub(crate) fn merge_style(attributes: &mut Vec<Attribute>, functional: &str) -> String {
63 let user = attributes
64 .iter()
65 .position(|a| a.name == "style")
66 .map(|i| attributes.remove(i));
67 match user.map(|a| a.value) {
68 Some(dioxus::core::AttributeValue::Text(s)) => format!("{functional} {s}"),
69 _ => functional.to_string(),
70 }
71}
72
73fn keyboard_drop_points(rect: Option<Rect>) -> (Point, Point) {
74 match rect {
75 Some(r) => {
76 let client = r.center();
77 (client, client - r.origin())
78 }
79 None => (Point::default(), Point::default()),
80 }
81}
82
83#[component]
85pub fn DndProvider<T: Clone + PartialEq + 'static>(
86 #[props(default)]
88 phantom: std::marker::PhantomData<T>,
89 #[props(default)]
92 dir: Direction,
93 children: Element,
94) -> Element {
95 let _ = phantom;
96 use_dnd_provider::<T>();
97 use_zone_registry::<T>().set_direction(dir);
100 rsx! {
101 {children}
102 }
103}
104
105fn pointer_client(evt: &PointerEvent) -> Point {
106 let c = evt.client_coordinates();
107 Point::new(c.x, c.y)
108}
109
110#[component]
120pub fn Draggable<T: Clone + PartialEq + 'static>(
121 payload: T,
123 #[props(default)]
125 zone: Option<ZoneId>,
126 #[props(default)]
128 effect: DropEffect,
129 #[props(default)]
131 disabled: bool,
132 #[props(default = 8.0)]
134 threshold: f64,
135 #[props(default)]
137 label: Option<String>,
138 #[props(default)]
140 on_drag_start: Option<EventHandler<()>>,
141 #[props(default)]
144 on_drag_end: Option<EventHandler<bool>>,
145 #[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
146 children: Element,
147) -> Element {
148 let mut dnd = use_dnd::<T>();
149 let registry = use_zone_registry::<T>();
150 let kb_payload = payload.clone();
152 let pointer_payload = payload.clone();
153 let kb_label = label.clone();
154 let attr_payload = payload.clone();
157 let mut phase = use_signal(|| GesturePhase::Idle);
158 let mut step = move |event: GestureEvent, threshold: f64| -> GestureEffect {
159 let (next, fx) = transition(*phase.peek(), event, threshold);
160 phase.set(next);
161 fx
162 };
163 let mut node = use_signal(|| None::<Rc<MountedData>>);
164 let mut press_offset = use_signal(Point::default);
165 let mut mods = use_signal(Modifiers::empty);
166 let mut attributes = attributes;
167 let style = merge_style(&mut attributes, "touch-action: none;");
168
169 let mut deliver_to = move |target: ZoneId, point: Point, effect: DropEffect| -> bool {
170 let Some(record) = registry.get(target) else {
171 return false;
172 };
173 let Some(p) = dnd.payload() else {
174 return false;
175 };
176 if !record.accepts_payload(&p) {
177 return false;
178 }
179 let origin = (*record.rect.peek())
180 .map(|r| r.origin())
181 .unwrap_or_default();
182 let mode = dnd.mode();
183 let grab = dnd.grab();
184 if let Some((p, from)) = dnd.take() {
185 record.on_drop.call(DropOutcome {
186 payload: p,
187 from,
188 to: target,
189 effect,
190 mode,
191 client: point,
192 element: point - origin,
193 grab,
194 });
195 return true;
196 }
197 false
198 };
199
200 let mut finish_drop = move |point: Point| {
201 let effect = effective_effect(effect, *mods.peek());
202 if let Some(target) = registry.hit_test(point) {
203 if deliver_to(target, point, effect) {
204 if let Some(h) = &on_drag_end {
205 h.call(true);
206 }
207 return;
208 }
209 }
210 spawn(async move {
211 registry.measure_all().await;
212 let target = dnd
213 .payload()
214 .and_then(|p| registry.hit_test_closest(point, &p, 48.0));
215 let dropped = match target {
216 Some(t) => deliver_to(t, point, effect),
217 None => false,
218 };
219 if !dropped {
220 dnd.cancel();
221 }
222 if let Some(h) = &on_drag_end {
223 h.call(dropped);
224 }
225 });
226 };
227
228 rsx! {
229 div {
230 style: style,
231 "data-dragging": if dnd.dragging() && dnd.payload().as_ref() == Some(&attr_payload) { "true" },
232 "data-disabled": if disabled { "true" },
233 onmounted: move |evt: Event<MountedData>| node.set(Some(evt.data())),
234 onpointerdown: move |evt: PointerEvent| {
235 if disabled || !evt.is_primary() {
236 return;
237 }
238 evt.stop_propagation();
239 if let Some(n) = node.peek().clone() {
240 platform::capture_pointer(&n, evt.pointer_id());
241 }
242 let o = evt.element_coordinates();
243 press_offset.set(Point::new(o.x, o.y));
244 let _ = step(
245 GestureEvent::Down { at: pointer_client(&evt), pointer_id: evt.pointer_id() },
246 threshold,
247 );
248 },
249 onpointermove: move |evt: PointerEvent| {
250 let at = pointer_client(&evt);
251 mods.set(evt.modifiers());
252 let event = if matches!(*phase.peek(), GesturePhase::Dragging { .. })
253 && evt.held_buttons().is_empty()
254 {
255 if let Some(n) = node.peek().clone() {
256 platform::release_pointer(&n, evt.pointer_id());
257 }
258 GestureEvent::Up { at, pointer_id: evt.pointer_id() }
259 } else {
260 GestureEvent::Move { at, pointer_id: evt.pointer_id() }
261 };
262 match step(event, threshold) {
263 GestureEffect::Begin { at, .. } => {
264 dnd.start(
265 pointer_payload.clone(),
266 zone,
267 at,
268 *press_offset.peek(),
269 effect,
270 DragMode::Pointer,
271 );
272 registry.refresh_rects();
273 if let Some(h) = &on_drag_start {
274 h.call(());
275 }
276 }
277 GestureEffect::Track { at } => {
278 dnd.update_pointer(at);
279 match registry.hit_test(at) {
280 Some(z) => dnd.enter(z),
281 None => {
282 if let Some(over) = dnd.over() {
283 dnd.leave(over);
284 }
285 }
286 }
287 }
288 GestureEffect::Drop { at: point } => finish_drop(point),
289 _ => {}
290 }
291 },
292 onpointerup: move |evt: PointerEvent| {
293 if let Some(n) = node.peek().clone() {
294 platform::release_pointer(&n, evt.pointer_id());
295 }
296 mods.set(evt.modifiers());
297 let GestureEffect::Drop { at: point } = step(
298 GestureEvent::Up { at: pointer_client(&evt), pointer_id: evt.pointer_id() },
299 threshold,
300 ) else {
301 return;
302 };
303 finish_drop(point);
304 },
305 onpointercancel: move |evt: PointerEvent| {
306 if let Some(n) = node.peek().clone() {
307 platform::release_pointer(&n, evt.pointer_id());
308 }
309 if step(GestureEvent::Cancel, threshold) == GestureEffect::Abort {
310 dnd.cancel();
311 if let Some(h) = &on_drag_end {
312 h.call(false);
313 }
314 }
315 },
316 onlostpointercapture: move |_| {
317 if step(GestureEvent::Cancel, threshold) == GestureEffect::Abort {
318 dnd.cancel();
319 if let Some(h) = &on_drag_end {
320 h.call(false);
321 }
322 }
323 },
324 tabindex: if disabled { -1_i64 } else { 0 },
329 role: "button",
330 aria_roledescription: "draggable",
331 onkeydown: move |evt: KeyboardEvent| {
332 if disabled {
333 return;
334 }
335 let registry = registry;
336 let key = evt.key();
337 let is_activate = matches!(key, Key::Enter)
338 || matches!(&key, Key::Character(c) if c == " ");
339 let kb_drag = dnd.dragging() && dnd.mode() == DragMode::Keyboard;
340
341 if !dnd.dragging() && is_activate {
342 evt.prevent_default();
343 dnd.start(
344 kb_payload.clone(),
345 zone,
346 Point::default(),
347 Point::default(),
348 effect,
349 DragMode::Keyboard,
350 );
351 registry.refresh_rects();
354 let name = kb_label.clone().unwrap_or_else(|| "item".to_string());
355 dnd.announce(format!(
356 "Picked up {name}. Use arrow keys to choose a drop target, Enter to drop, Escape to cancel."
357 ));
358 if let Some(h) = &on_drag_start {
359 h.call(());
360 }
361 return;
362 }
363
364 if !kb_drag {
365 return;
366 }
367
368 let nav = nav_key(&key, registry.direction());
375 if let (Some(nav), Some(p)) = (nav, dnd.payload()) {
376 evt.prevent_default();
377 let over = dnd.over();
378 let next = match nav {
379 NavKey::Next => registry.step_sibling(over, &p, 1),
380 NavKey::Prev => registry.step_sibling(over, &p, -1),
381 NavKey::Descend => over
382 .and_then(|z| registry.first_child(z, &p))
383 .or_else(|| registry.step_sibling(over, &p, 1)),
384 NavKey::Ascend => over
385 .and_then(|z| registry.ascend(z))
386 .or_else(|| registry.step_sibling(over, &p, -1)),
387 };
388 if let Some(next) = next {
389 dnd.enter(next);
390 let record = registry.get(next);
391 let name = record
392 .as_ref()
393 .and_then(|z| z.label.clone())
394 .unwrap_or_else(|| format!("zone {}", next.0));
395 let inside = record
396 .as_ref()
397 .and_then(|z| z.parent)
398 .and_then(|pid| registry.get(pid))
399 .and_then(|pz| pz.label);
400 match inside {
401 Some(parent) => dnd.announce(format!("Over {name}, inside {parent}.")),
402 None => dnd.announce(format!("Over {name}.")),
403 }
404 } else {
405 dnd.announce("No drop targets available.");
406 }
407 return;
408 }
409
410 if is_activate {
411 evt.prevent_default();
412 let target = dnd.over().filter(|z| registry.contains(*z)).or_else(|| {
415 dnd.payload().and_then(|p| registry.step_zone(None, &p, 1))
416 });
417 let Some(target) = target else {
418 dnd.announce("No drop target selected.");
419 return;
420 };
421 if let Some(record) = registry.get(target) {
422 if let Some((p, from)) = dnd.take() {
423 let (client, element) = keyboard_drop_points(*record.rect.peek());
424 record.on_drop.call(DropOutcome {
425 payload: p,
426 from,
427 to: target,
428 effect,
429 mode: DragMode::Keyboard,
430 client,
431 element,
432 grab: Point::default(),
433 });
434 let name = record
435 .label
436 .unwrap_or_else(|| format!("zone {}", target.0));
437 dnd.announce(format!("Dropped in {name}."));
438 if let Some(h) = &on_drag_end {
439 h.call(true);
440 }
441 }
442 }
443 return;
444 }
445
446 if matches!(key, Key::Escape) {
447 evt.prevent_default();
448 dnd.cancel();
449 dnd.announce("Drag cancelled.");
450 if let Some(h) = &on_drag_end {
451 h.call(false);
452 }
453 }
454 },
455 ..attributes,
456 {children}
457 }
458 }
459}
460
461#[component]
475pub fn DropZone<T: Clone + PartialEq + 'static>(
476 #[props(default)]
478 id: Option<ZoneId>,
479 #[props(default)]
481 label: Option<String>,
482 #[props(default)]
484 accepts: Option<Callback<T, bool>>,
485 on_drop: EventHandler<DropOutcome<T>>,
487 #[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
488 children: Element,
489) -> Element {
490 let dnd = use_dnd::<T>();
491 let mut registry = use_zone_registry::<T>();
492 let auto_id = use_zone_id();
493 let zone_id = id.unwrap_or(auto_id);
494 let parent = try_use_context::<ParentZone>().map(|p| p.0);
497 use_context_provider(|| ParentZone(zone_id));
498 let mounted = use_signal(|| None::<Rc<MountedData>>);
499 let rect = use_signal(|| None::<super::types::Rect>);
500
501 use_hook(|| {
505 registry.register(ZoneRecord {
506 id: zone_id,
507 parent,
508 label: label.clone(),
509 on_drop: Callback::new(move |o| on_drop.call(o)),
510 accepts,
511 mounted,
512 rect,
513 });
514 });
515 use_drop(move || {
516 registry.unregister(zone_id);
517 });
518 registry.sync_label(zone_id, label.clone());
521
522 let acceptable = move || -> bool {
523 match dnd.payload() {
524 Some(p) => accepts.map(|cb| cb.call(p)).unwrap_or(true),
525 None => false,
526 }
527 };
528
529 rsx! {
530 div {
531 "data-active": if dnd.dragging() && acceptable() { "true" },
532 "data-over": if dnd.over() == Some(zone_id) && acceptable() { "true" },
533 onmounted: move |evt: Event<MountedData>| {
534 let mut mounted = mounted;
535 mounted.set(Some(evt.data()));
536 },
537 ..attributes,
538 {children}
539 }
540 }
541}
542
543pub(crate) fn overlay_style(pos: Point) -> String {
548 format!(
549 "position: fixed; left: {}px; top: {}px; pointer-events: none; z-index: 9999;",
550 pos.x, pos.y
551 )
552}
553
554#[component]
567pub fn DragOverlay<T: Clone + PartialEq + 'static>(
568 #[props(default)]
570 phantom: std::marker::PhantomData<T>,
571 #[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
572 children: Element,
573) -> Element {
574 let _ = phantom;
575 let dnd = use_dnd::<T>();
576 if !dnd.dragging() {
577 return rsx! {};
578 }
579 let mut attributes = attributes;
580 let style = merge_style(&mut attributes, &overlay_style(dnd.pointer() - dnd.grab()));
581 rsx! {
582 div {
583 style: style,
584 ..attributes,
585 {children}
586 }
587 }
588}
589
590#[cfg(test)]
591mod tests {
592 use super::*;
593
594 #[test]
597 fn nav_keys_mirror_under_rtl() {
598 for dir in [Direction::Ltr, Direction::Rtl] {
599 assert_eq!(nav_key(&Key::ArrowDown, dir), Some(NavKey::Next));
600 assert_eq!(nav_key(&Key::ArrowUp, dir), Some(NavKey::Prev));
601 assert_eq!(nav_key(&Key::Enter, dir), None);
602 }
603 assert_eq!(nav_key(&Key::ArrowRight, Direction::Ltr), Some(NavKey::Descend));
604 assert_eq!(nav_key(&Key::ArrowLeft, Direction::Ltr), Some(NavKey::Ascend));
605 assert_eq!(nav_key(&Key::ArrowRight, Direction::Rtl), Some(NavKey::Ascend));
606 assert_eq!(nav_key(&Key::ArrowLeft, Direction::Rtl), Some(NavKey::Descend));
607 }
608
609 #[test]
610 fn keyboard_drop_points_use_zone_center_and_element_offset() {
611 let rect = Rect::new(40.0, 80.0, 200.0, 100.0);
612 let (client, element) = keyboard_drop_points(Some(rect));
613
614 assert_eq!(client, Point::new(140.0, 130.0));
615 assert_eq!(element, Point::new(100.0, 50.0));
616 }
617
618 #[test]
619 fn keyboard_drop_points_fall_back_to_origin_without_rect() {
620 let (client, element) = keyboard_drop_points(None);
621
622 assert_eq!(client, Point::default());
623 assert_eq!(element, Point::default());
624 }
625}