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}