Skip to main content

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}