Skip to main content

dioxus_dnd/core/world/
mod.rs

1//! Multi-window drag worlds: one shared drag state spanning several
2//! windows of a desktop app, each window an independent `VirtualDom`.
3//!
4//! Dioxus desktop polls every window's `VirtualDom` on the main thread, and
5//! signal storage is thread-local rather than runtime-local, so a `Signal`
6//! (and therefore a [`DndContext`](crate::core::DndContext)) created in one window's runtime can be
7//! read, written and subscribed from another's - a write in window A
8//! re-renders window B through B's own scheduler. `DndWorld` builds on
9//! exactly that: the payload crosses windows as a live Rust value, with no
10//! serialization and none of the platform roulette of native HTML5
11//! drag-and-drop. (`DataTransfer` interop for drags that leave the app
12//! entirely stays in [`crate::external`].)
13//!
14//! # Coordinate spaces
15//!
16//! Everything zone-shaped stays in **client CSS pixels of its own window**,
17//! exactly as in single-window use. The world adds one more space: **global
18//! desktop physical pixels**, in which windows are located and hit-tested.
19//! Each window's [`WindowGeometry`] carries the conversion: the client
20//! area's top-left in physical px (`inner_position()` on desktop), the
21//! window scale factor, and the client-area size in physical px. Conversion
22//! happens only at the world boundary.
23//!
24//! # Wiring
25//!
26//! With the `desktop` feature, render `MultiWindowProvider<T>` once in every
27//! window. It installs the geometry feed above the drag provider and the host
28//! bridge below it, so their required ordering is structural. Create sibling
29//! VDOMs with [`DndWorld::vdom`] so the world cannot be omitted; chain the app
30//! model and legitimate per-window context afterwards:
31//!
32//! ```text
33//! fn main_window() -> Element {
34//!     let world = use_dnd_world::<Card>();
35//!     let model = use_dnd_model(Model::new);
36//!     let popup_model = model.clone();
37//!     let open = move |_| {
38//!         let dom = world.vdom(popup).with_root_context(popup_model.clone());
39//!         dioxus::desktop::window().new_window(dom, Default::default());
40//!     };
41//!     rsx! {
42//!         MultiWindowProvider::<Card> {
43//!             button { onclick: open, "Open" }
44//!             // zones, overlay, live region
45//!         }
46//!     }
47//! }
48//!
49//! fn popup() -> Element {
50//!     let model = use_context::<Model>();
51//!     rsx! { MultiWindowProvider::<Card> { /* ... */ } }
52//! }
53//! ```
54//!
55//! `MultiWindowProvider` warns once if it mounts without a world in context;
56//! that window otherwise falls back to isolated drag state.
57//!
58//! Custom windowing hosts use the manual path: provide a [`WindowGeometry`]
59//! above `DndProvider<T>`, update it from host move/resize/focus events, and
60//! render the host bridge inside the provider after it joins. A provider that
61//! finds a world joins it instead of creating isolated state (nested providers
62//! keep today's shadowing semantics: only a window's outermost provider of `T`
63//! joins). Sample placement in global physical pixels and call
64//! [`WindowGeometry::set`]; call [`WindowGeometry::mark_focused`] on focus so
65//! overlapping windows resolve to the frontmost. **Without geometry the world
66//! degrades gracefully**: drags behave exactly as single-window drags (this is
67//! also the honest Wayland story, where a client can learn neither the cursor's
68//! global position nor its own windows' positions).
69//!
70//! # Lifetimes: close windows in any order
71//!
72//! A world's own state (the shared context and the window table) is
73//! **process-lived**: it is created under an owner this module holds for
74//! the life of the app, not under any window's scope. Whichever window
75//! created the world can close first and every other window keeps
76//! dragging - cross-window between the survivors, single-window when only
77//! one remains. Closing a joined window prunes it from the table and
78//! aborts an in-flight drag that originated there (its coordinate anchor
79//! is gone). The cost is a deliberate, bounded leak: a handful of signals
80//! per world, once per app.
81
82use dioxus::prelude::*;
83
84mod drag;
85mod geometry;
86mod host;
87mod joined;
88mod settle;
89mod state;
90
91pub use geometry::{WindowGeometry, WindowKey};
92pub use joined::JoinedWindow;
93pub(crate) use joined::{WorldHit, WorldMembership};
94pub use state::{DndWorld, WindowRecord, ZoneLocation};
95
96/// Create a `DndWorld<T>` (process-lived - see the module docs on
97/// lifetimes) and provide it in context, so providers in this window join
98/// it. Create sibling windows with [`DndWorld::vdom`]. Call it once, in any
99/// window.
100pub fn use_dnd_world<T: Clone + 'static>() -> DndWorld<T> {
101    use_hook(|| provide_context(DndWorld::<T>::new()))
102}
103
104/// The enclosing provider's world membership, if it joined a world - the
105/// handle desktop glue needs to bridge host-side input (see
106/// [`DndWorld::track_global`] / [`DndWorld::drop_at_global`]). Call it
107/// anywhere below the `DndProvider`.
108pub fn use_joined_window<T: Clone + 'static>() -> Option<JoinedWindow<T>> {
109    try_use_context::<WorldMembership<T>>().and_then(|m| m.0)
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115    use std::cell::RefCell;
116    use std::rc::Rc;
117
118    #[derive(Clone, Default)]
119    struct WorldSlot(Rc<RefCell<Option<DndWorld<String>>>>);
120
121    fn world_creator() -> Element {
122        let slot = use_context::<WorldSlot>();
123        slot.0.replace(Some(use_dnd_world::<String>()));
124        rsx! {}
125    }
126
127    fn seeded_sibling() -> Element {
128        let slot = use_context::<WorldSlot>();
129        slot.0.replace(Some(use_context::<DndWorld<String>>()));
130        rsx! {}
131    }
132
133    #[test]
134    fn vdom_seeds_the_world_root_context() {
135        let created = WorldSlot::default();
136        let mut creator = VirtualDom::new(world_creator).with_root_context(created.clone());
137        creator.rebuild_in_place();
138        let world = created.0.take().expect("creator published its world");
139
140        drop(creator);
141        let seen = WorldSlot::default();
142        let mut sibling = world.vdom(seeded_sibling).with_root_context(seen.clone());
143        sibling.rebuild_in_place();
144
145        assert!(seen.0.take().is_some_and(|seen| seen == world));
146    }
147}