dioxus_dnd/core/hooks.rs
1//! Hooks for providing and consuming the drag context.
2
3use std::cell::RefCell;
4use std::rc::Rc;
5
6use dioxus::html::MountedData;
7use dioxus::prelude::*;
8
9use super::registry::{RectRefresh, ZoneRecord, ZoneRegistration, ZoneRegistry};
10use super::state::{DndContext, DragState};
11use super::types::{DragId, DropOutcome, Point, Rect, ZoneId};
12use super::world::{use_joined_window, DndWorld, JoinedWindow, WindowGeometry, WorldMembership};
13
14/// Marker flag: a settle-enabled `DragOverlay<T>` is mounted somewhere in
15/// this provider's subtree, so `Draggable<T>` should route successful
16/// pointer drops through [`DndContext::take_settling`] instead of
17/// [`DndContext::take`]. Typed so nested providers of different payloads
18/// can't arm each other.
19pub(crate) struct SettleFlag<T> {
20 armed: Signal<Option<u64>>,
21 marker: std::marker::PhantomData<T>,
22}
23
24impl<T> Copy for SettleFlag<T> {}
25impl<T> Clone for SettleFlag<T> {
26 fn clone(&self) -> Self {
27 *self
28 }
29}
30
31impl<T> SettleFlag<T> {
32 pub(crate) fn arm(self, capability: u64) {
33 let mut armed = self.armed;
34 if let Ok(mut value) = armed.try_write() {
35 if *value != Some(capability) {
36 *value = Some(capability);
37 }
38 };
39 }
40
41 pub(crate) fn is_armed(self) -> bool {
42 matches!(self.armed.try_peek().as_deref(), Ok(Some(_)))
43 }
44
45 /// Retire `capability` only if it still owns this provider's settle
46 /// capability. The return value snapshots that ownership for teardown.
47 pub(crate) fn release(self, capability: u64) -> bool {
48 let mut armed = self.armed;
49 let Ok(mut value) = armed.try_write() else {
50 return false;
51 };
52 if *value != Some(capability) {
53 return false;
54 }
55 *value = None;
56 true
57 }
58}
59
60/// Provide a `DndContext<T>` (and its zone registry) to this component's
61/// subtree. Call once, high up (or use the
62/// [`crate::core::components::DndProvider`] component).
63///
64/// When a [`DndWorld<T>`] is in context (see
65/// [`crate::core::world::use_dnd_world`]), the provider **joins** it
66/// instead of creating isolated state: it re-provides the world's shared
67/// context and registers this window's zones for cross-window drags.
68/// Nested providers of the same `T` keep today's shadowing semantics -
69/// only the outermost provider in a window joins.
70pub fn use_dnd_provider<T: Clone + 'static>() -> DndContext<T> {
71 // Fallback state, created unconditionally (hooks must be stable) and
72 // simply unused when a world is joined.
73 let state = use_store(DragState::<T>::default);
74 let announcement = use_signal(String::new);
75 let registry = use_context_provider(|| ZoneRegistry::<T>::from_signal(Signal::new(Vec::new())));
76 let settle_flag = use_context_provider(|| SettleFlag::<T> {
77 armed: Signal::new(None),
78 marker: std::marker::PhantomData,
79 });
80 // World membership is decided once, at mount: a provider that finds a
81 // world (and isn't nested under a provider of the same T) joins as one
82 // window. `provide_context` inside the hook is deliberate - every
83 // provider publishes a membership (even `None`), so nested providers
84 // shadow their ancestors' membership exactly like they shadow contexts.
85 let membership = use_hook(move || {
86 let joined = try_consume_context::<DndWorld<T>>()
87 .filter(|_| try_consume_context::<WorldMembership<T>>().is_none())
88 .map(|world| {
89 let geometry = try_consume_context::<WindowGeometry>().unwrap_or_default();
90 let key = world.join(
91 geometry,
92 registry,
93 settle_flag,
94 Callback::new(move |_| registry.refresh_rects()),
95 );
96 JoinedWindow {
97 world,
98 key,
99 geometry,
100 }
101 });
102 provide_context(WorldMembership::<T>(joined));
103 joined
104 });
105 use_drop(move || {
106 if let Some(j) = membership {
107 j.world.leave(j.key);
108 }
109 });
110 let ctx = use_context_provider(move || match membership {
111 Some(j) => j.world.context(),
112 None => DndContext::from_parts(state, announcement),
113 });
114
115 // One rect-refresh channel per provider *tree*: the outermost provider
116 // creates it, nested providers inherit and re-provide the same one. A
117 // scroll surface anywhere below then reaches every registry above it
118 // through a single type-erased handle.
119 use_rect_refresh_provider();
120 // Re-measure this registry on ping - but only mid-drag. Rects are
121 // measured fresh at every pickup, so an idle provider has nothing to
122 // keep current, and the gate makes scroll-event pings free while idle.
123 use_rect_refresh_thunk(move |_| {
124 if ctx.dragging() {
125 registry.refresh_rects();
126 }
127 });
128
129 ctx
130}
131
132/// Create-or-inherit the tree's [`RectRefresh`] channel and provide it to
133/// descendants. The outermost participant (a `DndProvider`, an
134/// [`crate::autoscroll::AutoScroll`]) owns the signal; everyone below
135/// shares it, so self-contained components like `SortableList` can join
136/// even with no provider anywhere.
137pub(crate) fn use_rect_refresh_provider() -> RectRefresh {
138 let bus = use_hook(|| {
139 // Plain context lookup (not the memoizing hook - we're inside one).
140 try_consume_context::<RectRefresh>()
141 .unwrap_or_else(|| RectRefresh::from_signal(Signal::new(Vec::new())))
142 });
143 use_context_provider(|| bus);
144 bus
145}
146
147/// Register a re-measure thunk on the tree's channel for this component's
148/// lifetime; it leaves the channel on unmount. Quietly does nothing when no
149/// channel exists above (nothing could ever ping it). The thunk must gate
150/// itself on its own drag state - pings arrive for every scroll.
151pub(crate) fn use_rect_refresh_thunk(thunk: impl FnMut(()) + 'static) {
152 let joined = use_hook(move || {
153 try_consume_context::<RectRefresh>().map(|mut bus| {
154 let key = DragId::auto().0;
155 bus.register(key, Callback::new(thunk));
156 (bus, key)
157 })
158 });
159 use_drop(move || {
160 if let Some((mut bus, key)) = joined {
161 bus.unregister(key);
162 }
163 });
164}
165
166/// The provider tree's [`RectRefresh`] channel: ping `refresh_all()` after
167/// you move layout under a live drag (scrolling a custom container,
168/// collapsing a panel) so hit-testing and `data-over` track the new
169/// geometry. [`crate::autoscroll::AutoScroll`] pings it for you.
170///
171/// # Panics
172/// Panics if no ancestor provided a drag context.
173pub fn use_rect_refresh() -> RectRefresh {
174 use_context()
175}
176
177/// Grab the nearest `DndContext<T>` from context.
178///
179/// # Panics
180/// Panics if no ancestor provided a context for this payload type.
181pub fn use_dnd<T: Clone + 'static>() -> DndContext<T> {
182 use_context()
183}
184
185/// Grab the zone registry (mounted drop zones, in order). Provided alongside
186/// the context by [`use_dnd_provider`].
187pub fn use_zone_registry<T: Clone + 'static>() -> ZoneRegistry<T> {
188 use_context()
189}
190
191/// A stable, auto-generated [`ZoneId`] for this component instance.
192pub fn use_zone_id() -> ZoneId {
193 use_hook(ZoneId::auto)
194}
195
196/// A plain, component-owned fan-out for one bridge element's geometry.
197///
198/// Create one with [`Default`] and pass a clone to every [`use_bridge_world`]
199/// call for the element. Its mount and rect methods copy one DOM observation
200/// into every joined provider-owned registry without creating Dioxus signals
201/// or callbacks in the child scope.
202#[derive(Clone, Default)]
203pub struct BridgeGeometry {
204 writers: Rc<RefCell<Vec<BridgeGeometryWriter>>>,
205}
206
207#[derive(Clone)]
208struct BridgeGeometryWriter {
209 mounted: Rc<dyn Fn(Rc<MountedData>)>,
210 rect: Rc<dyn Fn(Rect)>,
211}
212
213impl std::fmt::Debug for BridgeGeometry {
214 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
215 f.debug_struct("BridgeGeometry")
216 .field("worlds", &self.writers.borrow().len())
217 .finish()
218 }
219}
220
221impl PartialEq for BridgeGeometry {
222 fn eq(&self, other: &Self) -> bool {
223 Rc::ptr_eq(&self.writers, &other.writers)
224 }
225}
226
227impl BridgeGeometry {
228 /// Copy the bridge element's mounted handle into every joined registry.
229 pub fn set_mounted(&self, mounted: &Rc<MountedData>) {
230 for writer in self.writers.borrow().iter() {
231 (writer.mounted)(mounted.clone());
232 }
233 }
234
235 /// Copy a completed bridge measurement into every registration that is
236 /// still current.
237 pub fn set_rect_if_present(&self, rect: Rect) {
238 for writer in self.writers.borrow().iter() {
239 (writer.rect)(rect);
240 }
241 }
242
243 fn register<T: Clone + 'static>(
244 &self,
245 registry: ZoneRegistry<T>,
246 registration: ZoneRegistration,
247 ) {
248 self.writers.borrow_mut().push(BridgeGeometryWriter {
249 mounted: Rc::new(move |mounted| {
250 let mut registry = registry;
251 registry.set_mounted(registration, mounted);
252 }),
253 rect: Rc::new(move |rect| {
254 let mut registry = registry;
255 registry.set_rect_if_present(registration, rect);
256 }),
257 });
258 }
259}
260
261/// Live, type-erased view of one payload world at a bridge zone, as returned
262/// by [`use_bridge_world`] - so callers can OR any number of worlds together
263/// without naming their `T`s again.
264#[derive(Debug, Clone, Copy, PartialEq, Eq)]
265pub struct BridgeWorld {
266 /// An acceptable drag of this world's payload is in flight.
267 pub active: bool,
268 /// That drag currently hovers this zone.
269 pub over: bool,
270}
271
272/// Register `zone_id` as a drop target in `T`'s payload world and report
273/// that world's live state this render.
274///
275/// This is the building block behind `BridgeDropZone` and the
276/// [`crate::bridge_drop_zone!`] macro: call it once per coexisting provider
277/// type with the same id and [`BridgeGeometry`]. Every registry owns its own
278/// plain geometry copy, while each drop still arrives through its own typed
279/// callback - no downcasts, no shared erased channel.
280///
281/// # Panics
282/// Panics if no ancestor provided a `DndProvider<T>`.
283pub fn use_bridge_world<T: Clone + PartialEq + 'static>(
284 zone_id: ZoneId,
285 parent: Option<ZoneId>,
286 label: Option<String>,
287 accepts: Option<Callback<T, bool>>,
288 on_drop: EventHandler<DropOutcome<T>>,
289 geometry: BridgeGeometry,
290) -> BridgeWorld {
291 let dnd = use_dnd::<T>();
292 let joined = use_joined_window::<T>();
293 let mut reg = use_zone_registry::<T>();
294 let registration = use_hook(|| {
295 reg.register(ZoneRecord {
296 id: zone_id,
297 parent,
298 label: label.clone(),
299 on_drop: Callback::new(move |o| on_drop.call(o)),
300 accepts,
301 mounted: None,
302 rect: None,
303 })
304 });
305 use_drop(move || reg.unregister(zone_id));
306 reg.sync_label(zone_id, label);
307 use_hook(move || {
308 geometry.register(reg, registration);
309 });
310
311 let acceptable = match dnd.payload() {
312 Some(p) => accepts.map(|cb| cb.call(p)).unwrap_or(true),
313 None => false,
314 };
315 BridgeWorld {
316 active: dnd.dragging() && acceptable,
317 over: match joined {
318 Some(joined) => joined.is_over(zone_id),
319 None => dnd.over() == Some(zone_id),
320 } && acceptable,
321 }
322}
323
324/// Client (viewport) coordinates of a native drag event as a [`Point`].
325/// In-app drags don't produce `DragEvent`s; this serves the boundary
326/// modules ([`crate::files`], [`crate::external`]) and custom native zones.
327pub fn client_point(evt: &DragEvent) -> Point {
328 let c = evt.client_coordinates();
329 Point::new(c.x, c.y)
330}
331
332/// Element-relative coordinates of a native drag event as a [`Point`].
333/// See [`client_point`] for when these apply.
334pub fn element_point(evt: &DragEvent) -> Point {
335 let c = evt.element_coordinates();
336 Point::new(c.x, c.y)
337}