Skip to main content

dioxus_dnd/desktop/
provider.rs

1//! The correctly ordered per-window multi-window provider.
2
3use dioxus::prelude::*;
4
5use crate::core::world::WorldMembership;
6use crate::core::{Direction, DndProvider, DndWorld};
7
8use super::{use_window_geometry_feed, DragBridge};
9
10/// Wire one dioxus-desktop window into a [`DndWorld`].
11///
12/// This component structurally enforces the ordering the desktop adapter
13/// needs: its geometry feed is mounted above [`DndProvider`], and its
14/// [`DragBridge`] is mounted inside the provider after the window joins.
15/// Keep app-styled pieces such as `DragOverlay` and `LiveRegion` among the
16/// children.
17///
18/// Mounting without a `DndWorld<T>` in context emits one warning and leaves
19/// the provider with its normal isolated, single-window state. Nesting this
20/// component below another same-payload `DndProvider<T>` also warns: replace
21/// the old provider rather than wrapping it, because only the outer provider
22/// may join a world. Create the world with [`crate::core::use_dnd_world`] and
23/// seed spawned windows with [`DndWorld::vdom`] to avoid the fallback.
24#[component]
25pub fn MultiWindowProvider<T: Clone + PartialEq + 'static>(
26    /// Internal marker; never set this.
27    #[props(default)]
28    phantom: std::marker::PhantomData<T>,
29    /// Layout direction forwarded to [`DndProvider`].
30    #[props(default)]
31    dir: Direction,
32    children: Element,
33) -> Element {
34    let _ = phantom;
35    use_window_geometry_feed();
36    use_hook(|| {
37        let has_world = try_consume_context::<DndWorld<T>>().is_some();
38        let has_ancestor_provider = try_consume_context::<WorldMembership<T>>().is_some();
39        if let Some(message) = wiring_warning(has_world, has_ancestor_provider) {
40            tracing::warn!(
41                target: "dioxus_dnd::desktop",
42                payload_type = std::any::type_name::<T>(),
43                "{message}"
44            );
45        }
46    });
47
48    rsx! {
49        DndProvider::<T> { dir,
50            DragBridge::<T> {}
51            {children}
52        }
53    }
54}
55
56fn wiring_warning(has_world: bool, has_ancestor_provider: bool) -> Option<&'static str> {
57    if has_ancestor_provider {
58        Some(
59            "MultiWindowProvider mounted beneath an existing DndProvider for the same payload; \
60             replace that provider instead of wrapping it, and ensure a DndWorld is in context; \
61             the nested provider's drags will otherwise remain isolated",
62        )
63    } else if !has_world {
64        Some(
65            "MultiWindowProvider mounted without a DndWorld in context; call use_dnd_world in one \
66             window and create siblings with DndWorld::vdom; this window's drags will remain isolated",
67        )
68    } else {
69        None
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn wiring_diagnostic_covers_missing_world_and_nested_provider() {
79        assert!(wiring_warning(true, false).is_none());
80        assert!(wiring_warning(false, false)
81            .expect("missing world warning")
82            .contains("without a DndWorld"));
83        assert!(wiring_warning(true, true)
84            .expect("nested provider warning")
85            .contains("replace that provider"));
86        assert!(wiring_warning(false, true)
87            .expect("combined warning")
88            .contains("ensure a DndWorld"));
89    }
90}