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, 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}
39
40impl<T> Default for DragState<T> {
41    fn default() -> Self {
42        Self {
43            payload: None,
44            source: None,
45            over: None,
46            pointer: Point::default(),
47            grab: Point::default(),
48            effect: DropEffect::default(),
49            mode: DragMode::default(),
50        }
51    }
52}
53
54/// Handle to the shared drag state. Cheap to copy - it's just a store key.
55pub struct DndContext<T: Clone + 'static> {
56    state: Store<DragState<T>>,
57    /// Screen-reader announcement channel, rendered by
58    /// [`crate::a11y::LiveRegion`].
59    announcement: Signal<String>,
60}
61
62// Manual impls: `derive` would add unnecessary `T: Copy` / `T: PartialEq`
63// bounds, but the handle is just a store key plus a signal key.
64impl<T: Clone + 'static> Copy for DndContext<T> {}
65impl<T: Clone + 'static> Clone for DndContext<T> {
66    fn clone(&self) -> Self {
67        *self
68    }
69}
70impl<T: Clone + 'static> PartialEq for DndContext<T> {
71    fn eq(&self, other: &Self) -> bool {
72        self.announcement == other.announcement
73    }
74}
75
76impl<T: Clone + 'static> DndContext<T> {
77    /// Wrap existing state. Prefer [`crate::core::hooks::use_dnd_provider`].
78    pub fn from_parts(state: Store<DragState<T>>, announcement: Signal<String>) -> Self {
79        Self {
80            state,
81            announcement,
82        }
83    }
84
85    /// Begin a drag. Notifies all fields (state transition).
86    pub fn start(
87        &mut self,
88        payload: T,
89        source: Option<ZoneId>,
90        pointer: Point,
91        grab: Point,
92        effect: DropEffect,
93        mode: DragMode,
94    ) {
95        self.state.set(DragState {
96            payload: Some(payload),
97            source,
98            over: None,
99            pointer,
100            grab,
101            effect,
102            mode,
103        });
104    }
105
106    /// Update the tracked pointer position (drives `DragOverlay`). Granular:
107    /// only `pointer` subscribers rerun.
108    pub fn update_pointer(&mut self, pointer: Point) {
109        // An exact (0,0) is overwhelmingly a bogus platform report (some
110        // webviews emit it for synthetic events), not a real drag at the
111        // viewport corner; ignore it so the overlay doesn't jump there.
112        if pointer.x == 0.0 && pointer.y == 0.0 {
113            return;
114        }
115        self.state.pointer().set(pointer);
116    }
117
118    /// Mark `zone` as hovered. Granular: only `over` subscribers rerun.
119    pub fn enter(&mut self, zone: ZoneId) {
120        self.state.over().set(Some(zone));
121    }
122
123    /// Clear hover, but only if `zone` is still the hovered one (avoids
124    /// enter/leave races between adjacent zones).
125    pub fn leave(&mut self, zone: ZoneId) {
126        let mut over = self.state.over();
127        if *over.peek() == Some(zone) {
128            over.set(None);
129        }
130    }
131
132    /// Consume the payload on a successful drop. Returns `(payload, source)`.
133    /// After this, `dragging()` is false.
134    pub fn take(&mut self) -> Option<(T, Option<ZoneId>)> {
135        let (payload, source) = {
136            let mut s = self.state.write();
137            (s.payload.take(), s.source)
138        };
139        let payload = payload?;
140        self.state.set(DragState::default());
141        Some((payload, source))
142    }
143
144    /// Abort the drag and reset all state.
145    pub fn cancel(&mut self) {
146        self.state.set(DragState::default());
147    }
148
149    // --- read accessors -----------------------------------------------
150    // Each reads through a field lens, so render-time reads subscribe only
151    // to that field.
152
153    /// Is a drag currently in flight?
154    pub fn dragging(&self) -> bool {
155        self.state.payload().is_some()
156    }
157
158    /// Clone of the current payload, if dragging.
159    pub fn payload(&self) -> Option<T> {
160        self.state.payload().cloned()
161    }
162
163    /// Zone currently hovered.
164    pub fn over(&self) -> Option<ZoneId> {
165        self.state.over().cloned()
166    }
167
168    /// Zone the drag started from.
169    pub fn source(&self) -> Option<ZoneId> {
170        self.state.source().cloned()
171    }
172
173    /// Last known pointer position.
174    pub fn pointer(&self) -> Point {
175        self.state.pointer().cloned()
176    }
177
178    /// Grab offset inside the dragged element.
179    pub fn grab(&self) -> Point {
180        self.state.grab().cloned()
181    }
182
183    /// Effect the drag was started with.
184    pub fn effect(&self) -> DropEffect {
185        self.state.effect().cloned()
186    }
187
188    /// How the current drag is being driven.
189    pub fn mode(&self) -> DragMode {
190        self.state.mode().cloned()
191    }
192
193    /// Push a screen-reader announcement (rendered by
194    /// [`crate::a11y::LiveRegion`]). Called automatically by the built-in
195    /// keyboard interaction; call it yourself for custom flows.
196    pub fn announce(&mut self, msg: impl Into<String>) {
197        self.announcement.set(msg.into());
198    }
199
200    /// The current announcement text.
201    pub fn announcement(&self) -> String {
202        self.announcement.read().clone()
203    }
204}