Expand description
Multi-window drag worlds: one shared drag state spanning several
windows of a desktop app, each window an independent VirtualDom.
Dioxus desktop polls every window’s VirtualDom on the main thread, and
signal storage is thread-local rather than runtime-local, so a Signal
(and therefore a DndContext) created in one window’s runtime can be
read, written and subscribed from another’s - a write in window A
re-renders window B through B’s own scheduler. DndWorld builds on
exactly that: the payload crosses windows as a live Rust value, with no
serialization and none of the platform roulette of native HTML5
drag-and-drop. (DataTransfer interop for drags that leave the app
entirely stays in crate::external.)
§Coordinate spaces
Everything zone-shaped stays in client CSS pixels of its own window,
exactly as in single-window use. The world adds one more space: global
desktop physical pixels, in which windows are located and hit-tested.
Each window’s WindowGeometry carries the conversion: the client
area’s top-left in physical px (inner_position() on desktop), the
window scale factor, and the client-area size in physical px. Conversion
happens only at the world boundary.
§Wiring
With the desktop feature, render MultiWindowProvider<T> once in every
window. It installs the geometry feed above the drag provider and the host
bridge below it, so their required ordering is structural. Create sibling
VDOMs with DndWorld::vdom so the world cannot be omitted; chain the app
model and legitimate per-window context afterwards:
fn main_window() -> Element {
let world = use_dnd_world::<Card>();
let model = use_dnd_model(Model::new);
let popup_model = model.clone();
let open = move |_| {
let dom = world.vdom(popup).with_root_context(popup_model.clone());
dioxus::desktop::window().new_window(dom, Default::default());
};
rsx! {
MultiWindowProvider::<Card> {
button { onclick: open, "Open" }
// zones, overlay, live region
}
}
}
fn popup() -> Element {
let model = use_context::<Model>();
rsx! { MultiWindowProvider::<Card> { /* ... */ } }
}MultiWindowProvider warns once if it mounts without a world in context;
that window otherwise falls back to isolated drag state.
Custom windowing hosts use the manual path: provide a WindowGeometry
above DndProvider<T>, update it from host move/resize/focus events, and
render the host bridge inside the provider after it joins. A provider that
finds a world joins it instead of creating isolated state (nested providers
keep today’s shadowing semantics: only a window’s outermost provider of T
joins). Sample placement in global physical pixels and call
WindowGeometry::set; call WindowGeometry::mark_focused on focus so
overlapping windows resolve to the frontmost. Without geometry the world
degrades gracefully: drags behave exactly as single-window drags (this is
also the honest Wayland story, where a client can learn neither the cursor’s
global position nor its own windows’ positions).
§Lifetimes: close windows in any order
A world’s own state (the shared context and the window table) is process-lived: it is created under an owner this module holds for the life of the app, not under any window’s scope. Whichever window created the world can close first and every other window keeps dragging - cross-window between the survivors, single-window when only one remains. Closing a joined window prunes it from the table and aborts an in-flight drag that originated there (its coordinate anchor is gone). The cost is a deliberate, bounded leak: a handful of signals per world, once per app.
Structs§
- DndWorld
- A drag world shared by several windows: one
DndContextevery joined provider re-provides, plus the window table cross-window hit-testing walks. Cheap to copy; seed sibling windows withSelf::vdom. - Joined
Window - A provider’s handle to the world it joined: the world, this window’s key, and this window’s geometry - everything the pointer path needs to think cross-window.
- Window
Geometry - One window’s placement on the desktop, as reactive signals the host feeds. Copy handle; create one per window (the provider creates an inert one when none is in context) and keep it updated from your windowing layer. Missing placement or host ineligibility makes it inert: the window still drags internally, but cannot take part in cross-window hit-testing.
- Window
Key - Identifies one joined window within a
DndWorld. Process-unique. - Window
Record - One window joined to a
DndWorld: its geometry, its zone registry, and the per-window handles drop delivery needs. - Zone
Location - A drop-zone identity qualified by the joined window that owns it.
Functions§
- use_
dnd_ world - Create a
DndWorld<T>(process-lived - see the module docs on lifetimes) and provide it in context, so providers in this window join it. Create sibling windows withDndWorld::vdom. Call it once, in any window. - use_
joined_ window - The enclosing provider’s world membership, if it joined a world - the
handle desktop glue needs to bridge host-side input (see
DndWorld::track_global/DndWorld::drop_at_global). Call it anywhere below theDndProvider.