dioxus_dnd/core/state.rs
1//! The shared drag state. One `DndContext<T>` lives in Dioxus context and is
2//! read/written by `Draggable` and `DropZone` components (and by you, if you
3//! wire events manually).
4//!
5//! Payloads travel through this Rust-side store - not through the browser's
6//! `DataTransfer` - so they can be any `Clone` type with zero serialization.
7//! (`DataTransfer` interop for external drags lives in [`crate::external`].)
8//!
9//! State is held in a [`struct@Store`], Dioxus 0.7's fine-grained reactivity
10//! primitive: each field gets its own lazy subscription. A component that
11//! reads `dnd.over()` in its render only reruns when the hovered zone
12//! changes - not on every pointer move.
13
14use dioxus::prelude::*;
15
16use super::session::SourceCompletion;
17use super::types::{DragMode, DropEffect, Point, PointerKind, Rect, ZoneId};
18
19/// A snapshot of an in-flight drag.
20///
21/// Deriving [`macro@Store`] generates per-field lenses, which
22/// [`DndContext`]'s accessors use for granular subscriptions.
23#[derive(Store, Debug, Clone, PartialEq)]
24pub struct DragState<T: 'static> {
25 /// The payload currently being dragged, if any.
26 pub payload: Option<T>,
27 /// Zone the drag started from.
28 pub source: Option<ZoneId>,
29 /// Zone the pointer is currently over.
30 pub over: Option<ZoneId>,
31 /// Last known pointer position (client coordinates).
32 pub pointer: Point,
33 /// Where inside the dragged element the user grabbed it.
34 pub grab: Point,
35 /// Effect requested by the draggable.
36 pub effect: DropEffect,
37 /// How this drag is being driven (pointer vs keyboard).
38 pub mode: DragMode,
39 /// Which pointer device drives a pointer drag (mouse/touch/pen).
40 /// Meaningful only while `mode` is [`DragMode::Pointer`]; host-side
41 /// glue reads it to bridge exactly the input layers the device
42 /// needs (see [`PointerKind`]). `Draggable` records it at pickup;
43 /// custom sources that never do get the safe `Mouse` default.
44 pub pointer_kind: PointerKind,
45 /// Client rect of the dragged element, measured at pickup. Feeds
46 /// size-matched ghosts (`DragOverlay { match_source: true }`); `None`
47 /// until the async measurement lands or when a custom source never set
48 /// it.
49 pub source_rect: Option<Rect>,
50 /// Payload of a just-completed keyboard drop, awaiting focus
51 /// restoration: the drop re-mounts the moved item at its landing place
52 /// and the browser dumps focus on `<body>` when the source element
53 /// unmounts, so the matching `Draggable` claims this on mount and
54 /// focuses itself - keyboard users keep their place. Cleared by the
55 /// claim or by the next drag starting.
56 pub refocus: Option<T>,
57 /// Destination rect of a just-completed drop whose overlay is still
58 /// gliding home (the drop-settle animation). While set, `dragging()` is
59 /// false but `payload` stays readable so the ghost keeps its content.
60 pub settle: Option<Rect>,
61}
62
63impl<T> Default for DragState<T> {
64 fn default() -> Self {
65 Self {
66 payload: None,
67 source: None,
68 over: None,
69 pointer: Point::default(),
70 grab: Point::default(),
71 effect: DropEffect::default(),
72 mode: DragMode::default(),
73 pointer_kind: PointerKind::default(),
74 source_rect: None,
75 refocus: None,
76 settle: None,
77 }
78 }
79}
80
81/// Handle to the shared drag state. Cheap to copy - it's just a store key.
82pub struct DndContext<T: Clone + 'static> {
83 state: Store<DragState<T>>,
84 /// Screen-reader announcement channel, rendered by
85 /// [`crate::a11y::LiveRegion`].
86 announcement: Signal<String>,
87 /// Origin-runtime callback for the current pointer gesture. Kept
88 /// outside `DragState` so consuming a payload cannot lose the source
89 /// lifecycle that still needs to be completed.
90 pub(super) completion: Signal<Option<SourceCompletion>>,
91}
92
93// Manual impls: `derive` would add unnecessary `T: Copy` / `T: PartialEq`
94// bounds, but the handle is just a store key plus a signal key.
95impl<T: Clone + 'static> Copy for DndContext<T> {}
96impl<T: Clone + 'static> Clone for DndContext<T> {
97 fn clone(&self) -> Self {
98 *self
99 }
100}
101impl<T: Clone + 'static> PartialEq for DndContext<T> {
102 fn eq(&self, other: &Self) -> bool {
103 self.announcement == other.announcement
104 }
105}
106
107impl<T: Clone + 'static> DndContext<T> {
108 /// Wrap existing state. Prefer [`crate::core::hooks::use_dnd_provider`].
109 pub fn from_parts(state: Store<DragState<T>>, announcement: Signal<String>) -> Self {
110 Self {
111 state,
112 announcement,
113 completion: Signal::new(None),
114 }
115 }
116
117 /// Begin a drag. Notifies all fields (state transition).
118 pub fn start(
119 &mut self,
120 payload: T,
121 source: Option<ZoneId>,
122 pointer: Point,
123 grab: Point,
124 effect: DropEffect,
125 mode: DragMode,
126 ) {
127 self.state.set(DragState {
128 payload: Some(payload),
129 source,
130 over: None,
131 pointer,
132 grab,
133 effect,
134 mode,
135 // The safe default; the drag source refines it right after
136 // this call via `set_pointer_kind` (as `Draggable` does).
137 pointer_kind: PointerKind::default(),
138 // Measured (async) by the drag source right after this call.
139 source_rect: None,
140 // A new drag supersedes any unclaimed focus restoration.
141 refocus: None,
142 // Starting a new drag interrupts any settle still gliding.
143 settle: None,
144 });
145 }
146
147 /// Record which pointer device drives the current drag (see
148 /// [`DragState::pointer_kind`]). `Draggable` sets this right after
149 /// pickup from the initiating event's `pointerType`; call it from
150 /// custom pointer sources so host-side glue (cursor pollers, raw
151 /// input bridges) can tell captured pointers from blind ones. Left
152 /// alone, every drag reads as `Mouse`.
153 pub fn set_pointer_kind(&mut self, kind: PointerKind) {
154 self.state.pointer_kind().set(kind);
155 }
156
157 /// Record that `payload` just landed via a keyboard drop and its new
158 /// element should take focus when it mounts (see
159 /// [`DragState::refocus`]). `Draggable` calls this on its own keyboard
160 /// drops; call it from custom keyboard sources to get the same focus
161 /// continuity.
162 pub fn request_refocus(&mut self, payload: T) {
163 self.state.refocus().set(Some(payload));
164 }
165
166 /// Claim a pending focus restoration if it matches `payload`; returns
167 /// whether the caller should focus itself. First matching claimant
168 /// wins - the request is consumed.
169 pub fn claim_refocus(&mut self, payload: &T) -> bool
170 where
171 T: PartialEq,
172 {
173 let mut refocus = self.state.refocus();
174 let hit = refocus.peek().as_ref() == Some(payload);
175 if hit {
176 refocus.set(None);
177 }
178 hit
179 }
180
181 /// Record the dragged element's client rect (see
182 /// [`DragState::source_rect`]). `Draggable` measures and sets this right
183 /// after pickup; call it from custom drag sources so size-matched ghosts
184 /// (`DragOverlay { match_source: true }`) can dress themselves.
185 pub fn set_source_rect(&mut self, rect: Option<Rect>) {
186 self.state.source_rect().set(rect);
187 }
188
189 /// Update the tracked pointer position (drives `DragOverlay`). Granular:
190 /// only `pointer` subscribers rerun.
191 pub fn update_pointer(&mut self, pointer: Point) {
192 // An exact (0,0) is overwhelmingly a bogus platform report (some
193 // webviews emit it for synthetic events), not a real drag at the
194 // viewport corner; ignore it so the overlay doesn't jump there.
195 if pointer.x == 0.0 && pointer.y == 0.0 {
196 return;
197 }
198 self.state.pointer().set(pointer);
199 }
200
201 /// Mark `zone` as hovered. Granular: only `over` subscribers rerun.
202 pub fn enter(&mut self, zone: ZoneId) {
203 self.state.over().set(Some(zone));
204 }
205
206 /// Clear hover, but only if `zone` is still the hovered one (avoids
207 /// enter/leave races between adjacent zones).
208 pub fn leave(&mut self, zone: ZoneId) {
209 let mut over = self.state.over();
210 if *over.peek() == Some(zone) {
211 over.set(None);
212 }
213 }
214
215 /// Consume the payload on a successful drop. Returns `(payload, source)`.
216 /// After this, `dragging()` is false.
217 pub fn take(&mut self) -> Option<(T, Option<ZoneId>)> {
218 let (payload, source) = {
219 let mut s = self.state.write();
220 (s.payload.take(), s.source)
221 };
222 let payload = payload?;
223 self.state.set(DragState::default());
224 Some((payload, source))
225 }
226
227 /// Consume the payload on a successful drop, like [`Self::take`], but
228 /// enter the *settling* phase instead of resetting: the returned clone
229 /// goes to the drop handler while the stored payload stays readable and
230 /// `settle` records the destination rect, so a settle-enabled
231 /// [`crate::core::components::DragOverlay`] can glide the ghost home.
232 /// After this, `dragging()` is false and `over()` is cleared; call
233 /// [`Self::finish_settle`] (the overlay does) to reset fully.
234 ///
235 /// Custom sources in a joined [`crate::core::world::DndWorld`] must call
236 /// [`crate::core::world::DndWorld::claim_settle`] first: world overlays
237 /// only present and finish a settle for the elected window.
238 pub fn take_settling(&mut self, to: Rect) -> Option<(T, Option<ZoneId>)> {
239 let mut s = self.state.write();
240 let payload = s.payload.clone()?;
241 let source = s.source;
242 s.over = None;
243 s.settle = Some(to);
244 Some((payload, source))
245 }
246
247 /// Re-aim an in-flight settle at a better rect - typically the landed
248 /// element's own, measured after the drop re-rendered the model
249 /// (`SettleSlot` does this for you). The overlay's glide retargets
250 /// smoothly, mid-flight included. A no-op unless currently settling.
251 pub fn retarget_settle(&mut self, to: Rect) {
252 let mut settle = self.state.settle();
253 // The equality guard is load-bearing: a `SettleSlot` retargets from
254 // an effect that (via its render) subscribes to `settle`, and
255 // signal writes notify even when the value is unchanged - writing
256 // the same rect back would loop effect -> write -> effect forever.
257 if settle.peek().is_some() && *settle.peek() != Some(to) {
258 settle.set(Some(to));
259 }
260 }
261
262 /// End the settling phase and reset all state. A no-op unless currently
263 /// settling, so a late `transitionend` can never clobber a new drag.
264 pub fn finish_settle(&mut self) {
265 if self.state.settle().peek().is_some() {
266 self.state.set(DragState::default());
267 }
268 }
269
270 /// Is the underlying state still alive? Destructors check this before
271 /// touching the context, because store lens access on a dead store
272 /// panics (even `try_` reads - the selector internals do) and a panic
273 /// in a destructor aborts the process. A world context is process-
274 /// lived so this holds by construction there; the gate keeps every
275 /// other wiring (custom `from_parts` contexts, unforeseen drop orders)
276 /// degrading gracefully instead. Probed through the announcement
277 /// signal, a plain `Signal` created alongside the store, whose
278 /// `try_peek` IS dead-safe.
279 pub(crate) fn alive(&self) -> bool {
280 self.announcement.try_peek().is_ok()
281 }
282
283 /// Abort the drag and reset all state.
284 pub fn cancel(&mut self) {
285 self.state.set(DragState::default());
286 }
287
288 // --- read accessors -----------------------------------------------
289 // Each reads through a field lens, so render-time reads subscribe only
290 // to that field.
291
292 /// Is a drag currently in flight? False while a completed drop is still
293 /// settling, even though [`Self::payload`] remains readable.
294 pub fn dragging(&self) -> bool {
295 self.state.payload().is_some() && self.state.settle().is_none()
296 }
297
298 /// Destination rect of a drop currently settling (see
299 /// [`Self::take_settling`]), if any.
300 pub fn settling(&self) -> Option<Rect> {
301 self.state.settle().cloned()
302 }
303
304 /// Non-subscribing version of [`Self::settling`] for imperative world
305 /// bookkeeping (destructors, event handlers) that must not subscribe.
306 pub(crate) fn settling_peek(&self) -> bool {
307 self.state.settle().peek().is_some()
308 }
309
310 /// Clone of the current payload, if dragging.
311 pub fn payload(&self) -> Option<T> {
312 self.state.payload().cloned()
313 }
314
315 /// Zone currently hovered.
316 pub fn over(&self) -> Option<ZoneId> {
317 self.state.over().cloned()
318 }
319
320 /// Zone the drag started from.
321 pub fn source(&self) -> Option<ZoneId> {
322 self.state.source().cloned()
323 }
324
325 /// Last known pointer position.
326 pub fn pointer(&self) -> Point {
327 self.state.pointer().cloned()
328 }
329
330 /// Grab offset inside the dragged element.
331 pub fn grab(&self) -> Point {
332 self.state.grab().cloned()
333 }
334
335 /// Client rect of the dragged element measured at pickup, if available.
336 pub fn source_rect(&self) -> Option<Rect> {
337 self.state.source_rect().cloned()
338 }
339
340 /// Effect the drag was started with.
341 pub fn effect(&self) -> DropEffect {
342 self.state.effect().cloned()
343 }
344
345 /// How the current drag is being driven.
346 pub fn mode(&self) -> DragMode {
347 self.state.mode().cloned()
348 }
349
350 /// Which pointer device drives the current drag (meaningful for
351 /// [`DragMode::Pointer`] drags; `Mouse` otherwise and by default).
352 pub fn pointer_kind(&self) -> PointerKind {
353 self.state.pointer_kind().cloned()
354 }
355
356 /// Push a screen-reader announcement (rendered by
357 /// [`crate::a11y::LiveRegion`]). Called automatically by the built-in
358 /// keyboard interaction; call it yourself for custom flows.
359 pub fn announce(&mut self, msg: impl Into<String>) {
360 self.announcement.set(msg.into());
361 }
362
363 /// The current announcement text.
364 pub fn announcement(&self) -> String {
365 self.announcement.read().clone()
366 }
367}