Skip to main content

dioxus_dnd/core/world/
host.rs

1//! Host-neutral drive operations: entry points for glue that sees the
2//! pointer where webviews cannot. No windowing-toolkit types, no OS
3//! branches - custom (non-Tao) hosts call these too.
4
5use dioxus::prelude::*;
6
7use crate::core::components::{DropCompletion, SettleRoute};
8use crate::core::types::{effective_effect, DragMode, Point, ZoneId};
9
10use super::geometry::WindowKey;
11use super::state::{DndWorld, ZoneLocation};
12
13/// Host-side drive: entry points for desktop glue that sees the pointer
14/// where webviews cannot. Webview pointer events stop at the viewport
15/// edge (and under a pointer grab, every non-origin window is fully
16/// event-blind on all platforms), so cross-window pointer data must come
17/// from the windowing layer: poll the global cursor while a drag is in
18/// flight and feed it here.
19impl<T: Clone + 'static> DndWorld<T> {
20    /// Modifiers currently associated with host delivery. Returns an empty
21    /// set outside an active world drag.
22    pub fn modifiers(&self) -> Modifiers {
23        self.active
24            .read()
25            .as_ref()
26            .map_or_else(Modifiers::empty, |active| active.modifiers)
27    }
28
29    /// Update the live modifiers for the active world drag. Late host events
30    /// after completion are ignored once the context stops dragging.
31    pub fn update_modifiers(&self, modifiers: Modifiers) {
32        if !self.ctx.dragging() {
33            return;
34        }
35        let mut active = self.active;
36        let Some(mut current) = *active.peek() else {
37            return;
38        };
39        if current.modifiers != modifiers {
40            current.modifiers = modifiers;
41            active.set(Some(current));
42        }
43    }
44
45    /// Track an in-flight pointer drag from a host-reported cursor
46    /// position (global physical px): updates the shared pointer (in the
47    /// origin window's client px, the coordinate anchor everything else
48    /// expects) and enters/leaves zones across every joined window. No-op
49    /// when nothing is dragging or the origin window is unknown.
50    ///
51    /// Every host leg converges here, so overlapping legs are safe by
52    /// construction rather than by leg exclusivity:
53    /// - Two legs reporting the same tick are idempotent: every write below
54    ///   is guarded by an equality check, and re-entering the current zone
55    ///   is a no-op.
56    /// - Legs run on one event-loop thread, so ticks serialize; a staler
57    ///   position arriving after a fresher one moves the hover briefly and
58    ///   the next tick corrects it - visual, transient, never structural.
59    /// - A tick landing after a drop cannot resurrect the drag: the
60    ///   `dragging()` gate below is dead after completion, and each leg
61    ///   additionally re-validates its captured `BridgeGeneration`
62    ///   immediately before calling in, so drag N's sleeper cannot feed
63    ///   replacement drag N+1 even during the same event burst.
64    pub fn track_global(&self, global: Point) {
65        // The kill switch gates the world entry point, not just the tao
66        // legs, so a custom host cannot keep cross-window drive alive on a
67        // world whose app disabled bridging (see `set_bridging`).
68        if !self.bridging_enabled() {
69            return;
70        }
71        let mut ctx = self.ctx;
72        if !ctx.dragging() || ctx.mode() != DragMode::Pointer {
73            return;
74        }
75        let Some(origin) = self.active_record() else {
76            return;
77        };
78        let mut global_pointer = self.global_pointer;
79        if *global_pointer.peek() != Some(global) {
80            global_pointer.set(Some(global));
81        }
82        if let Some(local) = origin.geometry.to_client(global) {
83            ctx.update_pointer(local);
84        }
85        let location = self.resolve_global(global).and_then(|(rec, local)| {
86            rec.registry.hit_test(local).map(|zone| ZoneLocation {
87                window: rec.key,
88                zone,
89            })
90        });
91        match location {
92            Some(location) => self.enter_location(location),
93            None => self.clear_hover(),
94        }
95    }
96
97    /// Complete an in-flight pointer drag at a host-reported cursor
98    /// position (global physical px): exact zone hit in whichever window
99    /// contains the point, else that window's 48px snap (in its own CSS
100    /// px), else cancel. Returns the receiving zone. Used by glue that
101    /// detects a release the webviews never saw - e.g. a non-origin
102    /// window receiving its first pointer event mid-"drag", which proves
103    /// the button is up. A no-op returning `None` when nothing is
104    /// dragging, so double delivery (webview pointerup plus host echo)
105    /// is harmless.
106    pub fn drop_at_global(&self, global: Point) -> Option<ZoneId>
107    where
108        T: PartialEq,
109    {
110        // Same kill-switch gate as `track_global`. An in-flight drag is not
111        // stranded: the origin webview still completes in-viewport releases
112        // itself, and out-of-viewport ones reconcile through the same
113        // held-button paths a Wayland session uses.
114        if !self.bridging_enabled() {
115            return None;
116        }
117        let mut ctx = self.ctx;
118        if !ctx.dragging() || ctx.mode() != DragMode::Pointer {
119            return None;
120        }
121        // The release is authoritative even when no final tracking tick ran.
122        self.track_global(global);
123        let session = self.drag_session();
124        let Some((rec, local)) = self.resolve_global(global) else {
125            match session {
126                Some(session) => {
127                    self.finish_session(session, false);
128                }
129                None => self.finish_untracked(false),
130            }
131            return None;
132        };
133        let target = rec.registry.hit_test(local).or_else(|| {
134            ctx.payload()
135                .and_then(|p| rec.registry.hit_test_closest(local, &p, 48.0))
136        });
137        // Imperative host delivery peeks the active snapshot rather than
138        // subscribing the bridge runtime to modifier updates.
139        let modifiers = self
140            .active
141            .peek()
142            .as_ref()
143            .map_or_else(Modifiers::empty, |active| active.modifiers);
144        let effect = effective_effect(ctx.effect(), modifiers);
145        let delivered = target.filter(|t| {
146            crate::core::components::deliver_drop(
147                rec.registry,
148                &mut ctx,
149                SettleRoute {
150                    flag: Some(rec.settle),
151                    owner: Some((self, rec.key)),
152                },
153                DropCompletion::World {
154                    world: self,
155                    session,
156                },
157                *t,
158                local,
159                effect,
160            )
161        });
162        match delivered {
163            Some(zone) => Some(zone),
164            None => {
165                match session {
166                    Some(session) => {
167                        self.finish_session(session, false);
168                    }
169                    None => self.finish_untracked(false),
170                }
171                None
172            }
173        }
174    }
175
176    /// Abort an in-flight drag from the host side (a window manager
177    /// signal, an escape hatch). No-op when nothing is dragging.
178    pub fn cancel_drag(&self) {
179        if let Some(session) = self.drag_session() {
180            self.finish_session(session, false);
181        } else if self.ctx.dragging() {
182            self.finish_untracked(false);
183        }
184    }
185
186    /// The key of the window the in-flight drag started in, if any - glue
187    /// uses it to tell "origin window, webview owns the events" from
188    /// "foreign window, I am the drag's eyes".
189    pub fn origin_window(&self) -> Option<WindowKey> {
190        (self.ctx.dragging() || self.ctx.settling().is_some())
191            .then(|| self.active.peek().as_ref().map(|active| active.origin))
192            .flatten()
193    }
194}