Skip to main content

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}