Skip to main content

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}