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;
8
9use dioxus::html::MountedData;
10use dioxus::prelude::*;
11
12use super::types::{Direction, DropOutcome, Point, Rect, ZoneId};
13
14/// One registered drop zone.
15pub struct ZoneRecord<T: Clone + 'static> {
16    pub id: ZoneId,
17    /// The enclosing zone, when this zone is nested inside another
18    /// `DropZone` (discovered automatically via context).
19    pub parent: Option<ZoneId>,
20    /// Human label used in screen-reader announcements.
21    pub label: Option<String>,
22    /// Delivers a completed drop to the zone's owner.
23    pub on_drop: Callback<DropOutcome<T>>,
24    /// The zone's acceptance filter, if any.
25    pub accepts: Option<Callback<T, bool>>,
26    /// The zone's mounted element, once available.
27    pub mounted: Signal<Option<Rc<MountedData>>>,
28    /// Cached client rect (refreshed via [`ZoneRegistry::refresh_rects`]).
29    pub rect: Signal<Option<Rect>>,
30}
31
32impl<T: Clone + 'static> Clone for ZoneRecord<T> {
33    fn clone(&self) -> Self {
34        Self {
35            id: self.id,
36            parent: self.parent,
37            label: self.label.clone(),
38            on_drop: self.on_drop,
39            accepts: self.accepts,
40            mounted: self.mounted,
41            rect: self.rect,
42        }
43    }
44}
45
46impl<T: Clone + 'static> ZoneRecord<T> {
47    /// Does this zone accept the payload?
48    pub fn accepts_payload(&self, payload: &T) -> bool {
49        match self.accepts {
50            Some(cb) => cb.call(payload.clone()),
51            None => true,
52        }
53    }
54}
55
56/// Registry of the currently mounted drop zones, in mount order.
57pub struct ZoneRegistry<T: Clone + 'static> {
58    zones: Signal<Vec<ZoneRecord<T>>>,
59    /// Layout direction for spatial ordering (keyboard navigation).
60    dir: Signal<Direction>,
61}
62
63impl<T: Clone + 'static> Copy for ZoneRegistry<T> {}
64impl<T: Clone + 'static> Clone for ZoneRegistry<T> {
65    fn clone(&self) -> Self {
66        *self
67    }
68}
69impl<T: Clone + 'static> PartialEq for ZoneRegistry<T> {
70    fn eq(&self, other: &Self) -> bool {
71        self.zones == other.zones && self.dir == other.dir
72    }
73}
74
75impl<T: Clone + 'static> ZoneRegistry<T> {
76    /// Wrap an existing signal. Prefer [`crate::core::hooks::use_dnd_provider`].
77    pub fn from_signal(zones: Signal<Vec<ZoneRecord<T>>>) -> Self {
78        Self {
79            zones,
80            dir: Signal::new(Direction::default()),
81        }
82    }
83
84    /// Layout direction spatial ordering follows.
85    pub fn direction(&self) -> Direction {
86        *self.dir.peek()
87    }
88
89    /// Set the layout direction (no-op if unchanged; safe to call every
90    /// render). `DndProvider`'s `dir` prop calls this for you.
91    pub fn set_direction(&mut self, dir: Direction) {
92        if *self.dir.peek() != dir {
93            self.dir.set(dir);
94        }
95    }
96
97    /// Add (or replace, by id) a zone.
98    pub fn register(&mut self, record: ZoneRecord<T>) {
99        let mut zones = self.zones.write();
100        if let Some(existing) = zones.iter_mut().find(|z| z.id == record.id) {
101            *existing = record;
102        } else {
103            zones.push(record);
104        }
105    }
106
107    /// Update a zone's label in place (no-op if unchanged or unknown).
108    pub fn sync_label(&mut self, id: ZoneId, label: Option<String>) {
109        let needs = self
110            .zones
111            .peek()
112            .iter()
113            .any(|z| z.id == id && z.label != label);
114        if needs {
115            if let Some(z) = self.zones.write().iter_mut().find(|z| z.id == id) {
116                z.label = label;
117            }
118        }
119    }
120
121    /// Remove a zone (call when its component unmounts).
122    pub fn unregister(&mut self, id: ZoneId) {
123        self.zones.write().retain(|z| z.id != id);
124    }
125
126    /// Look up a zone by id.
127    pub fn get(&self, id: ZoneId) -> Option<ZoneRecord<T>> {
128        self.zones.peek().iter().find(|z| z.id == id).cloned()
129    }
130
131    /// Is a zone with this id registered *here*? The parent-zone context is
132    /// shared across payload types, so a record's `parent` can name a zone
133    /// living in another type's registry - check before navigating to one.
134    pub fn contains(&self, id: ZoneId) -> bool {
135        self.zones.peek().iter().any(|z| z.id == id)
136    }
137
138    /// The zone keyboard navigation should enter when ascending from
139    /// `current`: its parent, but only when that parent is registered in
140    /// this registry. A `DropZone<A>` nested inside a `DropZone<B>` records
141    /// B's id as its parent, and entering an id this registry can't resolve
142    /// would leave the drag hovering a zone that can never receive it.
143    pub fn ascend(&self, current: ZoneId) -> Option<ZoneId> {
144        self.parent_of(current).filter(|pid| self.contains(*pid))
145    }
146
147    /// All zones accepting `payload`, in registration order.
148    pub fn acceptable(&self, payload: &T) -> Vec<ZoneRecord<T>> {
149        self.zones
150            .peek()
151            .iter()
152            .filter(|z| z.accepts_payload(payload))
153            .cloned()
154            .collect()
155    }
156
157    /// The next/previous zone (cyclic) relative to `current` among zones that
158    /// accept `payload`. `step` is `+1` or `-1`.
159    ///
160    /// Order is **spatial** (top-to-bottom, then left-to-right) for zones
161    /// with measured rects - call [`Self::refresh_rects`] first, as the
162    /// built-in keyboard interaction does on pickup. Unmeasured zones keep
163    /// registration order, after the measured ones.
164    pub fn step_zone(&self, current: Option<ZoneId>, payload: &T, step: isize) -> Option<ZoneId> {
165        let mut zones = self.acceptable(payload);
166        spatial_sort(&mut zones, self.direction());
167        let current_ix = current.and_then(|c| zones.iter().position(|z| z.id == c));
168        cycle(zones.len(), current_ix, step).map(|ix| zones[ix].id)
169    }
170
171    /// The parent of a zone, if it's nested.
172    pub fn parent_of(&self, id: ZoneId) -> Option<ZoneId> {
173        self.zones.peek().iter().find(|z| z.id == id)?.parent
174    }
175
176    /// Zones directly inside `parent` (`None` = root level) that accept
177    /// `payload`, in spatial order (top-to-bottom, left-to-right; unmeasured
178    /// zones keep registration order at the end).
179    pub fn children_of(&self, parent: Option<ZoneId>, payload: &T) -> Vec<ZoneRecord<T>> {
180        let mut zones: Vec<_> = self
181            .zones
182            .peek()
183            .iter()
184            .filter(|z| z.parent == parent && z.accepts_payload(payload))
185            .cloned()
186            .collect();
187        spatial_sort(&mut zones, self.direction());
188        zones
189    }
190
191    /// Next/previous zone (cyclic) among the *siblings* of `current` -
192    /// zones sharing its parent. With no `current`, cycles the root level.
193    pub fn step_sibling(
194        &self,
195        current: Option<ZoneId>,
196        payload: &T,
197        step: isize,
198    ) -> Option<ZoneId> {
199        let parent = current.and_then(|c| self.parent_of(c));
200        let siblings = self.children_of(parent, payload);
201        let current_ix = current.and_then(|c| siblings.iter().position(|z| z.id == c));
202        cycle(siblings.len(), current_ix, step).map(|ix| siblings[ix].id)
203    }
204
205    /// The first (spatially) acceptable zone nested inside `id`.
206    pub fn first_child(&self, id: ZoneId, payload: &T) -> Option<ZoneId> {
207        self.children_of(Some(id), payload).first().map(|z| z.id)
208    }
209
210    /// Topmost zone containing `point` (client coordinates), using cached
211    /// rects - call [`Self::refresh_rects`] when a drag starts. Later-mounted
212    /// zones win, approximating DOM paint order.
213    pub fn hit_test(&self, point: Point) -> Option<ZoneId> {
214        self.zones
215            .peek()
216            .iter()
217            .rev()
218            .find(|z| (*z.rect.peek()).map(|r| r.contains(point)).unwrap_or(false))
219            .map(|z| z.id)
220    }
221
222    /// Like [`Self::hit_test`], but acceptance-aware: it returns the topmost
223    /// zone that both contains the point **and** accepts `payload`, and when no
224    /// such zone contains the point, falls back to the acceptable zone whose
225    /// center is nearest - within `max_distance` CSS px. Skipping zones that
226    /// reject the payload lets a drop land on an accepting zone sitting *under*
227    /// a rejecting (or decorative) one, and is friendlier for imprecise (touch)
228    /// drops that land in the gutter between zones.
229    pub fn hit_test_closest(&self, point: Point, payload: &T, max_distance: f64) -> Option<ZoneId> {
230        if let Some(hit) = self
231            .zones
232            .peek()
233            .iter()
234            .rev()
235            .find(|z| {
236                z.accepts_payload(payload)
237                    && (*z.rect.peek()).map(|r| r.contains(point)).unwrap_or(false)
238            })
239            .map(|z| z.id)
240        {
241            return Some(hit);
242        }
243        let mut best: Option<(ZoneId, f64)> = None;
244        for z in self.acceptable(payload) {
245            let Some(r) = *z.rect.peek() else { continue };
246            let c = r.center();
247            let (dx, dy) = (c.x - point.x, c.y - point.y);
248            let d = (dx * dx + dy * dy).sqrt();
249            if d <= max_distance && best.map(|(_, bd)| d < bd).unwrap_or(true) {
250                best = Some((z.id, d));
251            }
252        }
253        best.map(|(id, _)| id)
254    }
255
256    /// Re-measure every mounted zone's client rect and **wait** for the
257    /// measurements to land - unlike [`Self::refresh_rects`], which fires
258    /// and forgets. Use before a hit-test that must see fresh geometry
259    /// (e.g. retrying a missed touch drop after a layout change).
260    pub async fn measure_all(&self) {
261        let zones: Vec<_> = self
262            .zones
263            .peek()
264            .iter()
265            .map(|z| (z.mounted.peek().clone(), z.rect))
266            .collect();
267        for (mounted, mut rect) in zones {
268            if let Some(m) = mounted {
269                if let Ok(r) = m.get_client_rect().await {
270                    rect.set(Some(Rect::new(
271                        r.origin.x,
272                        r.origin.y,
273                        r.size.width,
274                        r.size.height,
275                    )));
276                }
277            }
278        }
279    }
280
281    /// Re-measure every mounted zone's client rect (async, spawned).
282    pub fn refresh_rects(&self) {
283        for zone in self.zones.peek().iter() {
284            let mounted = zone.mounted.peek().clone();
285            let mut rect = zone.rect;
286            if let Some(m) = mounted {
287                spawn(async move {
288                    if let Ok(r) = m.get_client_rect().await {
289                        rect.set(Some(Rect::new(
290                            r.origin.x,
291                            r.origin.y,
292                            r.size.width,
293                            r.size.height,
294                        )));
295                    }
296                });
297            }
298        }
299    }
300}
301
302/// A payload-type-erased "re-measure your zones" channel, shared by every
303/// registry under one provider tree.
304///
305/// Cached client rects go stale the moment layout moves under a live drag -
306/// scrolling being the everyday case. Registries are per payload type, but
307/// the things that move layout (an auto-scrolling container, your own
308/// scroll surface, a collapsing panel) shouldn't need to know any payload
309/// type to say "geometry changed". Each provider registers a thunk here
310/// that re-measures its own registry **only while it has a drag in
311/// flight**, so pinging the channel from every scroll event costs nothing
312/// while idle.
313///
314/// [`crate::autoscroll::AutoScroll`] pings this automatically after every
315/// scroll it performs (and on any other scroll of its container); grab the
316/// channel with [`crate::core::hooks::use_rect_refresh`] to wire up custom
317/// layout mutators.
318pub struct RectRefresh {
319    thunks: Signal<Vec<(u64, Callback<()>)>>,
320}
321
322impl Copy for RectRefresh {}
323impl Clone for RectRefresh {
324    fn clone(&self) -> Self {
325        *self
326    }
327}
328impl PartialEq for RectRefresh {
329    fn eq(&self, other: &Self) -> bool {
330        self.thunks == other.thunks
331    }
332}
333
334impl RectRefresh {
335    /// Wrap an existing signal. Prefer [`crate::core::hooks::use_dnd_provider`],
336    /// which creates one per provider *tree* (nested providers inherit and
337    /// re-provide the outermost channel).
338    pub fn from_signal(thunks: Signal<Vec<(u64, Callback<()>)>>) -> Self {
339        Self { thunks }
340    }
341
342    /// Ask every provider in the tree to re-measure its zones. Providers
343    /// without a drag in flight ignore the ping, so this is safe to call
344    /// from high-frequency sources like scroll events.
345    pub fn refresh_all(&self) {
346        for (_, thunk) in self.thunks.peek().iter() {
347            thunk.call(());
348        }
349    }
350
351    /// Number of registered providers. Diagnostics and tests.
352    pub fn len(&self) -> usize {
353        self.thunks.peek().len()
354    }
355
356    /// Whether any provider is registered.
357    pub fn is_empty(&self) -> bool {
358        self.len() == 0
359    }
360
361    /// Add (or replace, by key) a provider's re-measure thunk.
362    pub(crate) fn register(&mut self, key: u64, thunk: Callback<()>) {
363        let mut thunks = self.thunks.write();
364        if let Some(existing) = thunks.iter_mut().find(|(k, _)| *k == key) {
365            existing.1 = thunk;
366        } else {
367            thunks.push((key, thunk));
368        }
369    }
370
371    /// Remove a provider's thunk (call when the provider unmounts).
372    pub(crate) fn unregister(&mut self, key: u64) {
373        self.thunks.write().retain(|(k, _)| *k != key);
374    }
375}
376
377/// Sort zones spatially: measured rects by (top, reading order), unmeasured
378/// last in their original relative order. Reading order within a row is
379/// left-to-right in LTR and right-to-left in RTL, so keyboard traversal
380/// follows what the user sees either way.
381fn spatial_sort<T: Clone + 'static>(zones: &mut [ZoneRecord<T>], dir: Direction) {
382    let reading_x = move |x: f64| match dir {
383        Direction::Ltr => x,
384        Direction::Rtl => -x,
385    };
386    zones.sort_by(|a, b| match (*a.rect.peek(), *b.rect.peek()) {
387        (Some(ra), Some(rb)) => (ra.y, reading_x(ra.x))
388            .partial_cmp(&(rb.y, reading_x(rb.x)))
389            .unwrap_or(std::cmp::Ordering::Equal),
390        (Some(_), None) => std::cmp::Ordering::Less,
391        (None, Some(_)) => std::cmp::Ordering::Greater,
392        (None, None) => std::cmp::Ordering::Equal,
393    });
394}
395
396/// Cyclic index stepping: `None` current starts at the first (or last)
397/// element depending on direction. Pure, for testability.
398pub(crate) fn cycle(len: usize, current: Option<usize>, step: isize) -> Option<usize> {
399    if len == 0 {
400        return None;
401    }
402    Some(match current {
403        None => {
404            if step >= 0 {
405                0
406            } else {
407                len - 1
408            }
409        }
410        Some(ix) => (ix as isize + step).rem_euclid(len as isize) as usize,
411    })
412}
413
414#[cfg(test)]
415mod tests {
416    use super::cycle;
417
418    #[test]
419    fn cycle_steps_and_wraps() {
420        assert_eq!(cycle(0, None, 1), None);
421        assert_eq!(cycle(3, None, 1), Some(0));
422        assert_eq!(cycle(3, None, -1), Some(2));
423        assert_eq!(cycle(3, Some(2), 1), Some(0));
424        assert_eq!(cycle(3, Some(0), -1), Some(2));
425        assert_eq!(cycle(3, Some(1), 1), Some(2));
426    }
427}