Skip to main content

dioxus_dnd/core/
registry.rs

1//! The zone registry: every mounted [`crate::core::DropZone`] records itself
2//! here (id, label, drop callback, acceptance filter, and its mounted DOM
3//! handle). Pointer drags hit-test against cached client rects; keyboard
4//! navigation walks the zones in spatial order (top-to-bottom, left-to-right,
5//! with unmeasured zones last in registration order).
6
7use std::rc::Rc;
8use std::sync::atomic::{AtomicU64, Ordering};
9
10use dioxus::html::MountedData;
11use dioxus::prelude::*;
12
13use super::types::{Direction, DropOutcome, Point, Rect, ZoneId};
14
15static NEXT_ZONE_REGISTRATION: AtomicU64 = AtomicU64::new(1);
16
17/// Identifies one particular registration of a [`ZoneId`].
18///
19/// A zone id can be replaced in place. Async measurements carry this token
20/// so a result started for the old registration cannot land in its
21/// same-id replacement.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23pub struct ZoneRegistration {
24    id: ZoneId,
25    generation: u64,
26}
27
28/// One registered drop zone.
29pub struct ZoneRecord<T: Clone + 'static> {
30    pub id: ZoneId,
31    /// The enclosing zone, when this zone is nested inside another
32    /// `DropZone` (discovered automatically via context).
33    pub parent: Option<ZoneId>,
34    /// Human label used in screen-reader announcements.
35    pub label: Option<String>,
36    /// Delivers a completed drop to the zone's owner.
37    pub on_drop: Callback<DropOutcome<T>>,
38    /// The zone's acceptance filter, if any.
39    pub accepts: Option<Callback<T, bool>>,
40    /// The zone's mounted element, once available. This plain value lives in
41    /// the provider-owned registry storage; zones update it through
42    /// [`ZoneRegistry::set_mounted`].
43    pub mounted: Option<Rc<MountedData>>,
44    /// Cached client rect (refreshed via [`ZoneRegistry::refresh_rects`]).
45    /// This plain value lives in the provider-owned registry storage; zones
46    /// update it through [`ZoneRegistry::set_rect_if_present`].
47    pub rect: Option<Rect>,
48}
49
50impl<T: Clone + 'static> Clone for ZoneRecord<T> {
51    fn clone(&self) -> Self {
52        Self {
53            id: self.id,
54            parent: self.parent,
55            label: self.label.clone(),
56            on_drop: self.on_drop,
57            accepts: self.accepts,
58            mounted: self.mounted.clone(),
59            rect: self.rect,
60        }
61    }
62}
63
64impl<T: Clone + 'static> ZoneRecord<T> {
65    /// Does this zone accept the payload?
66    pub fn accepts_payload(&self, payload: &T) -> bool {
67        match self.accepts {
68            Some(cb) => cb.call(payload.clone()),
69            None => true,
70        }
71    }
72
73    /// The cached client rect in this registry snapshot.
74    pub fn cached_rect(&self) -> Option<Rect> {
75        self.rect
76    }
77
78    /// The mounted element in this registry snapshot.
79    pub fn mounted_handle(&self) -> Option<Rc<MountedData>> {
80        self.mounted.clone()
81    }
82}
83
84/// Registry of the currently mounted drop zones, in mount order.
85pub struct ZoneRegistry<T: Clone + 'static> {
86    zones: Signal<Vec<ZoneRecord<T>>>,
87    /// Current generation for each id in `zones`. Kept separately so
88    /// `ZoneRecord` remains constructible with a public struct literal.
89    registrations: Signal<Vec<(ZoneId, u64)>>,
90    /// Changes only when the zone set or a mounted handle changes. The debug
91    /// overlay subscribes here so rect writes cannot retrigger measurement.
92    mount_revision: Signal<u64>,
93    /// Layout direction for spatial ordering (keyboard navigation).
94    dir: Signal<Direction>,
95}
96
97impl<T: Clone + 'static> Copy for ZoneRegistry<T> {}
98impl<T: Clone + 'static> Clone for ZoneRegistry<T> {
99    fn clone(&self) -> Self {
100        *self
101    }
102}
103impl<T: Clone + 'static> PartialEq for ZoneRegistry<T> {
104    fn eq(&self, other: &Self) -> bool {
105        self.zones == other.zones && self.dir == other.dir
106    }
107}
108
109impl<T: Clone + 'static> ZoneRegistry<T> {
110    /// Wrap an existing signal. Prefer [`crate::core::hooks::use_dnd_provider`].
111    pub fn from_signal(zones: Signal<Vec<ZoneRecord<T>>>) -> Self {
112        Self {
113            zones,
114            registrations: Signal::new(Vec::new()),
115            mount_revision: Signal::new(0),
116            dir: Signal::new(Direction::default()),
117        }
118    }
119
120    /// Layout direction spatial ordering follows.
121    pub fn direction(&self) -> Direction {
122        self.dir.try_peek().map(|dir| *dir).unwrap_or_default()
123    }
124
125    /// Set the layout direction (no-op if unchanged; safe to call every
126    /// render). `DndProvider`'s `dir` prop calls this for you.
127    pub fn set_direction(&mut self, dir: Direction) {
128        let changed = self.dir.try_peek().map(|current| *current != dir);
129        if changed == Ok(true) {
130            if let Ok(mut current) = self.dir.try_write() {
131                *current = dir;
132            }
133        }
134    }
135
136    /// Add (or replace, by id) a zone.
137    pub fn register(&mut self, record: ZoneRecord<T>) -> ZoneRegistration {
138        let registration = ZoneRegistration {
139            id: record.id,
140            generation: NEXT_ZONE_REGISTRATION.fetch_add(1, Ordering::Relaxed),
141        };
142        if let Ok(mut zones) = self.zones.try_write() {
143            if let Some(existing) = zones.iter_mut().find(|z| z.id == record.id) {
144                *existing = record;
145            } else {
146                zones.push(record);
147            }
148        }
149        if let Ok(mut registrations) = self.registrations.try_write() {
150            if let Some(existing) = registrations
151                .iter_mut()
152                .find(|(id, _)| *id == registration.id)
153            {
154                existing.1 = registration.generation;
155            } else {
156                registrations.push((registration.id, registration.generation));
157            }
158        }
159        self.bump_mount_revision();
160        registration
161    }
162
163    /// Update a zone's label in place (no-op if unchanged or unknown).
164    pub fn sync_label(&mut self, id: ZoneId, label: Option<String>) {
165        let needs = self
166            .zones
167            .try_peek()
168            .map(|zones| zones.iter().any(|z| z.id == id && z.label != label))
169            .unwrap_or(false);
170        if needs {
171            if let Ok(mut zones) = self.zones.try_write() {
172                if let Some(z) = zones.iter_mut().find(|z| z.id == id) {
173                    z.label = label;
174                }
175            }
176        }
177    }
178
179    /// Remove a zone (call when its component unmounts).
180    pub fn unregister(&mut self, id: ZoneId) {
181        let removed = self.zones.try_write().is_ok_and(|mut zones| {
182            let old_len = zones.len();
183            zones.retain(|z| z.id != id);
184            zones.len() != old_len
185        });
186        if let Ok(mut registrations) = self.registrations.try_write() {
187            registrations.retain(|(registered_id, _)| *registered_id != id);
188        }
189        if removed {
190            self.bump_mount_revision();
191        }
192    }
193
194    /// Attach the mounted element to this exact registration. A stale
195    /// registration token is ignored.
196    pub fn set_mounted(&mut self, registration: ZoneRegistration, mounted: Rc<MountedData>) {
197        if !self.is_current(registration) {
198            return;
199        }
200        let mut changed = false;
201        if let Ok(mut zones) = self.zones.try_write() {
202            if let Some(zone) = zones.iter_mut().find(|z| z.id == registration.id) {
203                zone.mounted = Some(mounted);
204                changed = true;
205            }
206        }
207        if changed {
208            self.bump_mount_revision();
209        }
210    }
211
212    /// Store a rect only while the registration that requested it is still
213    /// current. This never inserts a missing zone and therefore cannot
214    /// resurrect one that unmounted during an async measurement.
215    pub fn set_rect_if_present(&mut self, registration: ZoneRegistration, rect: Rect) {
216        if !self.is_current(registration) {
217            return;
218        }
219        if let Ok(mut zones) = self.zones.try_write() {
220            if let Some(zone) = zones.iter_mut().find(|z| z.id == registration.id) {
221                zone.rect = Some(rect);
222            }
223        }
224    }
225
226    /// Set geometry for the current registration of `id`. This is the
227    /// synchronous/manual counterpart to [`Self::set_rect_if_present`], used
228    /// by custom layout adapters and the headless test driver.
229    pub fn set_rect(&mut self, id: ZoneId, rect: Rect) {
230        if let Some(registration) = self.current_registration(id) {
231            self.set_rect_if_present(registration, rect);
232        }
233    }
234
235    /// Look up a zone by id.
236    pub fn get(&self, id: ZoneId) -> Option<ZoneRecord<T>> {
237        self.zones
238            .try_peek()
239            .ok()?
240            .iter()
241            .find(|z| z.id == id)
242            .cloned()
243    }
244
245    /// The zone's cached client rect, read without subscribing. Returns
246    /// `None` when unmeasured, unknown, or the provider is already gone.
247    pub fn cached_rect(&self, id: ZoneId) -> Option<Rect> {
248        self.zones
249            .try_peek()
250            .ok()?
251            .iter()
252            .find(|z| z.id == id)
253            .and_then(ZoneRecord::cached_rect)
254    }
255
256    /// The zone's mounted element, read without subscribing. Returns `None`
257    /// before mount, for an unknown zone, or after provider teardown.
258    pub fn mounted_handle(&self, id: ZoneId) -> Option<Rc<MountedData>> {
259        self.zones
260            .try_peek()
261            .ok()?
262            .iter()
263            .find(|z| z.id == id)
264            .and_then(ZoneRecord::mounted_handle)
265    }
266
267    /// Every registered zone, in registration order. Unlike the peeking
268    /// lookups around it this is a *subscribing* read - a component
269    /// rendering from it re-renders when zones mount or unmount - because
270    /// its consumers (the debug overlay, your own devtools) are renderers.
271    pub fn records(&self) -> Vec<ZoneRecord<T>> {
272        self.zones
273            .try_read()
274            .map(|zones| zones.to_vec())
275            .unwrap_or_default()
276    }
277
278    /// Is a zone with this id registered *here*? The parent-zone context is
279    /// shared across payload types, so a record's `parent` can name a zone
280    /// living in another type's registry - check before navigating to one.
281    pub fn contains(&self, id: ZoneId) -> bool {
282        self.zones
283            .try_peek()
284            .is_ok_and(|zones| zones.iter().any(|z| z.id == id))
285    }
286
287    /// The zone keyboard navigation should enter when ascending from
288    /// `current`: its parent, but only when that parent is registered in
289    /// this registry. A `DropZone<A>` nested inside a `DropZone<B>` records
290    /// B's id as its parent, and entering an id this registry can't resolve
291    /// would leave the drag hovering a zone that can never receive it.
292    pub fn ascend(&self, current: ZoneId) -> Option<ZoneId> {
293        self.parent_of(current).filter(|pid| self.contains(*pid))
294    }
295
296    /// All zones accepting `payload`, in registration order.
297    pub fn acceptable(&self, payload: &T) -> Vec<ZoneRecord<T>> {
298        self.zones
299            .try_peek()
300            .map(|zones| {
301                zones
302                    .iter()
303                    .filter(|z| z.accepts_payload(payload))
304                    .cloned()
305                    .collect()
306            })
307            .unwrap_or_default()
308    }
309
310    /// The next/previous zone (cyclic) relative to `current` among zones that
311    /// accept `payload`. `step` is `+1` or `-1`.
312    ///
313    /// Order is **spatial** (top-to-bottom, then left-to-right) for zones
314    /// with measured rects - call [`Self::refresh_rects`] first, as the
315    /// built-in keyboard interaction does on pickup. Unmeasured zones keep
316    /// registration order, after the measured ones.
317    pub fn step_zone(&self, current: Option<ZoneId>, payload: &T, step: isize) -> Option<ZoneId> {
318        let mut zones = self.acceptable(payload);
319        spatial_sort(&mut zones, self.direction());
320        let current_ix = current.and_then(|c| zones.iter().position(|z| z.id == c));
321        cycle(zones.len(), current_ix, step).map(|ix| zones[ix].id)
322    }
323
324    /// The parent of a zone, if it's nested.
325    pub fn parent_of(&self, id: ZoneId) -> Option<ZoneId> {
326        self.zones
327            .try_peek()
328            .ok()?
329            .iter()
330            .find(|z| z.id == id)?
331            .parent
332    }
333
334    /// Zones directly inside `parent` (`None` = root level) that accept
335    /// `payload`, in spatial order (top-to-bottom, left-to-right; unmeasured
336    /// zones keep registration order at the end).
337    pub fn children_of(&self, parent: Option<ZoneId>, payload: &T) -> Vec<ZoneRecord<T>> {
338        let mut zones: Vec<_> = self
339            .zones
340            .try_peek()
341            .map(|zones| {
342                zones
343                    .iter()
344                    .filter(|z| z.parent == parent && z.accepts_payload(payload))
345                    .cloned()
346                    .collect()
347            })
348            .unwrap_or_default();
349        spatial_sort(&mut zones, self.direction());
350        zones
351    }
352
353    /// Next/previous zone (cyclic) among the *siblings* of `current` -
354    /// zones sharing its parent. With no `current`, cycles the root level.
355    pub fn step_sibling(
356        &self,
357        current: Option<ZoneId>,
358        payload: &T,
359        step: isize,
360    ) -> Option<ZoneId> {
361        let parent = current.and_then(|c| self.parent_of(c));
362        let siblings = self.children_of(parent, payload);
363        let current_ix = current.and_then(|c| siblings.iter().position(|z| z.id == c));
364        cycle(siblings.len(), current_ix, step).map(|ix| siblings[ix].id)
365    }
366
367    /// The first (spatially) acceptable zone nested inside `id`.
368    pub fn first_child(&self, id: ZoneId, payload: &T) -> Option<ZoneId> {
369        self.children_of(Some(id), payload).first().map(|z| z.id)
370    }
371
372    /// Topmost zone containing `point` (client coordinates), using cached
373    /// rects - call [`Self::refresh_rects`] when a drag starts. Later-mounted
374    /// zones win, approximating DOM paint order.
375    pub fn hit_test(&self, point: Point) -> Option<ZoneId> {
376        self.zones
377            .try_peek()
378            .ok()?
379            .iter()
380            .rev()
381            .find(|z| z.cached_rect().map(|r| r.contains(point)).unwrap_or(false))
382            .map(|z| z.id)
383    }
384
385    /// Like [`Self::hit_test`], but acceptance-aware: it returns the topmost
386    /// zone that both contains the point **and** accepts `payload`, and when
387    /// no such zone contains the point, falls back to the acceptable zone
388    /// whose *rect* is nearest - within `max_distance` CSS px of its closest
389    /// edge, not its center, so a large zone snaps a release right beside it
390    /// even though its center sits far away. Skipping zones that reject the
391    /// payload lets a drop land on an accepting zone sitting *under* a
392    /// rejecting (or decorative) one, and is friendlier for imprecise
393    /// (touch) drops that land in the gutter between zones.
394    pub fn hit_test_closest(&self, point: Point, payload: &T, max_distance: f64) -> Option<ZoneId> {
395        if let Some(hit) = self
396            .zones
397            .try_peek()
398            .ok()?
399            .iter()
400            .rev()
401            .find(|z| {
402                z.accepts_payload(payload)
403                    && z.cached_rect().map(|r| r.contains(point)).unwrap_or(false)
404            })
405            .map(|z| z.id)
406        {
407            return Some(hit);
408        }
409        let mut best: Option<(ZoneId, f64)> = None;
410        for z in self.acceptable(payload) {
411            let Some(r) = z.cached_rect() else { continue };
412            // Distance to the rect's nearest point (zero on either axis the
413            // point already overlaps), not to its center.
414            let dx = (r.x - point.x).max(point.x - (r.x + r.width)).max(0.0);
415            let dy = (r.y - point.y).max(point.y - (r.y + r.height)).max(0.0);
416            let d = (dx * dx + dy * dy).sqrt();
417            if d <= max_distance && best.map(|(_, bd)| d < bd).unwrap_or(true) {
418                best = Some((z.id, d));
419            }
420        }
421        best.map(|(id, _)| id)
422    }
423
424    /// Re-measure every mounted zone's client rect and **wait** for the
425    /// measurements to land - unlike [`Self::refresh_rects`], which fires
426    /// and forgets. Use before a hit-test that must see fresh geometry
427    /// (e.g. retrying a missed touch drop after a layout change).
428    pub async fn measure_all(&self) {
429        let zones = self.measurement_targets();
430        for (registration, mounted) in zones {
431            if let Ok(r) = mounted.get_client_rect().await {
432                // The zone can unmount or be replaced during the await (a
433                // closing window mid-drag is the common case). The
434                // generation check quietly drops that stale measurement.
435                let mut registry = *self;
436                registry.set_rect_if_present(
437                    registration,
438                    Rect::new(r.origin.x, r.origin.y, r.size.width, r.size.height),
439                );
440            }
441        }
442    }
443
444    /// Re-measure every mounted zone's client rect (async, spawned).
445    pub fn refresh_rects(&self) {
446        for (registration, mounted) in self.measurement_targets() {
447            let mut registry = *self;
448            spawn(async move {
449                if let Ok(r) = mounted.get_client_rect().await {
450                    // See measure_all: the zone can die or be replaced
451                    // while this measurement is in flight.
452                    registry.set_rect_if_present(
453                        registration,
454                        Rect::new(r.origin.x, r.origin.y, r.size.width, r.size.height),
455                    );
456                }
457            });
458        }
459    }
460
461    /// Subscribe an effect to registration/mount changes without also
462    /// subscribing it to rect writes in the main registry vector.
463    pub(crate) fn track_mounts(&self) {
464        let _ = self.mount_revision.try_read();
465    }
466
467    fn measurement_targets(&self) -> Vec<(ZoneRegistration, Rc<MountedData>)> {
468        let registrations = self
469            .registrations
470            .try_peek()
471            .map(|registrations| registrations.clone())
472            .unwrap_or_default();
473        self.zones
474            .try_peek()
475            .map(|zones| {
476                zones
477                    .iter()
478                    .filter_map(|zone| {
479                        let mounted = zone.mounted_handle()?;
480                        let generation = registrations
481                            .iter()
482                            .find(|(id, _)| *id == zone.id)
483                            .map(|(_, generation)| *generation)?;
484                        Some((
485                            ZoneRegistration {
486                                id: zone.id,
487                                generation,
488                            },
489                            mounted,
490                        ))
491                    })
492                    .collect()
493            })
494            .unwrap_or_default()
495    }
496
497    fn is_current(&self, registration: ZoneRegistration) -> bool {
498        self.registrations.try_peek().is_ok_and(|registrations| {
499            registrations.iter().any(|(id, generation)| {
500                *id == registration.id && *generation == registration.generation
501            })
502        })
503    }
504
505    fn current_registration(&self, id: ZoneId) -> Option<ZoneRegistration> {
506        self.registrations
507            .try_peek()
508            .ok()?
509            .iter()
510            .find(|(registered_id, _)| *registered_id == id)
511            .map(|(_, generation)| ZoneRegistration {
512                id,
513                generation: *generation,
514            })
515    }
516
517    fn bump_mount_revision(&mut self) {
518        if let Ok(mut revision) = self.mount_revision.try_write() {
519            *revision = revision.wrapping_add(1);
520        }
521    }
522}
523
524/// A payload-type-erased "re-measure your zones" channel, shared by every
525/// registry under one provider tree.
526///
527/// Cached client rects go stale the moment layout moves under a live drag -
528/// scrolling being the everyday case. Registries are per payload type, but
529/// the things that move layout (an auto-scrolling container, your own
530/// scroll surface, a collapsing panel) shouldn't need to know any payload
531/// type to say "geometry changed". Each provider registers a thunk here
532/// that re-measures its own registry **only while it has a drag in
533/// flight**, so pinging the channel from every scroll event costs nothing
534/// while idle.
535///
536/// [`crate::autoscroll::AutoScroll`] pings this automatically after every
537/// scroll it performs (and on any other scroll of its container); grab the
538/// channel with [`crate::core::hooks::use_rect_refresh`] to wire up custom
539/// layout mutators.
540pub struct RectRefresh {
541    thunks: Signal<Vec<(u64, Callback<()>)>>,
542}
543
544impl Copy for RectRefresh {}
545impl Clone for RectRefresh {
546    fn clone(&self) -> Self {
547        *self
548    }
549}
550impl PartialEq for RectRefresh {
551    fn eq(&self, other: &Self) -> bool {
552        self.thunks == other.thunks
553    }
554}
555
556impl RectRefresh {
557    /// Wrap an existing signal. Prefer [`crate::core::hooks::use_dnd_provider`],
558    /// which creates one per provider *tree* (nested providers inherit and
559    /// re-provide the outermost channel).
560    pub fn from_signal(thunks: Signal<Vec<(u64, Callback<()>)>>) -> Self {
561        Self { thunks }
562    }
563
564    /// Ask every provider in the tree to re-measure its zones. Providers
565    /// without a drag in flight ignore the ping, so this is safe to call
566    /// from high-frequency sources like scroll events.
567    pub fn refresh_all(&self) {
568        for (_, thunk) in self.thunks.peek().iter() {
569            thunk.call(());
570        }
571    }
572
573    /// Number of registered providers. Diagnostics and tests.
574    pub fn len(&self) -> usize {
575        self.thunks.peek().len()
576    }
577
578    /// Whether any provider is registered.
579    pub fn is_empty(&self) -> bool {
580        self.len() == 0
581    }
582
583    /// Add (or replace, by key) a provider's re-measure thunk.
584    pub(crate) fn register(&mut self, key: u64, thunk: Callback<()>) {
585        let mut thunks = self.thunks.write();
586        if let Some(existing) = thunks.iter_mut().find(|(k, _)| *k == key) {
587            existing.1 = thunk;
588        } else {
589            thunks.push((key, thunk));
590        }
591    }
592
593    /// Remove a provider's thunk (call when the provider unmounts).
594    pub(crate) fn unregister(&mut self, key: u64) {
595        self.thunks.write().retain(|(k, _)| *k != key);
596    }
597}
598
599/// Sort zones spatially: measured rects by (top, reading order), unmeasured
600/// last in their original relative order. Reading order within a row is
601/// left-to-right in LTR and right-to-left in RTL, so keyboard traversal
602/// follows what the user sees either way.
603fn spatial_sort<T: Clone + 'static>(zones: &mut [ZoneRecord<T>], dir: Direction) {
604    let reading_x = move |x: f64| match dir {
605        Direction::Ltr => x,
606        Direction::Rtl => -x,
607    };
608    zones.sort_by(|a, b| match (a.cached_rect(), b.cached_rect()) {
609        (Some(ra), Some(rb)) => (ra.y, reading_x(ra.x))
610            .partial_cmp(&(rb.y, reading_x(rb.x)))
611            .unwrap_or(std::cmp::Ordering::Equal),
612        (Some(_), None) => std::cmp::Ordering::Less,
613        (None, Some(_)) => std::cmp::Ordering::Greater,
614        (None, None) => std::cmp::Ordering::Equal,
615    });
616}
617
618/// Cyclic index stepping: `None` current starts at the first (or last)
619/// element depending on direction. Pure, for testability.
620pub(crate) fn cycle(len: usize, current: Option<usize>, step: isize) -> Option<usize> {
621    if len == 0 {
622        return None;
623    }
624    Some(match current {
625        None => {
626            if step >= 0 {
627                0
628            } else {
629                len - 1
630            }
631        }
632        Some(ix) => (ix as isize + step).rem_euclid(len as isize) as usize,
633    })
634}
635
636#[cfg(test)]
637mod tests {
638    use super::cycle;
639
640    #[test]
641    fn cycle_steps_and_wraps() {
642        assert_eq!(cycle(0, None, 1), None);
643        assert_eq!(cycle(3, None, 1), Some(0));
644        assert_eq!(cycle(3, None, -1), Some(2));
645        assert_eq!(cycle(3, Some(2), 1), Some(0));
646        assert_eq!(cycle(3, Some(0), -1), Some(2));
647        assert_eq!(cycle(3, Some(1), 1), Some(2));
648    }
649}