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::{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}
60
61impl<T: Clone + 'static> Copy for ZoneRegistry<T> {}
62impl<T: Clone + 'static> Clone for ZoneRegistry<T> {
63    fn clone(&self) -> Self {
64        *self
65    }
66}
67impl<T: Clone + 'static> PartialEq for ZoneRegistry<T> {
68    fn eq(&self, other: &Self) -> bool {
69        self.zones == other.zones
70    }
71}
72
73impl<T: Clone + 'static> ZoneRegistry<T> {
74    /// Wrap an existing signal. Prefer [`crate::core::hooks::use_dnd_provider`].
75    pub fn from_signal(zones: Signal<Vec<ZoneRecord<T>>>) -> Self {
76        Self { zones }
77    }
78
79    /// Add (or replace, by id) a zone.
80    pub fn register(&mut self, record: ZoneRecord<T>) {
81        let mut zones = self.zones.write();
82        if let Some(existing) = zones.iter_mut().find(|z| z.id == record.id) {
83            *existing = record;
84        } else {
85            zones.push(record);
86        }
87    }
88
89    /// Update a zone's label in place (no-op if unchanged or unknown).
90    pub fn sync_label(&mut self, id: ZoneId, label: Option<String>) {
91        let needs = self
92            .zones
93            .peek()
94            .iter()
95            .any(|z| z.id == id && z.label != label);
96        if needs {
97            if let Some(z) = self.zones.write().iter_mut().find(|z| z.id == id) {
98                z.label = label;
99            }
100        }
101    }
102
103    /// Remove a zone (call when its component unmounts).
104    pub fn unregister(&mut self, id: ZoneId) {
105        self.zones.write().retain(|z| z.id != id);
106    }
107
108    /// Look up a zone by id.
109    pub fn get(&self, id: ZoneId) -> Option<ZoneRecord<T>> {
110        self.zones.peek().iter().find(|z| z.id == id).cloned()
111    }
112
113    /// All zones accepting `payload`, in registration order.
114    pub fn acceptable(&self, payload: &T) -> Vec<ZoneRecord<T>> {
115        self.zones
116            .peek()
117            .iter()
118            .filter(|z| z.accepts_payload(payload))
119            .cloned()
120            .collect()
121    }
122
123    /// The next/previous zone (cyclic) relative to `current` among zones that
124    /// accept `payload`. `step` is `+1` or `-1`.
125    ///
126    /// Order is **spatial** (top-to-bottom, then left-to-right) for zones
127    /// with measured rects - call [`Self::refresh_rects`] first, as the
128    /// built-in keyboard interaction does on pickup. Unmeasured zones keep
129    /// registration order, after the measured ones.
130    pub fn step_zone(&self, current: Option<ZoneId>, payload: &T, step: isize) -> Option<ZoneId> {
131        let mut zones = self.acceptable(payload);
132        spatial_sort(&mut zones);
133        let current_ix = current.and_then(|c| zones.iter().position(|z| z.id == c));
134        cycle(zones.len(), current_ix, step).map(|ix| zones[ix].id)
135    }
136
137    /// The parent of a zone, if it's nested.
138    pub fn parent_of(&self, id: ZoneId) -> Option<ZoneId> {
139        self.zones.peek().iter().find(|z| z.id == id)?.parent
140    }
141
142    /// Zones directly inside `parent` (`None` = root level) that accept
143    /// `payload`, in spatial order (top-to-bottom, left-to-right; unmeasured
144    /// zones keep registration order at the end).
145    pub fn children_of(&self, parent: Option<ZoneId>, payload: &T) -> Vec<ZoneRecord<T>> {
146        let mut zones: Vec<_> = self
147            .zones
148            .peek()
149            .iter()
150            .filter(|z| z.parent == parent && z.accepts_payload(payload))
151            .cloned()
152            .collect();
153        spatial_sort(&mut zones);
154        zones
155    }
156
157    /// Next/previous zone (cyclic) among the *siblings* of `current` -
158    /// zones sharing its parent. With no `current`, cycles the root level.
159    pub fn step_sibling(
160        &self,
161        current: Option<ZoneId>,
162        payload: &T,
163        step: isize,
164    ) -> Option<ZoneId> {
165        let parent = current.and_then(|c| self.parent_of(c));
166        let siblings = self.children_of(parent, payload);
167        let current_ix = current.and_then(|c| siblings.iter().position(|z| z.id == c));
168        cycle(siblings.len(), current_ix, step).map(|ix| siblings[ix].id)
169    }
170
171    /// The first (spatially) acceptable zone nested inside `id`.
172    pub fn first_child(&self, id: ZoneId, payload: &T) -> Option<ZoneId> {
173        self.children_of(Some(id), payload).first().map(|z| z.id)
174    }
175
176    /// Topmost zone containing `point` (client coordinates), using cached
177    /// rects - call [`Self::refresh_rects`] when a drag starts. Later-mounted
178    /// zones win, approximating DOM paint order.
179    pub fn hit_test(&self, point: Point) -> Option<ZoneId> {
180        self.zones
181            .peek()
182            .iter()
183            .rev()
184            .find(|z| (*z.rect.peek()).map(|r| r.contains(point)).unwrap_or(false))
185            .map(|z| z.id)
186    }
187
188    /// Like [`Self::hit_test`], but acceptance-aware: it returns the topmost
189    /// zone that both contains the point **and** accepts `payload`, and when no
190    /// such zone contains the point, falls back to the acceptable zone whose
191    /// center is nearest - within `max_distance` CSS px. Skipping zones that
192    /// reject the payload lets a drop land on an accepting zone sitting *under*
193    /// a rejecting (or decorative) one, and is friendlier for imprecise (touch)
194    /// drops that land in the gutter between zones.
195    pub fn hit_test_closest(&self, point: Point, payload: &T, max_distance: f64) -> Option<ZoneId> {
196        if let Some(hit) = self
197            .zones
198            .peek()
199            .iter()
200            .rev()
201            .find(|z| {
202                z.accepts_payload(payload)
203                    && (*z.rect.peek()).map(|r| r.contains(point)).unwrap_or(false)
204            })
205            .map(|z| z.id)
206        {
207            return Some(hit);
208        }
209        let mut best: Option<(ZoneId, f64)> = None;
210        for z in self.acceptable(payload) {
211            let Some(r) = *z.rect.peek() else { continue };
212            let c = r.center();
213            let (dx, dy) = (c.x - point.x, c.y - point.y);
214            let d = (dx * dx + dy * dy).sqrt();
215            if d <= max_distance && best.map(|(_, bd)| d < bd).unwrap_or(true) {
216                best = Some((z.id, d));
217            }
218        }
219        best.map(|(id, _)| id)
220    }
221
222    /// Re-measure every mounted zone's client rect and **wait** for the
223    /// measurements to land - unlike [`Self::refresh_rects`], which fires
224    /// and forgets. Use before a hit-test that must see fresh geometry
225    /// (e.g. retrying a missed touch drop after a layout change).
226    pub async fn measure_all(&self) {
227        let zones: Vec<_> = self
228            .zones
229            .peek()
230            .iter()
231            .map(|z| (z.mounted.peek().clone(), z.rect))
232            .collect();
233        for (mounted, mut rect) in zones {
234            if let Some(m) = mounted {
235                if let Ok(r) = m.get_client_rect().await {
236                    rect.set(Some(Rect::new(
237                        r.origin.x,
238                        r.origin.y,
239                        r.size.width,
240                        r.size.height,
241                    )));
242                }
243            }
244        }
245    }
246
247    /// Re-measure every mounted zone's client rect (async, spawned).
248    pub fn refresh_rects(&self) {
249        for zone in self.zones.peek().iter() {
250            let mounted = zone.mounted.peek().clone();
251            let mut rect = zone.rect;
252            if let Some(m) = mounted {
253                spawn(async move {
254                    if let Ok(r) = m.get_client_rect().await {
255                        rect.set(Some(Rect::new(
256                            r.origin.x,
257                            r.origin.y,
258                            r.size.width,
259                            r.size.height,
260                        )));
261                    }
262                });
263            }
264        }
265    }
266}
267
268/// Sort zones spatially: measured rects by (top, left), unmeasured last in
269/// their original relative order.
270fn spatial_sort<T: Clone + 'static>(zones: &mut [ZoneRecord<T>]) {
271    zones.sort_by(|a, b| match (*a.rect.peek(), *b.rect.peek()) {
272        (Some(ra), Some(rb)) => (ra.y, ra.x)
273            .partial_cmp(&(rb.y, rb.x))
274            .unwrap_or(std::cmp::Ordering::Equal),
275        (Some(_), None) => std::cmp::Ordering::Less,
276        (None, Some(_)) => std::cmp::Ordering::Greater,
277        (None, None) => std::cmp::Ordering::Equal,
278    });
279}
280
281/// Cyclic index stepping: `None` current starts at the first (or last)
282/// element depending on direction. Pure, for testability.
283pub(crate) fn cycle(len: usize, current: Option<usize>, step: isize) -> Option<usize> {
284    if len == 0 {
285        return None;
286    }
287    Some(match current {
288        None => {
289            if step >= 0 {
290                0
291            } else {
292                len - 1
293            }
294        }
295        Some(ix) => (ix as isize + step).rem_euclid(len as isize) as usize,
296    })
297}
298
299#[cfg(test)]
300mod tests {
301    use super::cycle;
302
303    #[test]
304    fn cycle_steps_and_wraps() {
305        assert_eq!(cycle(0, None, 1), None);
306        assert_eq!(cycle(3, None, 1), Some(0));
307        assert_eq!(cycle(3, None, -1), Some(2));
308        assert_eq!(cycle(3, Some(2), 1), Some(0));
309        assert_eq!(cycle(3, Some(0), -1), Some(2));
310        assert_eq!(cycle(3, Some(1), 1), Some(2));
311    }
312}