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}