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(Clone, Copy)]
32enum NavKey {
33 Next,
34 Prev,
35 Descend,
36 Ascend,
37}
38use super::types::{effective_effect, DragMode, DropEffect, DropOutcome, Point, Rect, ZoneId};
39
40pub(crate) fn merge_style(attributes: &mut Vec<Attribute>, functional: &str) -> String {
47 let user = attributes
48 .iter()
49 .position(|a| a.name == "style")
50 .map(|i| attributes.remove(i));
51 match user.map(|a| a.value) {
52 Some(dioxus::core::AttributeValue::Text(s)) => format!("{functional} {s}"),
53 _ => functional.to_string(),
54 }
55}
56
57fn keyboard_drop_points(rect: Option<Rect>) -> (Point, Point) {
58 match rect {
59 Some(r) => {
60 let client = r.center();
61 (client, client - r.origin())
62 }
63 None => (Point::default(), Point::default()),
64 }
65}
66
67#[component]
69pub fn DndProvider<T: Clone + PartialEq + 'static>(
70 #[props(default)]
72 phantom: std::marker::PhantomData<T>,
73 children: Element,
74) -> Element {
75 let _ = phantom;
76 use_dnd_provider::<T>();
77 rsx! {
78 {children}
79 }
80}
81
82fn pointer_client(evt: &PointerEvent) -> Point {
83 let c = evt.client_coordinates();
84 Point::new(c.x, c.y)
85}
86
87#[component]
97pub fn Draggable<T: Clone + PartialEq + 'static>(
98 payload: T,
100 #[props(default)]
102 zone: Option<ZoneId>,
103 #[props(default)]
105 effect: DropEffect,
106 #[props(default)]
108 disabled: bool,
109 #[props(default = 8.0)]
111 threshold: f64,
112 #[props(default)]
114 label: Option<String>,
115 #[props(default)]
117 on_drag_start: Option<EventHandler<()>>,
118 #[props(default)]
121 on_drag_end: Option<EventHandler<bool>>,
122 #[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
123 children: Element,
124) -> Element {
125 let mut dnd = use_dnd::<T>();
126 let registry = use_zone_registry::<T>();
127 let kb_payload = payload.clone();
129 let pointer_payload = payload.clone();
130 let kb_label = label.clone();
131 let attr_payload = payload.clone();
134 let mut phase = use_signal(|| GesturePhase::Idle);
135 let mut step = move |event: GestureEvent, threshold: f64| -> GestureEffect {
136 let (next, fx) = transition(*phase.peek(), event, threshold);
137 phase.set(next);
138 fx
139 };
140 let mut node = use_signal(|| None::<Rc<MountedData>>);
141 let mut press_offset = use_signal(Point::default);
142 let mut mods = use_signal(Modifiers::empty);
143 let mut attributes = attributes;
144 let style = merge_style(&mut attributes, "touch-action: none;");
145
146 let mut deliver_to = move |target: ZoneId, point: Point, effect: DropEffect| -> bool {
147 let Some(record) = registry.get(target) else {
148 return false;
149 };
150 let Some(p) = dnd.payload() else {
151 return false;
152 };
153 if !record.accepts_payload(&p) {
154 return false;
155 }
156 let origin = (*record.rect.peek())
157 .map(|r| r.origin())
158 .unwrap_or_default();
159 let mode = dnd.mode();
160 let grab = dnd.grab();
161 if let Some((p, from)) = dnd.take() {
162 record.on_drop.call(DropOutcome {
163 payload: p,
164 from,
165 to: target,
166 effect,
167 mode,
168 client: point,
169 element: point - origin,
170 grab,
171 });
172 return true;
173 }
174 false
175 };
176
177 let mut finish_drop = move |point: Point| {
178 let effect = effective_effect(effect, *mods.peek());
179 if let Some(target) = registry.hit_test(point) {
180 if deliver_to(target, point, effect) {
181 if let Some(h) = &on_drag_end {
182 h.call(true);
183 }
184 return;
185 }
186 }
187 spawn(async move {
188 registry.measure_all().await;
189 let target = dnd
190 .payload()
191 .and_then(|p| registry.hit_test_closest(point, &p, 48.0));
192 let dropped = match target {
193 Some(t) => deliver_to(t, point, effect),
194 None => false,
195 };
196 if !dropped {
197 dnd.cancel();
198 }
199 if let Some(h) = &on_drag_end {
200 h.call(dropped);
201 }
202 });
203 };
204
205 rsx! {
206 div {
207 style: style,
208 "data-dragging": if dnd.dragging() && dnd.payload().as_ref() == Some(&attr_payload) { "true" },
209 "data-disabled": if disabled { "true" },
210 onmounted: move |evt: Event<MountedData>| node.set(Some(evt.data())),
211 onpointerdown: move |evt: PointerEvent| {
212 if disabled || !evt.is_primary() {
213 return;
214 }
215 evt.stop_propagation();
216 if let Some(n) = node.peek().clone() {
217 platform::capture_pointer(&n, evt.pointer_id());
218 }
219 let o = evt.element_coordinates();
220 press_offset.set(Point::new(o.x, o.y));
221 let _ = step(
222 GestureEvent::Down { at: pointer_client(&evt), pointer_id: evt.pointer_id() },
223 threshold,
224 );
225 },
226 onpointermove: move |evt: PointerEvent| {
227 let at = pointer_client(&evt);
228 mods.set(evt.modifiers());
229 let event = if matches!(*phase.peek(), GesturePhase::Dragging { .. })
230 && evt.held_buttons().is_empty()
231 {
232 if let Some(n) = node.peek().clone() {
233 platform::release_pointer(&n, evt.pointer_id());
234 }
235 GestureEvent::Up { at, pointer_id: evt.pointer_id() }
236 } else {
237 GestureEvent::Move { at, pointer_id: evt.pointer_id() }
238 };
239 match step(event, threshold) {
240 GestureEffect::Begin { at, .. } => {
241 dnd.start(
242 pointer_payload.clone(),
243 zone,
244 at,
245 *press_offset.peek(),
246 effect,
247 DragMode::Pointer,
248 );
249 registry.refresh_rects();
250 if let Some(h) = &on_drag_start {
251 h.call(());
252 }
253 }
254 GestureEffect::Track { at } => {
255 dnd.update_pointer(at);
256 match registry.hit_test(at) {
257 Some(z) => dnd.enter(z),
258 None => {
259 if let Some(over) = dnd.over() {
260 dnd.leave(over);
261 }
262 }
263 }
264 }
265 GestureEffect::Drop { at: point } => finish_drop(point),
266 _ => {}
267 }
268 },
269 onpointerup: move |evt: PointerEvent| {
270 if let Some(n) = node.peek().clone() {
271 platform::release_pointer(&n, evt.pointer_id());
272 }
273 mods.set(evt.modifiers());
274 let GestureEffect::Drop { at: point } = step(
275 GestureEvent::Up { at: pointer_client(&evt), pointer_id: evt.pointer_id() },
276 threshold,
277 ) else {
278 return;
279 };
280 finish_drop(point);
281 },
282 onpointercancel: move |evt: PointerEvent| {
283 if let Some(n) = node.peek().clone() {
284 platform::release_pointer(&n, evt.pointer_id());
285 }
286 if step(GestureEvent::Cancel, threshold) == GestureEffect::Abort {
287 dnd.cancel();
288 if let Some(h) = &on_drag_end {
289 h.call(false);
290 }
291 }
292 },
293 onlostpointercapture: move |_| {
294 if step(GestureEvent::Cancel, threshold) == GestureEffect::Abort {
295 dnd.cancel();
296 if let Some(h) = &on_drag_end {
297 h.call(false);
298 }
299 }
300 },
301 tabindex: if disabled { -1_i64 } else { 0 },
306 role: "button",
307 aria_roledescription: "draggable",
308 onkeydown: move |evt: KeyboardEvent| {
309 if disabled {
310 return;
311 }
312 let registry = registry;
313 let key = evt.key();
314 let is_activate = matches!(key, Key::Enter)
315 || matches!(&key, Key::Character(c) if c == " ");
316 let kb_drag = dnd.dragging() && dnd.mode() == DragMode::Keyboard;
317
318 if !dnd.dragging() && is_activate {
319 evt.prevent_default();
320 dnd.start(
321 kb_payload.clone(),
322 zone,
323 Point::default(),
324 Point::default(),
325 effect,
326 DragMode::Keyboard,
327 );
328 registry.refresh_rects();
331 let name = kb_label.clone().unwrap_or_else(|| "item".to_string());
332 dnd.announce(format!(
333 "Picked up {name}. Use arrow keys to choose a drop target, Enter to drop, Escape to cancel."
334 ));
335 if let Some(h) = &on_drag_start {
336 h.call(());
337 }
338 return;
339 }
340
341 if !kb_drag {
342 return;
343 }
344
345 let nav = match key {
351 Key::ArrowDown => Some(NavKey::Next),
352 Key::ArrowUp => Some(NavKey::Prev),
353 Key::ArrowRight => Some(NavKey::Descend),
354 Key::ArrowLeft => Some(NavKey::Ascend),
355 _ => None,
356 };
357 if let (Some(nav), Some(p)) = (nav, dnd.payload()) {
358 evt.prevent_default();
359 let over = dnd.over();
360 let next = match nav {
361 NavKey::Next => registry.step_sibling(over, &p, 1),
362 NavKey::Prev => registry.step_sibling(over, &p, -1),
363 NavKey::Descend => over
364 .and_then(|z| registry.first_child(z, &p))
365 .or_else(|| registry.step_sibling(over, &p, 1)),
366 NavKey::Ascend => over
367 .and_then(|z| registry.ascend(z))
368 .or_else(|| registry.step_sibling(over, &p, -1)),
369 };
370 if let Some(next) = next {
371 dnd.enter(next);
372 let record = registry.get(next);
373 let name = record
374 .as_ref()
375 .and_then(|z| z.label.clone())
376 .unwrap_or_else(|| format!("zone {}", next.0));
377 let inside = record
378 .as_ref()
379 .and_then(|z| z.parent)
380 .and_then(|pid| registry.get(pid))
381 .and_then(|pz| pz.label);
382 match inside {
383 Some(parent) => dnd.announce(format!("Over {name}, inside {parent}.")),
384 None => dnd.announce(format!("Over {name}.")),
385 }
386 } else {
387 dnd.announce("No drop targets available.");
388 }
389 return;
390 }
391
392 if is_activate {
393 evt.prevent_default();
394 let target = dnd.over().filter(|z| registry.contains(*z)).or_else(|| {
397 dnd.payload().and_then(|p| registry.step_zone(None, &p, 1))
398 });
399 let Some(target) = target else {
400 dnd.announce("No drop target selected.");
401 return;
402 };
403 if let Some(record) = registry.get(target) {
404 if let Some((p, from)) = dnd.take() {
405 let (client, element) = keyboard_drop_points(*record.rect.peek());
406 record.on_drop.call(DropOutcome {
407 payload: p,
408 from,
409 to: target,
410 effect,
411 mode: DragMode::Keyboard,
412 client,
413 element,
414 grab: Point::default(),
415 });
416 let name = record
417 .label
418 .unwrap_or_else(|| format!("zone {}", target.0));
419 dnd.announce(format!("Dropped in {name}."));
420 if let Some(h) = &on_drag_end {
421 h.call(true);
422 }
423 }
424 }
425 return;
426 }
427
428 if matches!(key, Key::Escape) {
429 evt.prevent_default();
430 dnd.cancel();
431 dnd.announce("Drag cancelled.");
432 if let Some(h) = &on_drag_end {
433 h.call(false);
434 }
435 }
436 },
437 ..attributes,
438 {children}
439 }
440 }
441}
442
443#[component]
457pub fn DropZone<T: Clone + PartialEq + 'static>(
458 #[props(default)]
460 id: Option<ZoneId>,
461 #[props(default)]
463 label: Option<String>,
464 #[props(default)]
466 accepts: Option<Callback<T, bool>>,
467 on_drop: EventHandler<DropOutcome<T>>,
469 #[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
470 children: Element,
471) -> Element {
472 let dnd = use_dnd::<T>();
473 let mut registry = use_zone_registry::<T>();
474 let auto_id = use_zone_id();
475 let zone_id = id.unwrap_or(auto_id);
476 let parent = try_use_context::<ParentZone>().map(|p| p.0);
479 use_context_provider(|| ParentZone(zone_id));
480 let mounted = use_signal(|| None::<Rc<MountedData>>);
481 let rect = use_signal(|| None::<super::types::Rect>);
482
483 use_hook(|| {
487 registry.register(ZoneRecord {
488 id: zone_id,
489 parent,
490 label: label.clone(),
491 on_drop: Callback::new(move |o| on_drop.call(o)),
492 accepts,
493 mounted,
494 rect,
495 });
496 });
497 use_drop(move || {
498 registry.unregister(zone_id);
499 });
500 registry.sync_label(zone_id, label.clone());
503
504 let acceptable = move || -> bool {
505 match dnd.payload() {
506 Some(p) => accepts.map(|cb| cb.call(p)).unwrap_or(true),
507 None => false,
508 }
509 };
510
511 rsx! {
512 div {
513 "data-active": if dnd.dragging() && acceptable() { "true" },
514 "data-over": if dnd.over() == Some(zone_id) && acceptable() { "true" },
515 onmounted: move |evt: Event<MountedData>| {
516 let mut mounted = mounted;
517 mounted.set(Some(evt.data()));
518 },
519 ..attributes,
520 {children}
521 }
522 }
523}
524
525pub(crate) fn overlay_style(pos: Point) -> String {
530 format!(
531 "position: fixed; left: {}px; top: {}px; pointer-events: none; z-index: 9999;",
532 pos.x, pos.y
533 )
534}
535
536#[component]
549pub fn DragOverlay<T: Clone + PartialEq + 'static>(
550 #[props(default)]
552 phantom: std::marker::PhantomData<T>,
553 #[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
554 children: Element,
555) -> Element {
556 let _ = phantom;
557 let dnd = use_dnd::<T>();
558 if !dnd.dragging() {
559 return rsx! {};
560 }
561 let mut attributes = attributes;
562 let style = merge_style(&mut attributes, &overlay_style(dnd.pointer() - dnd.grab()));
563 rsx! {
564 div {
565 style: style,
566 ..attributes,
567 {children}
568 }
569 }
570}
571
572#[cfg(test)]
573mod tests {
574 use super::*;
575
576 #[test]
577 fn keyboard_drop_points_use_zone_center_and_element_offset() {
578 let rect = Rect::new(40.0, 80.0, 200.0, 100.0);
579 let (client, element) = keyboard_drop_points(Some(rect));
580
581 assert_eq!(client, Point::new(140.0, 130.0));
582 assert_eq!(element, Point::new(100.0, 50.0));
583 }
584
585 #[test]
586 fn keyboard_drop_points_fall_back_to_origin_without_rect() {
587 let (client, element) = keyboard_drop_points(None);
588
589 assert_eq!(client, Point::default());
590 assert_eq!(element, Point::default());
591 }
592}