1use dioxus::html::MountedData;
6use dioxus::prelude::*;
7
8use std::rc::Rc;
9
10use crate::core::hooks::{
11 use_bridge_world, use_dnd, use_zone_id, use_zone_registry, BridgeGeometry,
12};
13use crate::core::registry::{ZonePolicy, ZoneRecord};
14use crate::core::types::{edge_of, DragMode, DropOutcome, EdgeSet, Rect, ZoneId};
15use crate::core::world::use_joined_window;
16use crate::core::{DropEffects, DropQuery};
17
18#[derive(Clone, Copy, PartialEq)]
22pub struct ParentZone(pub ZoneId);
23
24#[derive(Clone, Copy, PartialEq)]
25struct LiveParentZone(Signal<ZoneId>);
26
27#[doc(hidden)]
31pub fn use_parent_zone() -> Option<ZoneId> {
32 let live = try_use_context::<LiveParentZone>();
33 let fixed = try_use_context::<ParentZone>();
34 live.map(|parent| *parent.0.read())
35 .or_else(|| fixed.map(|parent| parent.0))
36}
37
38#[doc(hidden)]
42#[component]
43pub fn BridgeParentZoneBoundary(zone_id: ZoneId, children: Element) -> Element {
44 let mut live = use_signal(|| zone_id);
45 use_effect(use_reactive!(|(zone_id)| {
46 if *live.peek() != zone_id {
47 live.set(zone_id);
48 }
49 }));
50 provide_context(LiveParentZone(live));
51 provide_context(ParentZone(zone_id));
52 rsx! { {children} }
53}
54
55#[component]
56fn FixedParentZoneBoundary(zone_id: ZoneId, children: Element) -> Element {
57 provide_context(ParentZone(zone_id));
58 rsx! { {children} }
59}
60
61#[component]
90pub fn DropZone<T: Clone + PartialEq + 'static>(
91 #[props(default)]
93 id: Option<ZoneId>,
94 #[props(default)]
96 label: Option<String>,
97 #[props(default)]
102 accepts: Option<Callback<T, bool>>,
103 #[props(default)]
106 accepts_query: Option<Callback<DropQuery<T>, bool>>,
107 #[props(default)]
110 allowed_effects: DropEffects,
111 #[props(default)]
116 edge: Option<EdgeSet>,
117 on_drop: EventHandler<DropOutcome<T>>,
119 #[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
120 children: Element,
121) -> Element {
122 let auto_id = use_zone_id();
123 let zone_id = id.unwrap_or(auto_id);
124 let parent = use_parent_zone();
125 rsx! {
126 for (keyed_zone_id, keyed_parent) in [(zone_id, parent)] {
127 DropZoneInstance::<T> {
128 key: "{keyed_zone_id.0}:{keyed_parent:?}",
129 zone_id: keyed_zone_id,
130 parent: keyed_parent,
131 provide_parent: true,
132 label: label.clone(),
133 accepts,
134 accepts_query,
135 allowed_effects,
136 edge,
137 on_drop,
138 attributes: attributes.clone(),
139 {children.clone()}
140 }
141 }
142 }
143}
144
145#[component]
146fn DropZoneInstance<T: Clone + PartialEq + 'static>(
147 zone_id: ZoneId,
148 parent: Option<ZoneId>,
149 provide_parent: bool,
150 label: Option<String>,
151 accepts: Option<Callback<T, bool>>,
152 accepts_query: Option<Callback<DropQuery<T>, bool>>,
153 allowed_effects: DropEffects,
154 edge: Option<EdgeSet>,
155 on_drop: EventHandler<DropOutcome<T>>,
156 attributes: Vec<Attribute>,
157 children: Element,
158) -> Element {
159 let dnd = use_dnd::<T>();
160 let joined = use_joined_window::<T>();
161 let mut registry = use_zone_registry::<T>();
162 let registered_label = label.clone();
168 let registration = use_hook(|| {
169 registry.register_with_policy(
170 ZoneRecord {
171 id: zone_id,
172 parent,
173 label: registered_label.clone(),
174 on_drop: Callback::new(move |outcome: DropOutcome<T>| on_drop.call(outcome)),
175 accepts,
176 mounted: None,
177 rect: None,
178 },
179 ZonePolicy {
180 accepts_query,
181 allowed_effects,
182 edge,
183 },
184 )
185 });
186 use_drop(move || {
187 registry.unregister_registration(registration);
188 });
189 let label_for_sync = label.clone();
190 use_effect(use_reactive!(|(label_for_sync)| {
191 registry.sync_label(zone_id, label_for_sync);
192 }));
193 use_effect(use_reactive!(|(parent)| {
194 registry.sync_parent(registration, parent);
195 }));
196 use_effect(use_reactive!(|(
197 accepts,
198 accepts_query,
199 allowed_effects,
200 edge,
201 )| {
202 registry.sync_policy(
203 registration,
204 accepts,
205 ZonePolicy {
206 accepts_query,
207 allowed_effects,
208 edge,
209 },
210 );
211 }));
212
213 let acceptable = move || -> bool {
214 let Some(payload) = dnd.payload() else {
215 return false;
216 };
217 let query = super::delivery::drop_query(&dnd, payload.clone(), dnd.proposed_effect());
218 query.proposed_effect != crate::core::DropEffect::None
219 && accepts.is_none_or(|callback| callback.call(payload.clone()))
220 && accepts_query.is_none_or(|callback| callback.call(query.clone()))
221 && allowed_effects.negotiate(query.proposed_effect).is_some()
222 };
223 let is_over = move || match joined {
224 Some(joined) => joined.is_over(zone_id),
225 None => dnd.over() == Some(zone_id),
226 };
227 let live_edge = move || -> Option<&'static str> {
232 let set = edge?;
233 if !is_over() || dnd.mode() != DragMode::Pointer || !acceptable() {
234 return None;
235 }
236 let r = registry.cached_rect(zone_id)?;
237 let pointer = joined
238 .and_then(|joined| joined.local_pointer())
239 .unwrap_or_else(|| dnd.pointer());
240 Some(edge_of(pointer, r, set).as_str())
241 };
242 let mut attributes = attributes;
243 super::protect_attributes(
244 &mut attributes,
245 &["data-active", "data-over", "data-edge", "onmounted"],
246 );
247
248 let content = if provide_parent {
249 rsx! {
250 FixedParentZoneBoundary { zone_id, {children} }
251 }
252 } else {
253 children
254 };
255
256 rsx! {
257 div {
258 "data-active": if dnd.dragging() && acceptable() { "true" },
259 "data-over": if is_over() && acceptable() { "true" },
260 "data-edge": live_edge(),
261 onmounted: move |evt: Event<MountedData>| {
262 let m: Rc<MountedData> = evt.data();
263 let mut registry = registry;
264 registry.set_mounted(registration, m.clone());
265 spawn(async move {
271 if let Ok(r) = m.get_client_rect().await {
272 registry.set_rect_if_present(registration, Rect::new(
273 r.origin.x,
274 r.origin.y,
275 r.size.width,
276 r.size.height,
277 ));
278 }
279 });
280 },
281 ..attributes,
282 {content}
283 }
284 }
285}
286
287#[component]
290pub(crate) fn FlatDropZone<T: Clone + PartialEq + 'static>(
291 zone_id: ZoneId,
292 #[props(default)] label: Option<String>,
293 #[props(default)] accepts_query: Option<Callback<DropQuery<T>, bool>>,
294 #[props(default)] edge: Option<EdgeSet>,
295 on_drop: EventHandler<DropOutcome<T>>,
296 #[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
297 children: Element,
298) -> Element {
299 let parent = use_parent_zone();
300 rsx! {
301 for (keyed_zone_id, keyed_parent) in [(zone_id, parent)] {
302 DropZoneInstance::<T> {
303 key: "{keyed_zone_id.0}:{keyed_parent:?}",
304 zone_id: keyed_zone_id,
305 parent: keyed_parent,
306 provide_parent: false,
307 label: label.clone(),
308 accepts: None,
309 accepts_query,
310 allowed_effects: DropEffects::default(),
311 edge,
312 on_drop,
313 attributes: attributes.clone(),
314 {children.clone()}
315 }
316 }
317 }
318}
319
320#[component]
343pub fn BridgeDropZone<A: Clone + PartialEq + 'static, B: Clone + PartialEq + 'static>(
344 #[props(default)]
347 id: Option<ZoneId>,
348 #[props(default)]
350 label: Option<String>,
351 #[props(default)]
353 accepts_a: Option<Callback<A, bool>>,
354 #[props(default)]
356 accepts_b: Option<Callback<B, bool>>,
357 on_drop_a: EventHandler<DropOutcome<A>>,
359 on_drop_b: EventHandler<DropOutcome<B>>,
361 #[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
362 children: Element,
363) -> Element {
364 let auto_id = use_zone_id();
365 let zone_id = id.unwrap_or(auto_id);
366 let parent = use_parent_zone();
367 rsx! {
368 for (keyed_zone_id, keyed_parent) in [(zone_id, parent)] {
369 BridgeDropZoneInstance::<A, B> {
370 key: "{keyed_zone_id.0}:{keyed_parent:?}",
371 zone_id: keyed_zone_id,
372 parent: keyed_parent,
373 label: label.clone(),
374 accepts_a,
375 accepts_b,
376 on_drop_a,
377 on_drop_b,
378 attributes: attributes.clone(),
379 {children.clone()}
380 }
381 }
382 }
383}
384
385#[component]
386fn BridgeDropZoneInstance<A: Clone + PartialEq + 'static, B: Clone + PartialEq + 'static>(
387 zone_id: ZoneId,
388 parent: Option<ZoneId>,
389 label: Option<String>,
390 accepts_a: Option<Callback<A, bool>>,
391 accepts_b: Option<Callback<B, bool>>,
392 on_drop_a: EventHandler<DropOutcome<A>>,
393 on_drop_b: EventHandler<DropOutcome<B>>,
394 attributes: Vec<Attribute>,
395 children: Element,
396) -> Element {
397 provide_context(ParentZone(zone_id));
400 let geometry = use_hook(BridgeGeometry::default);
401 let a = use_bridge_world::<A>(
404 zone_id,
405 parent,
406 label.clone(),
407 accepts_a,
408 on_drop_a,
409 geometry.clone(),
410 );
411 let b = use_bridge_world::<B>(
412 zone_id,
413 parent,
414 label,
415 accepts_b,
416 on_drop_b,
417 geometry.clone(),
418 );
419 let mut attributes = attributes;
420 super::protect_attributes(&mut attributes, &["data-active", "data-over", "onmounted"]);
421
422 rsx! {
423 div {
424 "data-active": if a.active || b.active { "true" },
425 "data-over": if a.over || b.over { "true" },
426 onmounted: move |evt: Event<MountedData>| {
427 let m: Rc<MountedData> = evt.data();
428 geometry.set_mounted(&m);
429 let geometry = geometry.clone();
433 spawn(async move {
434 if let Ok(r) = m.get_client_rect().await {
435 let rect = Rect::new(
436 r.origin.x,
437 r.origin.y,
438 r.size.width,
439 r.size.height,
440 );
441 geometry.set_rect_if_present(rect);
442 }
443 });
444 },
445 ..attributes,
446 {children}
447 }
448 }
449}
450
451#[macro_export]
489macro_rules! bridge_drop_zone {
490 (
491 $(#[$meta:meta])*
492 $vis:vis $name:ident {
493 $( ($ty:ty, $accepts:ident, $on_drop:ident) ),+ $(,)?
494 }
495 ) => {
496 $(#[$meta])*
497 #[::dioxus::prelude::component]
498 #[allow(non_snake_case)]
499 $vis fn $name(
500 #[props(default)]
503 id: ::std::option::Option<$crate::core::ZoneId>,
504 #[props(default)]
507 label: ::std::option::Option<::std::string::String>,
508 $(
509 #[props(default)]
510 $accepts: ::std::option::Option<::dioxus::prelude::Callback<$ty, bool>>,
511 $on_drop: ::dioxus::prelude::EventHandler<$crate::core::DropOutcome<$ty>>,
512 )+
513 #[props(extends = div, extends = GlobalAttributes)]
514 attributes: ::std::vec::Vec<::dioxus::prelude::Attribute>,
515 children: ::dioxus::prelude::Element,
516 ) -> ::dioxus::prelude::Element {
517 use ::dioxus::prelude::*;
518
519 let auto_id = $crate::core::use_zone_id();
520 let zone_id = id.unwrap_or(auto_id);
521 let parent = $crate::core::use_parent_zone();
522 let geometry = use_hook($crate::core::BridgeGeometry::default);
523 let mut attributes = attributes;
524 attributes.retain(|attribute| {
525 !matches!(attribute.name, "data-active" | "data-over" | "onmounted")
526 });
527 let mut active = false;
528 let mut over = false;
529 $(
530 let world = $crate::core::use_bridge_world::<$ty>(
531 zone_id,
532 parent,
533 label.clone(),
534 $accepts,
535 $on_drop,
536 geometry.clone(),
537 );
538 active |= world.active;
539 over |= world.over;
540 )+
541
542 rsx! {
543 $crate::core::BridgeParentZoneBoundary {
544 key: "{zone_id.0}",
545 zone_id,
546 div {
547 "data-active": if active { "true" },
548 "data-over": if over { "true" },
549 onmounted: move |evt: Event<::dioxus::html::MountedData>| {
550 let m = evt.data();
551 geometry.set_mounted(&m);
552 let geometry = geometry.clone();
557 spawn(async move {
558 if let Ok(r) = m.get_client_rect().await {
559 let rect = $crate::core::Rect::new(
560 r.origin.x,
561 r.origin.y,
562 r.size.width,
563 r.size.height,
564 );
565 geometry.set_rect_if_present(rect);
566 }
567 });
568 },
569 ..attributes,
570 {children}
571 }
572 }
573 }
574 }
575 };
576}
577
578#[cfg(test)]
579mod tests {
580 use std::cell::Cell;
581 use std::rc::Rc;
582
583 use super::*;
584 use crate::core::{DndProvider, DragMode, DropEffect, Point};
585
586 #[component]
587 fn ProposedEffectProbe() -> Element {
588 let mut dnd = use_dnd::<u8>();
589 use_hook(move || {
590 dnd.start(
591 7,
592 None,
593 Point::new(10.0, 10.0),
594 Point::default(),
595 DropEffect::Move,
596 DragMode::Pointer,
597 );
598 dnd.set_proposed_effect(DropEffect::Copy);
599 });
600 rsx! {
601 DropZone::<u8> {
602 allowed_effects: DropEffects::COPY,
603 accepts_query: move |query: DropQuery<u8>| {
604 query.proposed_effect == DropEffect::Copy
605 },
606 on_drop: move |_| {},
607 "copy target"
608 }
609 }
610 }
611
612 fn proposed_effect_app() -> Element {
613 rsx! {
614 DndProvider::<u8> { ProposedEffectProbe {} }
615 }
616 }
617
618 #[test]
619 fn active_state_uses_the_live_proposed_effect() {
620 let mut dom = VirtualDom::new(proposed_effect_app);
621 dom.rebuild_in_place();
622 let html = dioxus_ssr::render(&dom);
623 assert!(
624 html.contains(r#"data-active="true""#),
625 "copy target stayed dark: {html}"
626 );
627 }
628
629 #[derive(Clone, Props)]
630 struct DynamicIdProps {
631 phase: Rc<Cell<bool>>,
632 }
633
634 impl PartialEq for DynamicIdProps {
635 fn eq(&self, other: &Self) -> bool {
636 Rc::ptr_eq(&self.phase, &other.phase)
637 }
638 }
639
640 fn dynamic_id_app(props: DynamicIdProps) -> Element {
641 let id = if props.phase.get() {
642 ZoneId(2)
643 } else {
644 ZoneId(1)
645 };
646 rsx! {
647 DndProvider::<u8> {
648 DropZone::<u8> {
649 id,
650 on_drop: move |_| {},
651 DynamicIdProbe { expected: id }
652 }
653 }
654 }
655 }
656
657 #[component]
658 fn DynamicIdProbe(expected: ZoneId) -> Element {
659 let registry = use_zone_registry::<u8>();
660 let stale = if expected == ZoneId(1) {
661 ZoneId(2)
662 } else {
663 ZoneId(1)
664 };
665 assert!(registry.contains(expected));
666 assert!(!registry.contains(stale));
667 rsx! { div {} }
668 }
669
670 #[test]
671 fn changing_an_explicit_id_replaces_the_registered_instance() {
672 let phase = Rc::new(Cell::new(false));
673 let mut dom = VirtualDom::new_with_props(
674 dynamic_id_app,
675 DynamicIdProps {
676 phase: phase.clone(),
677 },
678 );
679 dom.rebuild_in_place();
680
681 phase.set(true);
682 dom.mark_dirty(ScopeId::APP);
683 dom.render_immediate(&mut dioxus::core::NoOpMutations);
684 }
685}