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