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