dioxus_dnd/core/world/state.rs
1//! World construction and the joined-window table: process-lived state,
2//! join/leave lifecycle, and window lookup.
3
4use std::cell::RefCell;
5use std::mem::ManuallyDrop;
6
7use dioxus::prelude::*;
8
9use crate::core::hooks::SettleFlag;
10use crate::core::model::DndScope;
11use crate::core::monitor::CancelReason;
12use crate::core::registry::ZoneRegistry;
13use crate::core::state::{DndContext, DragState};
14use crate::core::types::{Point, ZoneId};
15
16use super::drag::ActiveDrag;
17use super::geometry::{WindowGeometry, WindowKey};
18use super::settle::SettleClaim;
19
20thread_local! {
21 /// Owners of every world's state, held for the life of the process
22 /// (all of an app's windows share one thread). Worlds are deliberately
23 /// immortal: scope-owned state would die with its creating window and
24 /// panic every surviving window that still renders from it, and no
25 /// close order should be able to do that. Bounded: a few signals per
26 /// world, one world per payload type per app, window records pruned on
27 /// close. Both storage flavors: signals live in unsync storage, but a
28 /// store's subscription tree allocates in SYNC storage.
29 /// `ManuallyDrop` keeps the process-lifetime contract true even if the UI
30 /// thread itself exits before the surrounding process.
31 static WORLD_OWNERS: RefCell<Vec<ManuallyDrop<DndScope>>> = const { RefCell::new(Vec::new()) };
32}
33
34/// The world's initial bridging policy from `DIOXUS_DND_NO_BRIDGE`,
35/// read once at creation so end users can disable host-side bridging
36/// without a rebuild. Opt-out semantics: only an explicit non-`0`,
37/// non-empty value disables - an unset or neutered variable must never
38/// strand the flagship feature by accident.
39fn default_bridging(no_bridge: Option<&str>) -> bool {
40 match no_bridge {
41 None | Some("") | Some("0") => true,
42 Some(_) => false,
43 }
44}
45
46/// A drop-zone identity qualified by the joined window that owns it.
47///
48/// Legacy single-window APIs continue to expose [`ZoneId`]; worlds use this
49/// richer identity so separate windows may safely reuse the same explicit id.
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
51pub struct ZoneLocation {
52 pub window: WindowKey,
53 pub zone: ZoneId,
54}
55
56/// One window joined to a [`DndWorld`]: its geometry, its zone registry,
57/// and the per-window handles drop delivery needs.
58pub struct WindowRecord<T: Clone + 'static> {
59 pub key: WindowKey,
60 pub geometry: WindowGeometry,
61 pub registry: ZoneRegistry<T>,
62 /// The window's own settle flag: a drop landing here settles iff *this*
63 /// window has a settle-enabled overlay mounted.
64 pub(crate) settle: SettleFlag<T>,
65 /// Re-measures the window's zones. Created by the window's provider, so
66 /// calling it runs `refresh_rects` inside that window's own runtime
67 /// (`Callback::call` re-enters its origin runtime) - the spawned
68 /// measurements land on the right scheduler.
69 pub(crate) refresh: Callback<()>,
70}
71
72impl<T: Clone + 'static> Copy for WindowRecord<T> {}
73impl<T: Clone + 'static> Clone for WindowRecord<T> {
74 fn clone(&self) -> Self {
75 *self
76 }
77}
78
79/// A drag world shared by several windows: one [`DndContext`] every joined
80/// provider re-provides, plus the window table cross-window hit-testing
81/// walks. Cheap to copy; seed sibling windows with [`Self::vdom`].
82pub struct DndWorld<T: Clone + 'static> {
83 pub(super) ctx: DndContext<T>,
84 windows: Signal<Vec<WindowRecord<T>>>,
85 /// The window the in-flight drag started in - the coordinate anchor:
86 /// `ctx.pointer()` is always in *this* window's client px.
87 pub(super) active: Signal<Option<ActiveDrag>>,
88 /// Exact host- or world-resolved pointer position in global physical px.
89 /// Kept separate from `active` so pointer ticks do not invalidate
90 /// session-metadata subscribers.
91 pub(super) global_pointer: Signal<Option<Point>>,
92 /// Window-qualified hover identity. The legacy id remains in
93 /// `DragState` for single-window and custom-source compatibility.
94 pub(super) over_location: Signal<Option<ZoneLocation>>,
95 /// The elected settle presenter and its freshness generation. Kept as
96 /// one value so owner and generation can never disagree.
97 pub(super) settle_claim: Signal<Option<SettleClaim>>,
98 /// Host-side bridging kill switch (see [`DndWorld::set_bridging`]).
99 /// Owned by the world, not the desktop adapter, so a custom host
100 /// cannot keep driving a world whose app disabled bridging.
101 bridging: Signal<bool>,
102}
103
104impl<T: Clone + 'static> Copy for DndWorld<T> {}
105impl<T: Clone + 'static> Clone for DndWorld<T> {
106 fn clone(&self) -> Self {
107 *self
108 }
109}
110impl<T: Clone + 'static> PartialEq for DndWorld<T> {
111 fn eq(&self, other: &Self) -> bool {
112 self.windows == other.windows
113 }
114}
115
116impl<T: Clone + 'static> DndWorld<T> {
117 /// Create a world. Its state is **process-lived** (see the module docs
118 /// on lifetimes), so windows may close in any order afterwards. Must
119 /// run inside a Dioxus app; prefer [`use_dnd_world`](super::use_dnd_world), which also
120 /// provides the world in context.
121 pub fn new() -> Self {
122 let scope = DndScope::new();
123 let world = scope.with(|| Self {
124 ctx: DndContext::managed(Store::new(DragState::default()), Signal::new(String::new())),
125 windows: Signal::new(Vec::new()),
126 active: Signal::new(None),
127 global_pointer: Signal::new(None),
128 over_location: Signal::new(None),
129 settle_claim: Signal::new(None),
130 bridging: Signal::new(default_bridging(
131 std::env::var("DIOXUS_DND_NO_BRIDGE").ok().as_deref(),
132 )),
133 });
134 WORLD_OWNERS.with_borrow_mut(|owners| owners.push(ManuallyDrop::new(scope)));
135 world
136 }
137
138 /// Create a sibling window's [`VirtualDom`] with this world already in
139 /// its root context.
140 ///
141 /// Chain `with_root_context` for the app model and any per-window data:
142 ///
143 /// ```ignore
144 /// let dom = world
145 /// .vdom(tray_window)
146 /// .with_root_context(model)
147 /// .with_root_context(tray);
148 /// ```
149 ///
150 /// This is the preferred way to seed spawned windows because omitting the
151 /// world would otherwise leave their providers silently isolated.
152 pub fn vdom(self, root: fn() -> Element) -> VirtualDom {
153 VirtualDom::new(root).with_root_context(self)
154 }
155
156 /// The shared drag context every joined provider re-provides.
157 pub fn context(&self) -> DndContext<T> {
158 self.ctx
159 }
160
161 /// Enable or disable host-side bridging at runtime - the lever for the
162 /// day a webview or OS update ships a cross-window regression that a
163 /// rebuild cannot wait for. While disabled, every host-drive entry
164 /// point ([`Self::track_global`], [`Self::drop_at_global`]) is inert
165 /// and the `desktop` feature's bridge legs stand down, so drags
166 /// degrade to per-window - exactly the already-modeled Wayland
167 /// behavior. Local drags, geometry, settle and delivery are untouched.
168 /// [`Self::cancel_drag`] deliberately stays live: it is an escape
169 /// hatch, not a bridge leg.
170 ///
171 /// End users can flip the same switch without a rebuild by setting
172 /// `DIOXUS_DND_NO_BRIDGE=1` before launch (read once at world
173 /// creation; `0` or an empty value leaves bridging on).
174 pub fn set_bridging(&self, enabled: bool) {
175 let mut bridging = self.bridging;
176 if *bridging.peek() != enabled {
177 bridging.set(enabled);
178 }
179 }
180
181 /// Is host-side bridging currently enabled? (See [`Self::set_bridging`].)
182 pub fn bridging_enabled(&self) -> bool {
183 // try_peek: callable from destructors and foreign runtimes, like
184 // every other world read on the leg paths.
185 self.bridging.try_peek().map(|b| *b).unwrap_or(true)
186 }
187
188 /// Join a window. Called by `use_dnd_provider` when it finds a world in
189 /// context; call directly only from custom provider integrations.
190 pub(crate) fn join(
191 &self,
192 geometry: WindowGeometry,
193 registry: ZoneRegistry<T>,
194 settle: SettleFlag<T>,
195 refresh: Callback<()>,
196 ) -> WindowKey {
197 let key = WindowKey::auto();
198 let mut windows = self.windows;
199 windows.write().push(WindowRecord {
200 key,
201 geometry,
202 registry,
203 settle,
204 refresh,
205 });
206 key
207 }
208
209 /// Remove a window (its provider unmounted, usually because the window
210 /// closed). An active drag that originated there aborts because its
211 /// coordinate anchor is gone; a receiver-owned settle survives from its
212 /// snapshotted release point. A drag merely hovering one of the leaving
213 /// window's zones just loses the hover. Pruning keeps the world from ever
214 /// calling into a closed window's runtime.
215 pub(crate) fn leave(&self, key: WindowKey) {
216 // Worlds are process-lived, so this should be unreachable - but
217 // leave runs inside a destructor, where a panic aborts the whole
218 // process, so degrade to a no-op rather than trusting that.
219 if self.windows.try_peek().is_err() {
220 return;
221 }
222 let mut ctx = self.ctx;
223 {
224 let mut windows = self.windows;
225 windows.write().retain(|w| w.key != key);
226 }
227 // Qualified hover tells us exactly which window disappeared, even
228 // when another survivor reuses the same explicit ZoneId. Retain the
229 // legacy reachability fallback for keyboard/custom paths that have
230 // not supplied world-qualified metadata.
231 let qualified_over = *self.over_location.peek();
232 if qualified_over.is_some_and(|over| over.window == key) {
233 if let Some(over) = ctx.over() {
234 ctx.leave(over);
235 }
236 let mut over_location = self.over_location;
237 over_location.set(None);
238 } else if qualified_over.is_none() {
239 // Checked against the REMAINING windows, not the leaving one:
240 // scopes drop children first, so the leaving window's zones have
241 // usually unregistered before its provider leaves.
242 if let Some(over) = ctx.over() {
243 let reachable = self
244 .windows
245 .peek()
246 .iter()
247 .any(|w| w.registry.contains(over));
248 if !reachable {
249 ctx.leave(over);
250 }
251 }
252 }
253 let active_drag = *self.active.peek();
254 if active_drag.is_some_and(|active| active.origin == key) {
255 if ctx.dragging() {
256 match active_drag.and_then(|active| active.session) {
257 Some(session) if ctx.is_session(session) => {
258 // A built-in source normally finishes from its own
259 // cleanup before its provider leaves. Never call an
260 // unknown custom source after child teardown.
261 ctx.abandon_session(session, CancelReason::SourceUnmounted);
262 }
263 // An untracked replacement can coexist briefly with the
264 // old source's committed completion. Cancel the closing
265 // window's drag without consuming that unrelated slot.
266 _ => ctx.cancel_with_reason(CancelReason::SourceUnmounted),
267 }
268 self.clear_world_state();
269 return;
270 }
271 if ctx.settling().is_some() {
272 // A settle elected in another window no longer needs the
273 // origin runtime: the release point and origin scale were
274 // snapshotted into world state. Keep them until it lands.
275 match self.settle_presenter() {
276 Some(presenter) if presenter != key => {}
277 Some(_) => {
278 self.finish_settle_from(key);
279 }
280 None => {
281 // Compatibility for a custom source that entered the
282 // context's public settle state without a world claim:
283 // the origin is its only possible presenter.
284 ctx.finish_settle();
285 self.clear_world_state();
286 }
287 }
288 return;
289 }
290 self.clear_world_state();
291 return;
292 }
293 if self.settle_presenter_is(key) {
294 // The elected overlay is gone, so no transition listener remains
295 // to finish the glide. Non-presenter closure is intentionally
296 // inert because this equality fails for every other window.
297 self.finish_settle_from(key);
298 }
299 }
300
301 /// Look up a joined window. `None` for unknown keys.
302 pub fn record(&self, key: WindowKey) -> Option<WindowRecord<T>> {
303 self.windows
304 .try_peek()
305 .ok()?
306 .iter()
307 .find(|w| w.key == key)
308 .copied()
309 }
310
311 /// Every joined window, in join order. Subscribing read, like
312 /// [`ZoneRegistry::records`] - its consumers are renderers and tests.
313 pub fn windows(&self) -> Vec<WindowRecord<T>> {
314 self.windows
315 .try_read()
316 .map(|w| w.to_vec())
317 .unwrap_or_default()
318 }
319
320 /// The window containing `global` (physical px), most recently focused
321 /// first when several overlap. `None` while no live geometry contains
322 /// the point.
323 pub fn window_under(&self, global: Point) -> Option<WindowRecord<T>> {
324 self.windows
325 .try_peek()
326 .ok()?
327 .iter()
328 .filter(|w| w.geometry.contains_global(global))
329 .max_by_key(|w| w.geometry.focus_stamp())
330 .copied()
331 }
332
333 /// Resolve a global point to (window, client-local point). `None` when
334 /// no live window contains it.
335 pub fn resolve_global(&self, global: Point) -> Option<(WindowRecord<T>, Point)> {
336 let rec = self.window_under(global)?;
337 let local = rec.geometry.to_client(global)?;
338 Some((rec, local))
339 }
340
341 /// Ask every joined window to re-measure its zones, each inside its own
342 /// runtime through the window's internal refresh callback.
343 pub fn refresh_all_rects(&self) {
344 let Ok(windows) = self.windows.try_peek() else {
345 return;
346 };
347 for rec in windows.iter() {
348 rec.refresh.call(());
349 }
350 }
351}
352
353impl<T: Clone + 'static> Default for DndWorld<T> {
354 fn default() -> Self {
355 Self::new()
356 }
357}
358
359#[cfg(test)]
360mod tests {
361 use super::*;
362
363 #[test]
364 fn bridging_defaults_on_unless_no_bridge_is_meaningfully_set() {
365 assert!(default_bridging(None));
366 assert!(default_bridging(Some("")));
367 assert!(default_bridging(Some("0")));
368 assert!(!default_bridging(Some("1")));
369 assert!(!default_bridging(Some("true")));
370 }
371}