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.parent_of(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().or_else(|| {
395 dnd.payload().and_then(|p| registry.step_zone(None, &p, 1))
396 });
397 let Some(target) = target else {
398 dnd.announce("No drop target selected.");
399 return;
400 };
401 if let Some(record) = registry.get(target) {
402 if let Some((p, from)) = dnd.take() {
403 let (client, element) = keyboard_drop_points(*record.rect.peek());
404 record.on_drop.call(DropOutcome {
405 payload: p,
406 from,
407 to: target,
408 effect,
409 mode: DragMode::Keyboard,
410 client,
411 element,
412 grab: Point::default(),
413 });
414 let name = record
415 .label
416 .unwrap_or_else(|| format!("zone {}", target.0));
417 dnd.announce(format!("Dropped in {name}."));
418 if let Some(h) = &on_drag_end {
419 h.call(true);
420 }
421 }
422 }
423 return;
424 }
425
426 if matches!(key, Key::Escape) {
427 evt.prevent_default();
428 dnd.cancel();
429 dnd.announce("Drag cancelled.");
430 if let Some(h) = &on_drag_end {
431 h.call(false);
432 }
433 }
434 },
435 ..attributes,
436 {children}
437 }
438 }
439}
440
441#[component]
455pub fn DropZone<T: Clone + PartialEq + 'static>(
456 #[props(default)]
458 id: Option<ZoneId>,
459 #[props(default)]
461 label: Option<String>,
462 #[props(default)]
464 accepts: Option<Callback<T, bool>>,
465 on_drop: EventHandler<DropOutcome<T>>,
467 #[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
468 children: Element,
469) -> Element {
470 let dnd = use_dnd::<T>();
471 let mut registry = use_zone_registry::<T>();
472 let auto_id = use_zone_id();
473 let zone_id = id.unwrap_or(auto_id);
474 let parent = try_use_context::<ParentZone>().map(|p| p.0);
477 use_context_provider(|| ParentZone(zone_id));
478 let mounted = use_signal(|| None::<Rc<MountedData>>);
479 let rect = use_signal(|| None::<super::types::Rect>);
480
481 use_hook(|| {
485 registry.register(ZoneRecord {
486 id: zone_id,
487 parent,
488 label: label.clone(),
489 on_drop: Callback::new(move |o| on_drop.call(o)),
490 accepts,
491 mounted,
492 rect,
493 });
494 });
495 use_drop(move || {
496 registry.unregister(zone_id);
497 });
498 registry.sync_label(zone_id, label.clone());
501
502 let acceptable = move || -> bool {
503 match dnd.payload() {
504 Some(p) => accepts.map(|cb| cb.call(p)).unwrap_or(true),
505 None => false,
506 }
507 };
508
509 rsx! {
510 div {
511 "data-active": if dnd.dragging() && acceptable() { "true" },
512 "data-over": if dnd.over() == Some(zone_id) && acceptable() { "true" },
513 onmounted: move |evt: Event<MountedData>| {
514 let mut mounted = mounted;
515 mounted.set(Some(evt.data()));
516 },
517 ..attributes,
518 {children}
519 }
520 }
521}
522
523pub(crate) fn overlay_style(pos: Point) -> String {
528 format!(
529 "position: fixed; left: {}px; top: {}px; pointer-events: none; z-index: 9999;",
530 pos.x, pos.y
531 )
532}
533
534#[component]
547pub fn DragOverlay<T: Clone + PartialEq + 'static>(
548 #[props(default)]
550 phantom: std::marker::PhantomData<T>,
551 #[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
552 children: Element,
553) -> Element {
554 let _ = phantom;
555 let dnd = use_dnd::<T>();
556 if !dnd.dragging() {
557 return rsx! {};
558 }
559 let mut attributes = attributes;
560 let style = merge_style(&mut attributes, &overlay_style(dnd.pointer() - dnd.grab()));
561 rsx! {
562 div {
563 style: style,
564 ..attributes,
565 {children}
566 }
567 }
568}
569
570#[cfg(test)]
571mod tests {
572 use super::*;
573
574 #[test]
575 fn keyboard_drop_points_use_zone_center_and_element_offset() {
576 let rect = Rect::new(40.0, 80.0, 200.0, 100.0);
577 let (client, element) = keyboard_drop_points(Some(rect));
578
579 assert_eq!(client, Point::new(140.0, 130.0));
580 assert_eq!(element, Point::new(100.0, 50.0));
581 }
582
583 #[test]
584 fn keyboard_drop_points_fall_back_to_origin_without_rect() {
585 let (client, element) = keyboard_drop_points(None);
586
587 assert_eq!(client, Point::default());
588 assert_eq!(element, Point::default());
589 }
590}