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::components::{drop_query, resolve_drag_hover};
10use super::registry::{RectRefresh, ZoneRecord, ZoneRegistration, ZoneRegistry};
11use super::state::{DndContext, DragState};
12use super::types::{DragId, DropEffect, DropOutcome, Point, Rect, ZoneId};
13use super::world::{
14    use_joined_window, DndWorld, JoinedWindow, WindowGeometry, WorldHit, WorldMembership,
15};
16
17/// Marker flag: a settle-enabled `DragOverlay<T>` is mounted somewhere in
18/// this provider's subtree, so `Draggable<T>` should route successful
19/// pointer drops through [`DndContext::take_settling`] instead of
20/// [`DndContext::take`]. Typed so nested providers of different payloads
21/// can't arm each other.
22pub(crate) struct SettleFlag<T> {
23    armed: Signal<Option<u64>>,
24    marker: std::marker::PhantomData<T>,
25}
26
27impl<T> Copy for SettleFlag<T> {}
28impl<T> Clone for SettleFlag<T> {
29    fn clone(&self) -> Self {
30        *self
31    }
32}
33
34impl<T> SettleFlag<T> {
35    pub(crate) fn arm(self, capability: u64) {
36        let mut armed = self.armed;
37        if let Ok(mut value) = armed.try_write() {
38            if *value != Some(capability) {
39                *value = Some(capability);
40            }
41        };
42    }
43
44    pub(crate) fn is_armed(self) -> bool {
45        matches!(self.armed.try_peek().as_deref(), Ok(Some(_)))
46    }
47
48    /// Retire `capability` only if it still owns this provider's settle
49    /// capability. The return value snapshots that ownership for teardown.
50    pub(crate) fn release(self, capability: u64) -> bool {
51        let mut armed = self.armed;
52        let Ok(mut value) = armed.try_write() else {
53            return false;
54        };
55        if *value != Some(capability) {
56            return false;
57        }
58        *value = None;
59        true
60    }
61}
62
63/// Provide a `DndContext<T>` (and its zone registry) to this component's
64/// subtree. Call once, high up (or use the
65/// [`crate::core::components::DndProvider`] component).
66///
67/// When a [`DndWorld<T>`] is in context (see
68/// [`crate::core::world::use_dnd_world`]), the provider **joins** it
69/// instead of creating isolated state: it re-provides the world's shared
70/// context and registers this window's zones for cross-window drags.
71/// Nested providers of the same `T` keep today's shadowing semantics -
72/// only the outermost provider in a window joins.
73pub fn use_dnd_provider<T: Clone + 'static>() -> DndContext<T> {
74    // Fallback state, created unconditionally (hooks must be stable) and
75    // simply unused when a world is joined.
76    let state = use_store(DragState::<T>::default);
77    let announcement = use_signal(String::new);
78    let registry = use_context_provider(|| ZoneRegistry::<T>::from_signal(Signal::new(Vec::new())));
79    let settle_flag = use_context_provider(|| SettleFlag::<T> {
80        armed: Signal::new(None),
81        marker: std::marker::PhantomData,
82    });
83    // World membership is decided once, at mount: a provider that finds a
84    // world (and isn't nested under a provider of the same T) joins as one
85    // window. `provide_context` inside the hook is deliberate - every
86    // provider publishes a membership (even `None`), so nested providers
87    // shadow their ancestors' membership exactly like they shadow contexts.
88    let membership = use_hook(move || {
89        let joined = try_consume_context::<DndWorld<T>>()
90            .filter(|_| try_consume_context::<WorldMembership<T>>().is_none())
91            .map(|world| {
92                let geometry = try_consume_context::<WindowGeometry>().unwrap_or_default();
93                let key = world.join(
94                    geometry,
95                    registry,
96                    settle_flag,
97                    Callback::new(move |_| registry.refresh_rects()),
98                );
99                JoinedWindow {
100                    world,
101                    key,
102                    geometry,
103                }
104            });
105        provide_context(WorldMembership::<T>(joined));
106        joined
107    });
108    use_drop(move || {
109        if let Some(j) = membership {
110            j.world.leave(j.key);
111        }
112    });
113    let ctx = use_context_provider(move || match membership {
114        Some(j) => j.world.context(),
115        None => DndContext::managed(state, announcement),
116    });
117
118    // One rect-refresh channel per provider *tree*: the outermost provider
119    // creates it, nested providers inherit and re-provide the same one. A
120    // scroll surface anywhere below then reaches every registry above it
121    // through a single type-erased handle.
122    use_rect_refresh_provider();
123    // Re-measure this registry on ping - but only mid-drag. Rects are
124    // measured fresh at every pickup, so an idle provider has nothing to
125    // keep current, and the gate makes scroll-event pings free while idle.
126    use_rect_refresh_thunk(move |_| {
127        if !ctx.dragging() {
128            return;
129        }
130        let drag_id = ctx.drag_id();
131        let session = ctx.drag_session_id();
132        registry.refresh_rects_then(move || {
133            // Receiver callbacks can synchronously finish this drag and
134            // start another while measurements are in flight. Never let an
135            // old batch alter the successor's hover.
136            if !ctx.alive()
137                || !ctx.dragging()
138                || ctx.drag_id() != drag_id
139                || ctx.drag_session_id() != session
140            {
141                return;
142            }
143
144            let proposed = ctx.proposed_effect();
145            let point = membership
146                .and_then(|joined| joined.local_pointer())
147                .unwrap_or_else(|| ctx.pointer());
148            match membership {
149                Some(joined) => {
150                    let query = ctx
151                        .payload()
152                        .map(|payload| drop_query(&ctx, payload, proposed));
153                    match query
154                        .as_ref()
155                        .map(|query| joined.zone_under_query(point, query))
156                        .unwrap_or(WorldHit::Unresolved)
157                    {
158                        WorldHit::Zone(location) => joined.enter(location),
159                        WorldHit::Window => joined.clear_hover(),
160                        WorldHit::Unresolved => {
161                            match resolve_drag_hover(registry, &ctx, point, proposed) {
162                                Some(zone) => joined.enter(joined.location(zone)),
163                                None => joined.clear_hover(),
164                            }
165                        }
166                    }
167                }
168                None => match resolve_drag_hover(registry, &ctx, point, proposed) {
169                    Some(zone) => {
170                        let mut ctx = ctx;
171                        ctx.enter(zone);
172                    }
173                    None => {
174                        if let Some(over) = ctx.over() {
175                            let mut ctx = ctx;
176                            ctx.leave(over);
177                        }
178                    }
179                },
180            }
181        });
182    });
183
184    ctx
185}
186
187/// Create-or-inherit the tree's [`RectRefresh`] channel and provide it to
188/// descendants. The outermost participant (a `DndProvider`, an
189/// [`crate::autoscroll::AutoScroll`]) owns the signal; everyone below
190/// shares it, so self-contained components like `SortableList` can join
191/// even with no provider anywhere.
192pub(crate) fn use_rect_refresh_provider() -> RectRefresh {
193    let bus = use_hook(|| {
194        // Plain context lookup (not the memoizing hook - we're inside one).
195        try_consume_context::<RectRefresh>()
196            .unwrap_or_else(|| RectRefresh::from_signal(Signal::new(Vec::new())))
197    });
198    use_context_provider(|| bus);
199    bus
200}
201
202/// Register a re-measure thunk on the tree's channel for this component's
203/// lifetime; it leaves the channel on unmount. Quietly does nothing when no
204/// channel exists above (nothing could ever ping it). The thunk must gate
205/// itself on its own drag state - pings arrive for every scroll.
206pub(crate) fn use_rect_refresh_thunk(thunk: impl FnMut(()) + 'static) {
207    let joined = use_hook(move || {
208        try_consume_context::<RectRefresh>().map(|mut bus| {
209            let key = DragId::auto().0;
210            bus.register(key, Callback::new(thunk));
211            (bus, key)
212        })
213    });
214    use_drop(move || {
215        if let Some((mut bus, key)) = joined {
216            bus.unregister(key);
217        }
218    });
219}
220
221/// The provider tree's [`RectRefresh`] channel: ping `refresh_all()` after
222/// you move layout under a live drag (scrolling a custom container,
223/// collapsing a panel) so hit-testing and `data-over` track the new
224/// geometry. [`crate::autoscroll::AutoScroll`] pings it for you.
225///
226/// # Panics
227/// Panics if no ancestor provided a drag context.
228pub fn use_rect_refresh() -> RectRefresh {
229    use_context()
230}
231
232/// Grab the nearest `DndContext<T>` from context.
233///
234/// # Panics
235/// Panics if no ancestor provided a context for this payload type.
236pub fn use_dnd<T: Clone + 'static>() -> DndContext<T> {
237    use_context()
238}
239
240/// Grab the zone registry (mounted drop zones, in order). Provided alongside
241/// the context by [`use_dnd_provider`].
242pub fn use_zone_registry<T: Clone + 'static>() -> ZoneRegistry<T> {
243    use_context()
244}
245
246/// A stable, auto-generated [`ZoneId`] for this component instance.
247pub fn use_zone_id() -> ZoneId {
248    use_hook(ZoneId::auto)
249}
250
251/// A plain, component-owned fan-out for one bridge element's geometry.
252///
253/// Create one with [`Default`] and pass a clone to every [`use_bridge_world`]
254/// call for the element. Its mount and rect methods copy one DOM observation
255/// into every joined provider-owned registry without creating Dioxus signals
256/// or callbacks in the child scope.
257#[derive(Clone, Default)]
258pub struct BridgeGeometry {
259    state: Rc<RefCell<BridgeGeometryState>>,
260}
261
262#[derive(Default)]
263struct BridgeGeometryState {
264    next_writer: u64,
265    writers: Vec<BridgeGeometryWriter>,
266    mounted: Option<Rc<MountedData>>,
267    rect: Option<Rect>,
268}
269
270#[derive(Clone)]
271struct BridgeGeometryWriter {
272    id: u64,
273    mounted: Rc<dyn Fn(Rc<MountedData>)>,
274    rect: Rc<dyn Fn(Rect)>,
275}
276
277struct BridgeGeometryRegistration {
278    state: Rc<RefCell<BridgeGeometryState>>,
279    id: u64,
280}
281
282impl Drop for BridgeGeometryRegistration {
283    fn drop(&mut self) {
284        self.state
285            .borrow_mut()
286            .writers
287            .retain(|writer| writer.id != self.id);
288    }
289}
290
291impl std::fmt::Debug for BridgeGeometry {
292    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
293        f.debug_struct("BridgeGeometry")
294            .field("worlds", &self.state.borrow().writers.len())
295            .finish()
296    }
297}
298
299impl PartialEq for BridgeGeometry {
300    fn eq(&self, other: &Self) -> bool {
301        Rc::ptr_eq(&self.state, &other.state)
302    }
303}
304
305impl BridgeGeometry {
306    /// Copy the bridge element's mounted handle into every joined registry.
307    pub fn set_mounted(&self, mounted: &Rc<MountedData>) {
308        let writers = {
309            let mut state = self.state.borrow_mut();
310            state.mounted = Some(mounted.clone());
311            state.writers.clone()
312        };
313        for writer in writers {
314            (writer.mounted)(mounted.clone());
315        }
316    }
317
318    /// Copy a completed bridge measurement into every registration that is
319    /// still current.
320    pub fn set_rect_if_present(&self, rect: Rect) {
321        let writers = {
322            let mut state = self.state.borrow_mut();
323            state.rect = Some(rect);
324            state.writers.clone()
325        };
326        for writer in writers {
327            (writer.rect)(rect);
328        }
329    }
330
331    fn register<T: Clone + 'static>(
332        &self,
333        registry: ZoneRegistry<T>,
334        registration: ZoneRegistration,
335    ) -> BridgeGeometryRegistration {
336        let mut state = self.state.borrow_mut();
337        let id = state.next_writer;
338        state.next_writer = state.next_writer.wrapping_add(1);
339        let writer = BridgeGeometryWriter {
340            id,
341            mounted: Rc::new(move |mounted| {
342                let mut registry = registry;
343                registry.set_mounted(registration, mounted);
344            }),
345            rect: Rc::new(move |rect| {
346                let mut registry = registry;
347                registry.set_rect_if_present(registration, rect);
348            }),
349        };
350        let mounted = state.mounted.clone();
351        let rect = state.rect;
352        state.writers.push(writer.clone());
353        drop(state);
354        if let Some(mounted) = mounted {
355            (writer.mounted)(mounted);
356        }
357        if let Some(rect) = rect {
358            (writer.rect)(rect);
359        }
360        BridgeGeometryRegistration {
361            state: self.state.clone(),
362            id,
363        }
364    }
365}
366
367/// Live, type-erased view of one payload world at a bridge zone, as returned
368/// by [`use_bridge_world`] - so callers can OR any number of worlds together
369/// without naming their `T`s again.
370#[derive(Debug, Clone, Copy, PartialEq, Eq)]
371pub struct BridgeWorld {
372    /// An acceptable drag of this world's payload is in flight.
373    pub active: bool,
374    /// That drag currently hovers this zone.
375    pub over: bool,
376}
377
378/// Register `zone_id` as a drop target in `T`'s payload world and report
379/// that world's live state this render.
380///
381/// This is the building block behind `BridgeDropZone` and the
382/// [`crate::bridge_drop_zone!`] macro: call it once per coexisting provider
383/// type with the same id and [`BridgeGeometry`]. Every registry owns its own
384/// plain geometry copy, while each drop still arrives through its own typed
385/// callback - no downcasts, no shared erased channel.
386///
387/// # Panics
388/// Panics if no ancestor provided a `DndProvider<T>`.
389pub fn use_bridge_world<T: Clone + PartialEq + 'static>(
390    zone_id: ZoneId,
391    parent: Option<ZoneId>,
392    label: Option<String>,
393    accepts: Option<Callback<T, bool>>,
394    on_drop: EventHandler<DropOutcome<T>>,
395    geometry: BridgeGeometry,
396) -> BridgeWorld {
397    let dnd = use_dnd::<T>();
398    let joined = use_joined_window::<T>();
399    let mut reg = use_zone_registry::<T>();
400    let on_drop = use_callback(move |outcome| on_drop.call(outcome));
401    // Register one stable callback whose Dioxus callback slot is refreshed
402    // with the current optional policy every render. Delivery and styling
403    // therefore consult the same policy without waiting for a post-render
404    // registry synchronization pass.
405    let registered_accepts = use_callback(move |payload| {
406        accepts
407            .map(|callback| callback.call(payload))
408            .unwrap_or(true)
409    });
410    let initial_label = label.clone();
411    let initial_geometry = geometry.clone();
412    let registrations = use_hook(move || {
413        let registration = reg.register(ZoneRecord {
414            id: zone_id,
415            parent,
416            label: initial_label,
417            on_drop,
418            accepts: Some(registered_accepts),
419            mounted: None,
420            rect: None,
421        });
422        let writer = initial_geometry.register(reg, registration);
423        Rc::new(RefCell::new(Some((zone_id, parent, registration, writer))))
424    });
425    let effect_registrations = registrations.clone();
426    let effect_geometry = geometry.clone();
427    use_effect(use_reactive!(|(zone_id, parent, label)| {
428        let unchanged = effect_registrations.borrow().as_ref().is_some_and(
429            |(current_id, current_parent, ..)| *current_id == zone_id && *current_parent == parent,
430        );
431        if unchanged {
432            reg.sync_label(zone_id, label);
433            return;
434        }
435
436        if let Some((_, _, old_registration, old_writer)) = effect_registrations.borrow_mut().take()
437        {
438            reg.unregister_registration(old_registration);
439            drop(old_writer);
440        }
441        let registration = reg.register(ZoneRecord {
442            id: zone_id,
443            parent,
444            label,
445            on_drop,
446            accepts: Some(registered_accepts),
447            mounted: None,
448            rect: None,
449        });
450        let writer = effect_geometry.register(reg, registration);
451        *effect_registrations.borrow_mut() = Some((zone_id, parent, registration, writer));
452    }));
453    use_drop(move || {
454        if let Some((_, _, registration, writer)) = registrations.borrow_mut().take() {
455            reg.unregister_registration(registration);
456            drop(writer);
457        }
458    });
459
460    let acceptable = dnd.proposed_effect() != DropEffect::None
461        && match dnd.payload() {
462            Some(p) => accepts.map(|cb| cb.call(p)).unwrap_or(true),
463            None => false,
464        };
465    BridgeWorld {
466        active: dnd.dragging() && acceptable,
467        over: match joined {
468            Some(joined) => joined.is_over(zone_id),
469            None => dnd.over() == Some(zone_id),
470        } && acceptable,
471    }
472}
473
474/// Client (viewport) coordinates of a native drag event as a [`Point`].
475/// In-app drags don't produce `DragEvent`s; this serves the boundary
476/// modules ([`crate::files`], [`crate::external`]) and custom native zones.
477pub fn client_point(evt: &DragEvent) -> Point {
478    let c = evt.client_coordinates();
479    Point::new(c.x, c.y)
480}
481
482/// Element-relative coordinates of a native drag event as a [`Point`].
483/// See [`client_point`] for when these apply.
484pub fn element_point(evt: &DragEvent) -> Point {
485    let c = evt.element_coordinates();
486    Point::new(c.x, c.y)
487}