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.8'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::types::{DragMode, DropEffect, Point, Rect, ZoneId};
17
18/// A snapshot of an in-flight drag.
19///
20/// Deriving [`macro@Store`] generates per-field lenses, which
21/// [`DndContext`]'s accessors use for granular subscriptions.
22#[derive(Store, Debug, Clone, PartialEq)]
23pub struct DragState<T: 'static> {
24 /// The payload currently being dragged, if any.
25 pub payload: Option<T>,
26 /// Zone the drag started from.
27 pub source: Option<ZoneId>,
28 /// Zone the pointer is currently over.
29 pub over: Option<ZoneId>,
30 /// Last known pointer position (client coordinates).
31 pub pointer: Point,
32 /// Where inside the dragged element the user grabbed it.
33 pub grab: Point,
34 /// Effect requested by the draggable.
35 pub effect: DropEffect,
36 /// How this drag is being driven (pointer vs keyboard).
37 pub mode: DragMode,
38 /// Destination rect of a just-completed drop whose overlay is still
39 /// gliding home (the drop-settle animation). While set, `dragging()` is
40 /// false but `payload` stays readable so the ghost keeps its content.
41 pub settle: Option<Rect>,
42}
43
44impl<T> Default for DragState<T> {
45 fn default() -> Self {
46 Self {
47 payload: None,
48 source: None,
49 over: None,
50 pointer: Point::default(),
51 grab: Point::default(),
52 effect: DropEffect::default(),
53 mode: DragMode::default(),
54 settle: None,
55 }
56 }
57}
58
59/// Handle to the shared drag state. Cheap to copy - it's just a store key.
60pub struct DndContext<T: Clone + 'static> {
61 state: Store<DragState<T>>,
62 /// Screen-reader announcement channel, rendered by
63 /// [`crate::a11y::LiveRegion`].
64 announcement: Signal<String>,
65}
66
67// Manual impls: `derive` would add unnecessary `T: Copy` / `T: PartialEq`
68// bounds, but the handle is just a store key plus a signal key.
69impl<T: Clone + 'static> Copy for DndContext<T> {}
70impl<T: Clone + 'static> Clone for DndContext<T> {
71 fn clone(&self) -> Self {
72 *self
73 }
74}
75impl<T: Clone + 'static> PartialEq for DndContext<T> {
76 fn eq(&self, other: &Self) -> bool {
77 self.announcement == other.announcement
78 }
79}
80
81impl<T: Clone + 'static> DndContext<T> {
82 /// Wrap existing state. Prefer [`crate::core::hooks::use_dnd_provider`].
83 pub fn from_parts(state: Store<DragState<T>>, announcement: Signal<String>) -> Self {
84 Self {
85 state,
86 announcement,
87 }
88 }
89
90 /// Begin a drag. Notifies all fields (state transition).
91 pub fn start(
92 &mut self,
93 payload: T,
94 source: Option<ZoneId>,
95 pointer: Point,
96 grab: Point,
97 effect: DropEffect,
98 mode: DragMode,
99 ) {
100 self.state.set(DragState {
101 payload: Some(payload),
102 source,
103 over: None,
104 pointer,
105 grab,
106 effect,
107 mode,
108 // Starting a new drag interrupts any settle still gliding.
109 settle: None,
110 });
111 }
112
113 /// Update the tracked pointer position (drives `DragOverlay`). Granular:
114 /// only `pointer` subscribers rerun.
115 pub fn update_pointer(&mut self, pointer: Point) {
116 // An exact (0,0) is overwhelmingly a bogus platform report (some
117 // webviews emit it for synthetic events), not a real drag at the
118 // viewport corner; ignore it so the overlay doesn't jump there.
119 if pointer.x == 0.0 && pointer.y == 0.0 {
120 return;
121 }
122 self.state.pointer().set(pointer);
123 }
124
125 /// Mark `zone` as hovered. Granular: only `over` subscribers rerun.
126 pub fn enter(&mut self, zone: ZoneId) {
127 self.state.over().set(Some(zone));
128 }
129
130 /// Clear hover, but only if `zone` is still the hovered one (avoids
131 /// enter/leave races between adjacent zones).
132 pub fn leave(&mut self, zone: ZoneId) {
133 let mut over = self.state.over();
134 if *over.peek() == Some(zone) {
135 over.set(None);
136 }
137 }
138
139 /// Consume the payload on a successful drop. Returns `(payload, source)`.
140 /// After this, `dragging()` is false.
141 pub fn take(&mut self) -> Option<(T, Option<ZoneId>)> {
142 let (payload, source) = {
143 let mut s = self.state.write();
144 (s.payload.take(), s.source)
145 };
146 let payload = payload?;
147 self.state.set(DragState::default());
148 Some((payload, source))
149 }
150
151 /// Consume the payload on a successful drop, like [`Self::take`], but
152 /// enter the *settling* phase instead of resetting: the returned clone
153 /// goes to the drop handler while the stored payload stays readable and
154 /// `settle` records the destination rect, so a settle-enabled
155 /// [`crate::core::components::DragOverlay`] can glide the ghost home.
156 /// After this, `dragging()` is false and `over()` is cleared; call
157 /// [`Self::finish_settle`] (the overlay does) to reset fully.
158 pub fn take_settling(&mut self, to: Rect) -> Option<(T, Option<ZoneId>)> {
159 let mut s = self.state.write();
160 let payload = s.payload.clone()?;
161 let source = s.source;
162 s.over = None;
163 s.settle = Some(to);
164 Some((payload, source))
165 }
166
167 /// End the settling phase and reset all state. A no-op unless currently
168 /// settling, so a late `transitionend` can never clobber a new drag.
169 pub fn finish_settle(&mut self) {
170 if self.state.settle().peek().is_some() {
171 self.state.set(DragState::default());
172 }
173 }
174
175 /// Abort the drag and reset all state.
176 pub fn cancel(&mut self) {
177 self.state.set(DragState::default());
178 }
179
180 // --- read accessors -----------------------------------------------
181 // Each reads through a field lens, so render-time reads subscribe only
182 // to that field.
183
184 /// Is a drag currently in flight? False while a completed drop is still
185 /// settling, even though [`Self::payload`] remains readable.
186 pub fn dragging(&self) -> bool {
187 self.state.payload().is_some() && self.state.settle().is_none()
188 }
189
190 /// Destination rect of a drop currently settling (see
191 /// [`Self::take_settling`]), if any.
192 pub fn settling(&self) -> Option<Rect> {
193 self.state.settle().cloned()
194 }
195
196 /// Clone of the current payload, if dragging.
197 pub fn payload(&self) -> Option<T> {
198 self.state.payload().cloned()
199 }
200
201 /// Zone currently hovered.
202 pub fn over(&self) -> Option<ZoneId> {
203 self.state.over().cloned()
204 }
205
206 /// Zone the drag started from.
207 pub fn source(&self) -> Option<ZoneId> {
208 self.state.source().cloned()
209 }
210
211 /// Last known pointer position.
212 pub fn pointer(&self) -> Point {
213 self.state.pointer().cloned()
214 }
215
216 /// Grab offset inside the dragged element.
217 pub fn grab(&self) -> Point {
218 self.state.grab().cloned()
219 }
220
221 /// Effect the drag was started with.
222 pub fn effect(&self) -> DropEffect {
223 self.state.effect().cloned()
224 }
225
226 /// How the current drag is being driven.
227 pub fn mode(&self) -> DragMode {
228 self.state.mode().cloned()
229 }
230
231 /// Push a screen-reader announcement (rendered by
232 /// [`crate::a11y::LiveRegion`]). Called automatically by the built-in
233 /// keyboard interaction; call it yourself for custom flows.
234 pub fn announce(&mut self, msg: impl Into<String>) {
235 self.announcement.set(msg.into());
236 }
237
238 /// The current announcement text.
239 pub fn announcement(&self) -> String {
240 self.announcement.read().clone()
241 }
242}