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