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::cell::Cell;
8use std::rc::Rc;
9use std::sync::atomic::{AtomicU64, Ordering};
10
11use dioxus::html::MountedData;
12use dioxus::prelude::*;
13
14use super::collision::{
15    rank_builtin_candidates, rank_collisions, CollisionDetector, CollisionRequest, ReleasePolicy,
16    ZoneCandidate,
17};
18use super::effects::{DropEffects, DropQuery};
19use super::types::{Direction, DropEffect, DropOutcome, EdgeSet, Point, Rect, ZoneId};
20
21// Identity freshness only: Relaxed is sufficient because the counter carries
22// no synchronization. Correctness assumes this process-lifetime u64 never
23// wraps; do not narrow it.
24static NEXT_ZONE_REGISTRATION: AtomicU64 = AtomicU64::new(1);
25
26fn trace_registry_failure(
27    operation: &'static str,
28    storage: &'static str,
29    zone: Option<ZoneId>,
30    generation: Option<u64>,
31    error: &impl std::fmt::Display,
32) {
33    tracing::trace!(
34        target: "dioxus_dnd::registry",
35        operation,
36        storage,
37        zone_id = ?zone,
38        registration_generation = ?generation,
39        error = %error,
40        "zone registry operation skipped"
41    );
42}
43
44/// Identifies one particular registration of a [`ZoneId`].
45///
46/// A zone id can be replaced in place. Async measurements carry this token
47/// so a result started for the old registration cannot land in its
48/// same-id replacement.
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
50pub struct ZoneRegistration {
51    id: ZoneId,
52    generation: u64,
53}
54
55/// One registered drop zone.
56pub struct ZoneRecord<T: Clone + 'static> {
57    pub id: ZoneId,
58    /// The enclosing zone, when this zone is nested inside another
59    /// `DropZone` (discovered automatically via context).
60    pub parent: Option<ZoneId>,
61    /// Human label used in screen-reader announcements.
62    pub label: Option<String>,
63    /// Delivers a completed drop to the zone's owner.
64    pub on_drop: Callback<DropOutcome<T>>,
65    /// The zone's acceptance filter, if any.
66    pub accepts: Option<Callback<T, bool>>,
67    /// The zone's mounted element, once available. This plain value lives in
68    /// the provider-owned registry storage; zones update it through
69    /// [`ZoneRegistry::set_mounted`].
70    pub mounted: Option<Rc<MountedData>>,
71    /// Cached client rect (refreshed via [`ZoneRegistry::refresh_rects`]).
72    /// This plain value lives in the provider-owned registry storage; zones
73    /// update it through [`ZoneRegistry::set_rect_if_present`].
74    pub rect: Option<Rect>,
75}
76
77impl<T: Clone + 'static> Clone for ZoneRecord<T> {
78    fn clone(&self) -> Self {
79        Self {
80            id: self.id,
81            parent: self.parent,
82            label: self.label.clone(),
83            on_drop: self.on_drop,
84            accepts: self.accepts,
85            mounted: self.mounted.clone(),
86            rect: self.rect,
87        }
88    }
89}
90
91impl<T: Clone + 'static> ZoneRecord<T> {
92    /// Create a zone record with permissive default acceptance policy.
93    pub fn new(id: ZoneId, on_drop: Callback<DropOutcome<T>>) -> Self {
94        Self {
95            id,
96            parent: None,
97            label: None,
98            on_drop,
99            accepts: None,
100            mounted: None,
101            rect: None,
102        }
103    }
104
105    /// Does this zone accept the payload?
106    pub fn accepts_payload(&self, payload: &T) -> bool {
107        match self.accepts {
108            Some(cb) => cb.call(payload.clone()),
109            None => true,
110        }
111    }
112
113    /// The cached client rect in this registry snapshot.
114    pub fn cached_rect(&self) -> Option<Rect> {
115        self.rect
116    }
117
118    /// The mounted element in this registry snapshot.
119    pub fn mounted_handle(&self) -> Option<Rc<MountedData>> {
120        self.mounted.clone()
121    }
122}
123
124/// Target behavior added after the 3.x `ZoneRecord` shape was published.
125///
126/// This remains private and is keyed by the complete registration token so
127/// policy updates from an unmounted component cannot affect a same-id
128/// replacement.
129#[derive(Clone, PartialEq)]
130pub(crate) struct ZonePolicy<T: Clone + 'static> {
131    pub(crate) accepts_query: Option<Callback<DropQuery<T>, bool>>,
132    pub(crate) allowed_effects: DropEffects,
133    pub(crate) edge: Option<EdgeSet>,
134}
135
136impl<T: Clone + 'static> Default for ZonePolicy<T> {
137    fn default() -> Self {
138        Self {
139            accepts_query: None,
140            allowed_effects: DropEffects::default(),
141            edge: None,
142        }
143    }
144}
145
146#[derive(Clone)]
147struct RegisteredZone<T: Clone + 'static> {
148    record: ZoneRecord<T>,
149    policy: ZonePolicy<T>,
150}
151
152impl<T: Clone + 'static> RegisteredZone<T> {
153    fn negotiate(&self, query: &DropQuery<T>) -> Option<DropEffect> {
154        if query.proposed_effect == DropEffect::None || !self.record.accepts_payload(&query.payload)
155        {
156            return None;
157        }
158        if self
159            .policy
160            .accepts_query
161            .is_some_and(|callback| !callback.call(query.clone()))
162        {
163            return None;
164        }
165        self.policy.allowed_effects.negotiate(query.proposed_effect)
166    }
167}
168
169pub(crate) struct NegotiatedZone<T: Clone + 'static> {
170    pub(crate) record: ZoneRecord<T>,
171    pub(crate) effect: DropEffect,
172    pub(crate) edge: Option<EdgeSet>,
173}
174
175/// Registry of the currently registered drop zones, in registration order.
176pub struct ZoneRegistry<T: Clone + 'static> {
177    zones: Signal<Vec<ZoneRecord<T>>>,
178    /// Current generation for each id in `zones`. Kept separately so
179    /// registration identity is not part of the public `ZoneRecord` shape.
180    registrations: Signal<Vec<(ZoneId, u64)>>,
181    /// New target behavior kept out of the public 3.x `ZoneRecord` shape.
182    policies: Signal<Vec<(ZoneRegistration, ZonePolicy<T>)>>,
183    /// Changes only when the zone set or a mounted handle changes. The debug
184    /// overlay subscribes here so rect writes cannot retrigger measurement.
185    mount_revision: Signal<u64>,
186    /// Layout direction for spatial ordering (keyboard navigation).
187    dir: Signal<Direction>,
188    /// Collision and release behavior for this provider/window.
189    release: Signal<ReleasePolicy<T>>,
190}
191
192impl<T: Clone + 'static> Copy for ZoneRegistry<T> {}
193impl<T: Clone + 'static> Clone for ZoneRegistry<T> {
194    fn clone(&self) -> Self {
195        *self
196    }
197}
198impl<T: Clone + 'static> PartialEq for ZoneRegistry<T> {
199    fn eq(&self, other: &Self) -> bool {
200        self.zones == other.zones
201            && self.registrations == other.registrations
202            && self.policies == other.policies
203            && self.mount_revision == other.mount_revision
204            && self.dir == other.dir
205            && self.release == other.release
206    }
207}
208
209impl<T: Clone + 'static> ZoneRegistry<T> {
210    /// Wrap an existing signal. Prefer [`crate::core::hooks::use_dnd_provider`].
211    pub fn from_signal(zones: Signal<Vec<ZoneRecord<T>>>) -> Self {
212        Self {
213            zones,
214            registrations: Signal::new(Vec::new()),
215            policies: Signal::new(Vec::new()),
216            mount_revision: Signal::new(0),
217            dir: Signal::new(Direction::default()),
218            release: Signal::new(ReleasePolicy::default()),
219        }
220    }
221
222    /// Current collision and release policy.
223    pub fn release_policy(&self) -> ReleasePolicy<T> {
224        self.release
225            .try_peek()
226            .map(|policy| *policy)
227            .unwrap_or_default()
228    }
229
230    /// Synchronize the provider's collision and release policy.
231    pub fn set_release_policy(&mut self, policy: ReleasePolicy<T>) {
232        if self
233            .release
234            .try_peek()
235            .is_ok_and(|current| *current == policy)
236        {
237            return;
238        }
239        if let Ok(mut current) = self.release.try_write() {
240            *current = policy;
241        }
242    }
243
244    /// Layout direction spatial ordering follows.
245    pub fn direction(&self) -> Direction {
246        self.dir.try_peek().map(|dir| *dir).unwrap_or_default()
247    }
248
249    /// Set the layout direction (no-op if unchanged; safe to call every
250    /// render). `DndProvider`'s `dir` prop calls this for you.
251    pub fn set_direction(&mut self, dir: Direction) {
252        let changed = match self.dir.try_peek() {
253            Ok(current) => *current != dir,
254            Err(error) => {
255                trace_registry_failure("set_direction", "dir", None, None, &error);
256                return;
257            }
258        };
259        if changed {
260            match self.dir.try_write() {
261                Ok(mut current) => *current = dir,
262                Err(error) => trace_registry_failure("set_direction", "dir", None, None, &error),
263            }
264        }
265    }
266
267    /// Add (or replace, by id) a zone.
268    pub fn register(&mut self, record: ZoneRecord<T>) -> ZoneRegistration {
269        self.register_with_policy(record, ZonePolicy::default())
270    }
271
272    /// Register a built-in zone with behavior that is intentionally private
273    /// so the public `ZoneRecord` remains source compatible with 3.x.
274    pub(crate) fn register_with_policy(
275        &mut self,
276        record: ZoneRecord<T>,
277        policy: ZonePolicy<T>,
278    ) -> ZoneRegistration {
279        let registration = ZoneRegistration {
280            id: record.id,
281            generation: NEXT_ZONE_REGISTRATION.fetch_add(1, Ordering::Relaxed),
282        };
283        // Acquire both halves before mutating either. A runtime borrow
284        // collision must not leave `zones` and `registrations` disagreeing.
285        let mut zones = match self.zones.try_write() {
286            Ok(zones) => zones,
287            Err(error) => {
288                trace_registry_failure(
289                    "register",
290                    "zones",
291                    Some(registration.id),
292                    Some(registration.generation),
293                    &error,
294                );
295                return registration;
296            }
297        };
298        let mut registrations = match self.registrations.try_write() {
299            Ok(registrations) => registrations,
300            Err(error) => {
301                trace_registry_failure(
302                    "register",
303                    "registrations",
304                    Some(registration.id),
305                    Some(registration.generation),
306                    &error,
307                );
308                return registration;
309            }
310        };
311        let mut policies = match self.policies.try_write() {
312            Ok(policies) => policies,
313            Err(error) => {
314                trace_registry_failure(
315                    "register",
316                    "policies",
317                    Some(registration.id),
318                    Some(registration.generation),
319                    &error,
320                );
321                return registration;
322            }
323        };
324        if let Some(existing) = zones.iter_mut().find(|z| z.id == record.id) {
325            *existing = record;
326        } else {
327            zones.push(record);
328        }
329        if let Some(existing) = registrations
330            .iter_mut()
331            .find(|(id, _)| *id == registration.id)
332        {
333            existing.1 = registration.generation;
334        } else {
335            registrations.push((registration.id, registration.generation));
336        }
337        policies.retain(|(candidate, _)| candidate.id != registration.id);
338        policies.push((registration, policy));
339        drop(policies);
340        drop(registrations);
341        drop(zones);
342        self.bump_mount_revision();
343        registration
344    }
345
346    /// Update a zone's label in place (no-op if unchanged or unknown).
347    pub fn sync_label(&mut self, id: ZoneId, label: Option<String>) {
348        let needs = match self.zones.try_peek() {
349            Ok(zones) => zones.iter().any(|z| z.id == id && z.label != label),
350            Err(error) => {
351                trace_registry_failure("sync_label", "zones", Some(id), None, &error);
352                return;
353            }
354        };
355        if needs {
356            match self.zones.try_write() {
357                Ok(mut zones) => {
358                    if let Some(z) = zones.iter_mut().find(|z| z.id == id) {
359                        z.label = label;
360                    }
361                }
362                Err(error) => trace_registry_failure("sync_label", "zones", Some(id), None, &error),
363            }
364        }
365    }
366
367    /// Update hierarchical ownership for this exact live registration.
368    pub(crate) fn sync_parent(&mut self, registration: ZoneRegistration, parent: Option<ZoneId>) {
369        if !self.is_current(registration, "sync_parent") {
370            return;
371        }
372        let needs = self.zones.try_peek().is_ok_and(|zones| {
373            zones
374                .iter()
375                .any(|zone| zone.id == registration.id && zone.parent != parent)
376        });
377        if !needs {
378            return;
379        }
380        match self.zones.try_write() {
381            Ok(mut zones) => {
382                if let Some(zone) = zones.iter_mut().find(|zone| zone.id == registration.id) {
383                    zone.parent = parent;
384                }
385            }
386            Err(error) => trace_registry_failure(
387                "sync_parent",
388                "zones",
389                Some(registration.id),
390                Some(registration.generation),
391                &error,
392            ),
393        }
394    }
395
396    /// Update acceptance, effect, and edge policy in place. Components call
397    /// this from reactive synchronization so policy props can change without
398    /// replacing the zone or losing its measured geometry.
399    pub(crate) fn sync_policy(
400        &mut self,
401        registration: ZoneRegistration,
402        accepts: Option<Callback<T, bool>>,
403        policy: ZonePolicy<T>,
404    ) {
405        if !self.is_current(registration, "sync_policy") {
406            return;
407        }
408        let mut zones = match self.zones.try_write() {
409            Ok(zones) => zones,
410            Err(error) => {
411                trace_registry_failure(
412                    "sync_policy",
413                    "zones",
414                    Some(registration.id),
415                    Some(registration.generation),
416                    &error,
417                );
418                return;
419            }
420        };
421        let mut policies = match self.policies.try_write() {
422            Ok(policies) => policies,
423            Err(error) => {
424                trace_registry_failure(
425                    "sync_policy",
426                    "policies",
427                    Some(registration.id),
428                    Some(registration.generation),
429                    &error,
430                );
431                return;
432            }
433        };
434        if let Some(zone) = zones.iter_mut().find(|zone| zone.id == registration.id) {
435            zone.accepts = accepts;
436        }
437        if let Some((_, current)) = policies
438            .iter_mut()
439            .find(|(candidate, _)| *candidate == registration)
440        {
441            *current = policy;
442        }
443    }
444
445    /// Remove a zone (call when its component unmounts).
446    pub fn unregister(&mut self, id: ZoneId) {
447        // Structural state is a pair; acquire both guards before changing it.
448        let mut zones = match self.zones.try_write() {
449            Ok(zones) => zones,
450            Err(error) => {
451                trace_registry_failure("unregister", "zones", Some(id), None, &error);
452                return;
453            }
454        };
455        let mut registrations = match self.registrations.try_write() {
456            Ok(registrations) => registrations,
457            Err(error) => {
458                trace_registry_failure("unregister", "registrations", Some(id), None, &error);
459                return;
460            }
461        };
462        let mut policies = match self.policies.try_write() {
463            Ok(policies) => policies,
464            Err(error) => {
465                trace_registry_failure("unregister", "policies", Some(id), None, &error);
466                return;
467            }
468        };
469        let old_len = zones.len();
470        zones.retain(|z| z.id != id);
471        let removed = zones.len() != old_len;
472        registrations.retain(|(registered_id, _)| *registered_id != id);
473        policies.retain(|(registration, _)| registration.id != id);
474        drop(policies);
475        drop(registrations);
476        drop(zones);
477        if removed {
478            self.bump_mount_revision();
479        }
480    }
481
482    /// Remove this exact registration if it is still current.
483    ///
484    /// A keyed Dioxus replacement may mount a new record with the same id
485    /// before the old component's cleanup runs. Token-aware cleanup prevents
486    /// that stale cleanup from deleting the replacement.
487    pub fn unregister_registration(&mut self, registration: ZoneRegistration) {
488        let current = self
489            .registrations
490            .try_read()
491            .ok()
492            .and_then(|registrations| {
493                registrations
494                    .iter()
495                    .find(|(id, _)| *id == registration.id)
496                    .copied()
497            });
498        if current == Some((registration.id, registration.generation)) {
499            self.unregister(registration.id);
500        }
501    }
502
503    /// Attach the mounted element to this exact registration. A stale
504    /// registration token is ignored.
505    pub fn set_mounted(&mut self, registration: ZoneRegistration, mounted: Rc<MountedData>) {
506        if !self.is_current(registration, "set_mounted") {
507            return;
508        }
509        let mut changed = false;
510        match self.zones.try_write() {
511            Ok(mut zones) => {
512                if let Some(zone) = zones.iter_mut().find(|z| z.id == registration.id) {
513                    zone.mounted = Some(mounted);
514                    changed = true;
515                }
516            }
517            Err(error) => {
518                trace_registry_failure(
519                    "set_mounted",
520                    "zones",
521                    Some(registration.id),
522                    Some(registration.generation),
523                    &error,
524                );
525            }
526        }
527        if changed {
528            self.bump_mount_revision();
529        }
530    }
531
532    /// Store a rect only while the registration that requested it is still
533    /// current. This never inserts a missing zone and therefore cannot
534    /// resurrect one that unmounted during an async measurement.
535    pub fn set_rect_if_present(&mut self, registration: ZoneRegistration, rect: Rect) {
536        if !self.is_current(registration, "set_rect_if_present") {
537            return;
538        }
539        match self.zones.try_write() {
540            Ok(mut zones) => {
541                if let Some(zone) = zones.iter_mut().find(|z| z.id == registration.id) {
542                    zone.rect = Some(rect);
543                }
544            }
545            Err(error) => {
546                trace_registry_failure(
547                    "set_rect_if_present",
548                    "zones",
549                    Some(registration.id),
550                    Some(registration.generation),
551                    &error,
552                );
553            }
554        }
555    }
556
557    /// Set geometry for the current registration of `id`. This is the
558    /// synchronous/manual counterpart to [`Self::set_rect_if_present`], used
559    /// by custom layout adapters and the headless test driver.
560    pub fn set_rect(&mut self, id: ZoneId, rect: Rect) {
561        if let Some(registration) = self.current_registration(id, "set_rect") {
562            self.set_rect_if_present(registration, rect);
563        }
564    }
565
566    /// Look up a zone by id.
567    pub fn get(&self, id: ZoneId) -> Option<ZoneRecord<T>> {
568        self.zones
569            .try_peek()
570            .ok()?
571            .iter()
572            .find(|z| z.id == id)
573            .cloned()
574    }
575
576    /// The zone's cached client rect, read without subscribing. Returns
577    /// `None` when unmeasured, unknown, or the provider is already gone.
578    pub fn cached_rect(&self, id: ZoneId) -> Option<Rect> {
579        self.zones
580            .try_peek()
581            .ok()?
582            .iter()
583            .find(|z| z.id == id)
584            .and_then(ZoneRecord::cached_rect)
585    }
586
587    /// The zone's mounted element, read without subscribing. Returns `None`
588    /// before mount, for an unknown zone, or after provider teardown.
589    pub fn mounted_handle(&self, id: ZoneId) -> Option<Rc<MountedData>> {
590        self.zones
591            .try_peek()
592            .ok()?
593            .iter()
594            .find(|z| z.id == id)
595            .and_then(ZoneRecord::mounted_handle)
596    }
597
598    /// Every registered zone, in registration order. Unlike the peeking
599    /// lookups around it this is a *subscribing* read - a component
600    /// rendering from it re-renders when zones mount or unmount - because
601    /// its consumers (the debug overlay, your own devtools) are renderers.
602    pub fn records(&self) -> Vec<ZoneRecord<T>> {
603        let records = self
604            .zones
605            .try_read()
606            .map(|zones| zones.to_vec())
607            .unwrap_or_default();
608        records
609    }
610
611    /// Take a bounded record snapshot before invoking application callbacks.
612    /// Dioxus signals are runtime-borrowed, so user acceptance or collision
613    /// code must never run while the registry's read guard is live.
614    fn snapshot(&self) -> Vec<RegisteredZone<T>> {
615        let records = self
616            .zones
617            .try_peek()
618            .map(|zones| zones.to_vec())
619            .unwrap_or_default();
620        let registrations = self
621            .registrations
622            .try_peek()
623            .map(|registrations| registrations.to_vec())
624            .unwrap_or_default();
625        let policies = self
626            .policies
627            .try_peek()
628            .map(|policies| policies.to_vec())
629            .unwrap_or_default();
630        records
631            .into_iter()
632            .map(|record| {
633                let registration = registrations.iter().find(|(id, _)| *id == record.id).map(
634                    |(id, generation)| ZoneRegistration {
635                        id: *id,
636                        generation: *generation,
637                    },
638                );
639                let policy = registration
640                    .and_then(|registration| {
641                        policies
642                            .iter()
643                            .find(|(candidate, _)| *candidate == registration)
644                            .map(|(_, policy)| policy.clone())
645                    })
646                    .unwrap_or_default();
647                RegisteredZone { record, policy }
648            })
649            .collect()
650    }
651
652    /// Is a zone with this id registered *here*? The parent-zone context is
653    /// shared across payload types, so a record's `parent` can name a zone
654    /// living in another type's registry - check before navigating to one.
655    pub fn contains(&self, id: ZoneId) -> bool {
656        self.zones
657            .try_peek()
658            .is_ok_and(|zones| zones.iter().any(|z| z.id == id))
659    }
660
661    /// The zone keyboard navigation should enter when ascending from
662    /// `current`: its parent, but only when that parent is registered in
663    /// this registry. A `DropZone<A>` nested inside a `DropZone<B>` records
664    /// B's id as its parent, and entering an id this registry can't resolve
665    /// would leave the drag hovering a zone that can never receive it.
666    pub fn ascend(&self, current: ZoneId) -> Option<ZoneId> {
667        self.parent_of(current).filter(|pid| self.contains(*pid))
668    }
669
670    /// All zones accepting `payload`, in registration order.
671    pub fn acceptable(&self, payload: &T) -> Vec<ZoneRecord<T>> {
672        self.snapshot()
673            .into_iter()
674            .filter(|zone| zone.record.accepts_payload(payload))
675            .map(|zone| zone.record)
676            .collect()
677    }
678
679    /// All zones accepting a complete drop query.
680    pub fn acceptable_query(&self, query: &DropQuery<T>) -> Vec<ZoneRecord<T>> {
681        self.snapshot()
682            .into_iter()
683            .filter(|zone| zone.negotiate(query).is_some())
684            .map(|zone| zone.record)
685            .collect()
686    }
687
688    /// Negotiate one exact zone against its current private policy.
689    pub(crate) fn negotiate_zone(
690        &self,
691        id: ZoneId,
692        query: &DropQuery<T>,
693    ) -> Option<NegotiatedZone<T>> {
694        let zone = self
695            .snapshot()
696            .into_iter()
697            .find(|zone| zone.record.id == id)?;
698        let effect = zone.negotiate(query)?;
699        Some(NegotiatedZone {
700            record: zone.record,
701            effect,
702            edge: zone.policy.edge,
703        })
704    }
705
706    /// The next/previous zone (cyclic) relative to `current` among zones that
707    /// accept `payload`. `step` is `+1` or `-1`.
708    ///
709    /// Order is **spatial** (top-to-bottom, then left-to-right) for zones
710    /// with measured rects; tops within one CSS pixel form a row so sub-pixel
711    /// layout jitter cannot override horizontal reading order. Call
712    /// [`Self::refresh_rects`] first, as the built-in keyboard interaction
713    /// does on pickup. Unmeasured zones keep registration order afterwards.
714    pub fn step_zone(&self, current: Option<ZoneId>, payload: &T, step: isize) -> Option<ZoneId> {
715        let mut zones = self.acceptable(payload);
716        spatial_sort(&mut zones, self.direction());
717        let current_ix = current.and_then(|c| zones.iter().position(|z| z.id == c));
718        cycle(zones.len(), current_ix, step).map(|ix| zones[ix].id)
719    }
720
721    pub fn step_zone_query(
722        &self,
723        current: Option<ZoneId>,
724        query: &DropQuery<T>,
725        step: isize,
726    ) -> Option<ZoneId> {
727        let mut zones = self.acceptable_query(query);
728        spatial_sort(&mut zones, self.direction());
729        let current_ix = current.and_then(|candidate| zones.iter().position(|z| z.id == candidate));
730        cycle(zones.len(), current_ix, step).map(|index| zones[index].id)
731    }
732
733    /// The parent of a zone, if it's nested.
734    pub fn parent_of(&self, id: ZoneId) -> Option<ZoneId> {
735        self.zones
736            .try_peek()
737            .ok()?
738            .iter()
739            .find(|z| z.id == id)?
740            .parent
741    }
742
743    /// Zones directly inside `parent` (`None` = root level) that accept
744    /// `payload`, in spatial order (top-to-bottom, then left-to-right within
745    /// a one-CSS-pixel row band; unmeasured zones keep registration order at
746    /// the end).
747    pub fn children_of(&self, parent: Option<ZoneId>, payload: &T) -> Vec<ZoneRecord<T>> {
748        let mut zones: Vec<_> = self
749            .snapshot()
750            .into_iter()
751            .filter(|zone| zone.record.parent == parent && zone.record.accepts_payload(payload))
752            .map(|zone| zone.record)
753            .collect();
754        spatial_sort(&mut zones, self.direction());
755        zones
756    }
757
758    pub fn children_of_query(
759        &self,
760        parent: Option<ZoneId>,
761        query: &DropQuery<T>,
762    ) -> Vec<ZoneRecord<T>> {
763        let mut zones: Vec<_> = self
764            .snapshot()
765            .into_iter()
766            .filter(|zone| zone.record.parent == parent && zone.negotiate(query).is_some())
767            .map(|zone| zone.record)
768            .collect();
769        spatial_sort(&mut zones, self.direction());
770        zones
771    }
772
773    /// Next/previous zone (cyclic) among the *siblings* of `current` -
774    /// zones sharing its parent. With no `current`, cycles the root level.
775    pub fn step_sibling(
776        &self,
777        current: Option<ZoneId>,
778        payload: &T,
779        step: isize,
780    ) -> Option<ZoneId> {
781        let parent = current.and_then(|c| self.parent_of(c));
782        let siblings = self.children_of(parent, payload);
783        let current_ix = current.and_then(|c| siblings.iter().position(|z| z.id == c));
784        cycle(siblings.len(), current_ix, step).map(|ix| siblings[ix].id)
785    }
786
787    pub fn step_sibling_query(
788        &self,
789        current: Option<ZoneId>,
790        query: &DropQuery<T>,
791        step: isize,
792    ) -> Option<ZoneId> {
793        let parent = current.and_then(|candidate| self.parent_of(candidate));
794        let siblings = self.children_of_query(parent, query);
795        let current_ix =
796            current.and_then(|candidate| siblings.iter().position(|zone| zone.id == candidate));
797        cycle(siblings.len(), current_ix, step).map(|index| siblings[index].id)
798    }
799
800    /// The first (spatially) acceptable zone nested inside `id`.
801    pub fn first_child(&self, id: ZoneId, payload: &T) -> Option<ZoneId> {
802        self.children_of(Some(id), payload).first().map(|z| z.id)
803    }
804
805    pub fn first_child_query(&self, id: ZoneId, query: &DropQuery<T>) -> Option<ZoneId> {
806        self.children_of_query(Some(id), query)
807            .first()
808            .map(|zone| zone.id)
809    }
810
811    /// Last record in registry order containing `point` (client coordinates),
812    /// using cached rects - call [`Self::refresh_rects`] when a drag starts.
813    /// This only approximates DOM paint order; CSS stacking and portals are
814    /// not inspected. Replacing a same-id record retains its existing slot.
815    pub fn hit_test(&self, point: Point) -> Option<ZoneId> {
816        self.zones
817            .try_peek()
818            .ok()?
819            .iter()
820            .rev()
821            .find(|z| z.cached_rect().map(|r| r.contains(point)).unwrap_or(false))
822            .map(|z| z.id)
823    }
824
825    /// Like [`Self::hit_test`], but acceptance-aware: it returns the last
826    /// record in registry order that both contains the point **and** accepts
827    /// `payload`, and when no such zone contains the point, falls back to the
828    /// acceptable zone whose *rect* is nearest - within `max_distance` CSS px
829    /// of its closest edge, not its center, so a large zone snaps a release
830    /// right beside it even though its center sits far away. Skipping zones
831    /// that reject the payload lets a drop land on an earlier accepting
832    /// overlap, and is friendlier for imprecise (touch) drops that land in the
833    /// gutter between zones.
834    pub fn hit_test_closest(&self, point: Point, payload: &T, max_distance: f64) -> Option<ZoneId> {
835        let zones = self.snapshot();
836        let mut best: Option<(ZoneId, f64)> = None;
837        // One borrowed pass: the former miss path built and cloned an entire
838        // `Vec<ZoneRecord<T>>`, then evaluated every acceptance filter twice.
839        for z in zones.iter().rev() {
840            if !z.record.accepts_payload(payload) {
841                continue;
842            }
843            let Some(r) = z.record.cached_rect() else {
844                continue;
845            };
846            if r.contains(point) {
847                return Some(z.record.id);
848            }
849            // Distance to the rect's nearest point (zero on either axis the
850            // point already overlaps), not to its center.
851            let dx = (r.x - point.x).max(point.x - (r.x + r.width)).max(0.0);
852            let dy = (r.y - point.y).max(point.y - (r.y + r.height)).max(0.0);
853            let d = (dx * dx + dy * dy).sqrt();
854            // Reverse iteration preserves direct-hit precedence. Replacing on
855            // an equal distance preserves the old fallback tie-break: the
856            // earlier record in registry order wins.
857            if d <= max_distance && best.map(|(_, bd)| d <= bd).unwrap_or(true) {
858                best = Some((z.record.id, d));
859            }
860        }
861        best.map(|(id, _)| id)
862    }
863
864    /// Resolve a target with this registry's collision policy and return the
865    /// target-negotiated effect. Candidates are filtered by the full query
866    /// before collision ranking, so hover and release share acceptance.
867    pub fn resolve(
868        &self,
869        query: &DropQuery<T>,
870        point: Point,
871        active_rect: Option<Rect>,
872        max_distance: f64,
873    ) -> Option<(ZoneId, DropEffect)> {
874        let zones = self.snapshot();
875        let accepted: Vec<_> = zones
876            .iter()
877            .enumerate()
878            .filter_map(|(order, zone)| {
879                let effect = zone.negotiate(query)?;
880                Some((
881                    ZoneCandidate {
882                        id: zone.record.id,
883                        rect: zone.record.cached_rect()?,
884                        order,
885                    },
886                    effect,
887                ))
888            })
889            .collect();
890        let candidates = accepted.iter().map(|(candidate, _)| *candidate).collect();
891        let policy = self.release_policy();
892        let max_distance = max_distance.max(0.0);
893        let ranked = match policy.collision {
894            CollisionDetector::BuiltIn(strategy) => {
895                rank_builtin_candidates(strategy, point, active_rect, candidates, max_distance)
896            }
897            CollisionDetector::Custom(callback) => rank_collisions(
898                CollisionDetector::Custom(callback),
899                CollisionRequest {
900                    pointer: point,
901                    active_rect,
902                    payload: query.payload.clone(),
903                    candidates,
904                    max_distance,
905                },
906            ),
907        };
908        for collision in ranked {
909            if let Some((candidate, effect)) = accepted
910                .iter()
911                .find(|(candidate, _)| candidate.id == collision.zone)
912            {
913                return Some((candidate.id, *effect));
914            }
915        }
916        None
917    }
918
919    /// Resolve live hover with optional sticky retention. Exact collisions
920    /// always win. On an exact miss, a sticky policy may retain only the
921    /// current acceptable target while the pointer remains inside its
922    /// recovery radius.
923    pub fn resolve_hover(
924        &self,
925        query: &DropQuery<T>,
926        point: Point,
927        active_rect: Option<Rect>,
928        current: Option<ZoneId>,
929    ) -> Option<(ZoneId, DropEffect)> {
930        if let Some(hit) = self.resolve(query, point, active_rect, 0.0) {
931            return Some(hit);
932        }
933        let policy = self.release_policy();
934        if !policy.sticky {
935            return None;
936        }
937        let current = current?;
938        let zone = self.negotiate_zone(current, query)?;
939        let rect = zone.record.cached_rect()?;
940        (crate::core::collision::point_rect_distance(point, rect) <= policy.recovery_radius)
941            .then_some((current, zone.effect))
942    }
943
944    /// Re-measure every mounted zone's client rect and **wait** for the
945    /// measurements to land - unlike [`Self::refresh_rects`], which fires
946    /// and forgets. Use before a hit-test that must see fresh geometry
947    /// (e.g. retrying a missed touch drop after a layout change).
948    pub async fn measure_all(&self) {
949        let zones = self.measurement_targets();
950        for (registration, mounted) in zones {
951            if let Ok(r) = mounted.get_client_rect().await {
952                // The zone can unmount or be replaced during the await (a
953                // closing window mid-drag is the common case). The
954                // generation check quietly drops that stale measurement.
955                let mut registry = *self;
956                registry.set_rect_if_present(
957                    registration,
958                    Rect::new(r.origin.x, r.origin.y, r.size.width, r.size.height),
959                );
960            }
961        }
962    }
963
964    /// Re-measure every mounted zone's client rect (async, spawned).
965    pub fn refresh_rects(&self) {
966        self.spawn_rect_refresh(None);
967    }
968
969    /// Re-measure every mounted zone in parallel, then notify the caller
970    /// once the whole batch has settled. The completion is used internally
971    /// to repeat hover resolution against the geometry that actually landed;
972    /// without that ordering, a final pointer move can race the async DOM
973    /// measurements and leave the previous zone highlighted.
974    pub(crate) fn refresh_rects_then(&self, on_complete: impl Fn() + 'static) {
975        self.spawn_rect_refresh(Some(Rc::new(on_complete)));
976    }
977
978    fn spawn_rect_refresh(&self, on_complete: Option<Rc<dyn Fn()>>) {
979        let targets = self.measurement_targets();
980        if targets.is_empty() {
981            if let Some(on_complete) = on_complete {
982                on_complete();
983            }
984            return;
985        }
986
987        let remaining = on_complete.map(|callback| (Rc::new(Cell::new(targets.len())), callback));
988        for (registration, mounted) in targets {
989            let mut registry = *self;
990            let remaining = remaining.clone();
991            spawn(async move {
992                if let Ok(r) = mounted.get_client_rect().await {
993                    // See measure_all: the zone can die or be replaced
994                    // while this measurement is in flight.
995                    registry.set_rect_if_present(
996                        registration,
997                        Rect::new(r.origin.x, r.origin.y, r.size.width, r.size.height),
998                    );
999                }
1000                if let Some((remaining, on_complete)) = remaining {
1001                    let pending = remaining.get();
1002                    debug_assert!(pending > 0, "rect refresh completion counted twice");
1003                    remaining.set(pending.saturating_sub(1));
1004                    if pending == 1 {
1005                        on_complete();
1006                    }
1007                }
1008            });
1009        }
1010    }
1011
1012    /// Subscribe an effect to registration/mount changes without also
1013    /// subscribing it to rect writes in the main registry vector.
1014    pub(crate) fn track_mounts(&self) {
1015        let _ = self.mount_revision.try_read();
1016    }
1017
1018    fn measurement_targets(&self) -> Vec<(ZoneRegistration, Rc<MountedData>)> {
1019        let registrations = self
1020            .registrations
1021            .try_peek()
1022            .map(|registrations| registrations.clone())
1023            .unwrap_or_default();
1024        self.zones
1025            .try_peek()
1026            .map(|zones| {
1027                zones
1028                    .iter()
1029                    .filter_map(|zone| {
1030                        let mounted = zone.mounted_handle()?;
1031                        let generation = registrations
1032                            .iter()
1033                            .find(|(id, _)| *id == zone.id)
1034                            .map(|(_, generation)| *generation)?;
1035                        Some((
1036                            ZoneRegistration {
1037                                id: zone.id,
1038                                generation,
1039                            },
1040                            mounted,
1041                        ))
1042                    })
1043                    .collect()
1044            })
1045            .unwrap_or_default()
1046    }
1047
1048    fn is_current(&self, registration: ZoneRegistration, operation: &'static str) -> bool {
1049        match self.registrations.try_peek() {
1050            Ok(registrations) => registrations.iter().any(|(id, generation)| {
1051                *id == registration.id && *generation == registration.generation
1052            }),
1053            Err(error) => {
1054                trace_registry_failure(
1055                    operation,
1056                    "registrations",
1057                    Some(registration.id),
1058                    Some(registration.generation),
1059                    &error,
1060                );
1061                false
1062            }
1063        }
1064    }
1065
1066    fn current_registration(
1067        &self,
1068        id: ZoneId,
1069        operation: &'static str,
1070    ) -> Option<ZoneRegistration> {
1071        match self.registrations.try_peek() {
1072            Ok(registrations) => registrations
1073                .iter()
1074                .find(|(registered_id, _)| *registered_id == id)
1075                .map(|(_, generation)| ZoneRegistration {
1076                    id,
1077                    generation: *generation,
1078                }),
1079            Err(error) => {
1080                trace_registry_failure(operation, "registrations", Some(id), None, &error);
1081                None
1082            }
1083        }
1084    }
1085
1086    fn bump_mount_revision(&mut self) {
1087        match self.mount_revision.try_write() {
1088            Ok(mut revision) => *revision = revision.wrapping_add(1),
1089            Err(error) => {
1090                trace_registry_failure("bump_mount_revision", "mount_revision", None, None, &error)
1091            }
1092        }
1093    }
1094}
1095
1096/// A payload-type-erased "re-measure your zones" channel, shared by every
1097/// registry under one provider tree.
1098///
1099/// Cached client rects go stale the moment layout moves under a live drag -
1100/// scrolling being the everyday case. Registries are per payload type, but
1101/// the things that move layout (an auto-scrolling container, your own
1102/// scroll surface, a collapsing panel) shouldn't need to know any payload
1103/// type to say "geometry changed". Each provider registers a thunk here
1104/// that re-measures its own registry **only while it has a drag in
1105/// flight**, so pinging the channel from every scroll event costs nothing
1106/// while idle.
1107///
1108/// [`crate::autoscroll::AutoScroll`] pings this automatically after every
1109/// scroll it performs (and on any other scroll of its container); grab the
1110/// channel with [`crate::core::hooks::use_rect_refresh`] to wire up custom
1111/// layout mutators.
1112pub struct RectRefresh {
1113    thunks: Signal<Vec<(u64, Callback<()>)>>,
1114}
1115
1116impl Copy for RectRefresh {}
1117impl Clone for RectRefresh {
1118    fn clone(&self) -> Self {
1119        *self
1120    }
1121}
1122impl PartialEq for RectRefresh {
1123    fn eq(&self, other: &Self) -> bool {
1124        self.thunks == other.thunks
1125    }
1126}
1127
1128impl RectRefresh {
1129    /// Wrap an existing signal. Prefer [`crate::core::hooks::use_dnd_provider`],
1130    /// which creates one per provider *tree* (nested providers inherit and
1131    /// re-provide the outermost channel).
1132    pub fn from_signal(thunks: Signal<Vec<(u64, Callback<()>)>>) -> Self {
1133        Self { thunks }
1134    }
1135
1136    /// Ask every provider in the tree to re-measure its zones. Providers
1137    /// without a drag in flight ignore the ping, so this is safe to call
1138    /// from high-frequency sources like scroll events.
1139    pub fn refresh_all(&self) {
1140        for (_, thunk) in self.thunks.peek().iter() {
1141            thunk.call(());
1142        }
1143    }
1144
1145    /// Number of registered providers. Diagnostics and tests.
1146    pub fn len(&self) -> usize {
1147        self.thunks.peek().len()
1148    }
1149
1150    /// Whether any provider is registered.
1151    pub fn is_empty(&self) -> bool {
1152        self.len() == 0
1153    }
1154
1155    /// Add (or replace, by key) a provider's re-measure thunk.
1156    pub(crate) fn register(&mut self, key: u64, thunk: Callback<()>) {
1157        let mut thunks = self.thunks.write();
1158        if let Some(existing) = thunks.iter_mut().find(|(k, _)| *k == key) {
1159            existing.1 = thunk;
1160        } else {
1161            thunks.push((key, thunk));
1162        }
1163    }
1164
1165    /// Remove a provider's thunk (call when the provider unmounts).
1166    pub(crate) fn unregister(&mut self, key: u64) {
1167        self.thunks.write().retain(|(k, _)| *k != key);
1168    }
1169}
1170
1171/// Sort zones spatially: measured rects by row then reading order, unmeasured
1172/// last in their original relative order. Tops within one CSS pixel form a
1173/// row so sub-pixel layout jitter cannot override horizontal reading order.
1174/// Reading order is left-to-right in LTR and right-to-left in RTL.
1175fn spatial_sort<T: Clone + 'static>(zones: &mut [ZoneRecord<T>], dir: Direction) {
1176    const ROW_TOP_SLOP: f64 = 1.0;
1177
1178    // First establish a total, stable vertical order and move unmeasured
1179    // records to the end. Row tolerance cannot live inside this comparator:
1180    // pairwise "close enough" comparisons are non-transitive.
1181    zones.sort_by(|a, b| match (a.cached_rect(), b.cached_rect()) {
1182        (Some(ra), Some(rb)) => ra.y.total_cmp(&rb.y),
1183        (Some(_), None) => std::cmp::Ordering::Less,
1184        (None, Some(_)) => std::cmp::Ordering::Greater,
1185        (None, None) => std::cmp::Ordering::Equal,
1186    });
1187
1188    let measured = zones
1189        .iter()
1190        .position(|zone| zone.cached_rect().is_none())
1191        .unwrap_or(zones.len());
1192    let mut row_start = 0;
1193    while row_start < measured {
1194        let row_y = zones[row_start].cached_rect().unwrap().y;
1195        let mut row_end = row_start + 1;
1196        while row_end < measured {
1197            let y = zones[row_end].cached_rect().unwrap().y;
1198            if !row_y.is_finite() || !y.is_finite() || (y - row_y).abs() > ROW_TOP_SLOP {
1199                break;
1200            }
1201            row_end += 1;
1202        }
1203        zones[row_start..row_end].sort_by(|a, b| {
1204            let ax = a.cached_rect().unwrap().x;
1205            let bx = b.cached_rect().unwrap().x;
1206            match dir {
1207                Direction::Ltr => ax.total_cmp(&bx),
1208                Direction::Rtl => bx.total_cmp(&ax),
1209            }
1210        });
1211        row_start = row_end;
1212    }
1213}
1214
1215/// Cyclic index stepping: `None` current starts at the first (or last)
1216/// element depending on direction. Pure, for testability.
1217pub(crate) fn cycle(len: usize, current: Option<usize>, step: isize) -> Option<usize> {
1218    if len == 0 {
1219        return None;
1220    }
1221    Some(match current {
1222        None => {
1223            if step >= 0 {
1224                0
1225            } else {
1226                len - 1
1227            }
1228        }
1229        Some(ix) => (ix as isize + step).rem_euclid(len as isize) as usize,
1230    })
1231}
1232
1233#[cfg(test)]
1234mod tests {
1235    use std::cell::Cell;
1236    use std::rc::Rc;
1237
1238    use dioxus::prelude::*;
1239
1240    use super::{
1241        cycle, Direction, DropQuery, Point, Rect, ReleasePolicy, ZoneId, ZonePolicy, ZoneRecord,
1242        ZoneRegistry,
1243    };
1244
1245    #[test]
1246    fn cycle_steps_and_wraps() {
1247        assert_eq!(cycle(0, None, 1), None);
1248        assert_eq!(cycle(3, None, 1), Some(0));
1249        assert_eq!(cycle(3, None, -1), Some(2));
1250        assert_eq!(cycle(3, Some(2), 1), Some(0));
1251        assert_eq!(cycle(3, Some(0), -1), Some(2));
1252        assert_eq!(cycle(3, Some(1), 1), Some(2));
1253    }
1254
1255    fn equality_probe() -> Element {
1256        let zones = use_signal(Vec::<ZoneRecord<u8>>::new);
1257        let registrations = use_signal(Vec::<(ZoneId, u64)>::new);
1258        let other_registrations = use_signal(Vec::<(ZoneId, u64)>::new);
1259        let policies = use_signal(Vec::new);
1260        let mount_revision = use_signal(|| 0u64);
1261        let other_mount_revision = use_signal(|| 0u64);
1262        let dir = use_signal(Direction::default);
1263        let release = use_signal(ReleasePolicy::default);
1264        let registry = ZoneRegistry {
1265            zones,
1266            registrations,
1267            policies,
1268            mount_revision,
1269            dir,
1270            release,
1271        };
1272        let copy = registry;
1273
1274        assert!(registry == copy, "a copied handle must compare equal");
1275        assert!(
1276            registry
1277                != ZoneRegistry {
1278                    registrations: other_registrations,
1279                    ..registry
1280                },
1281            "registration identity is part of registry identity"
1282        );
1283        assert!(
1284            registry
1285                != ZoneRegistry {
1286                    mount_revision: other_mount_revision,
1287                    ..registry
1288                },
1289            "mount-revision identity is part of registry identity"
1290        );
1291        rsx! {}
1292    }
1293
1294    #[test]
1295    fn equality_covers_every_registry_storage_handle() {
1296        let mut dom = VirtualDom::new(equality_probe);
1297        dom.rebuild_in_place();
1298    }
1299
1300    fn single_negotiation_probe() -> Element {
1301        let calls = Rc::new(Cell::new(0));
1302        let observed_calls = calls.clone();
1303        let mut registry = ZoneRegistry::from_signal(Signal::new(Vec::<ZoneRecord<u8>>::new()));
1304        let record = ZoneRecord::new(ZoneId(1), Callback::new(|_| {}));
1305        let registration = registry.register_with_policy(
1306            record,
1307            ZonePolicy {
1308                accepts_query: Some(Callback::new(move |_| {
1309                    calls.set(calls.get() + 1);
1310                    true
1311                })),
1312                ..ZonePolicy::default()
1313            },
1314        );
1315        registry.set_rect_if_present(registration, Rect::new(0.0, 0.0, 20.0, 20.0));
1316
1317        assert_eq!(
1318            registry.resolve(&DropQuery::new(7), Point::new(10.0, 10.0), None, 0.0,),
1319            Some((ZoneId(1), crate::core::DropEffect::Move))
1320        );
1321        assert_eq!(
1322            observed_calls.get(),
1323            1,
1324            "one hit-test must evaluate target policy once"
1325        );
1326        rsx! {}
1327    }
1328
1329    #[test]
1330    fn resolution_negotiates_each_candidate_once() {
1331        let mut dom = VirtualDom::new(single_negotiation_probe);
1332        dom.rebuild_in_place();
1333    }
1334
1335    fn reentrant_acceptance_probe() -> Element {
1336        let mut registry = ZoneRegistry::from_signal(Signal::new(Vec::<ZoneRecord<u8>>::new()));
1337        let mut callback_registry = registry;
1338        let mut record = ZoneRecord::new(ZoneId(1), Callback::new(|_| {}));
1339        record.accepts = Some(Callback::new(move |_| {
1340            callback_registry.register(ZoneRecord::new(ZoneId(2), Callback::new(|_| {})));
1341            true
1342        }));
1343        registry.register(record);
1344
1345        let acceptable = registry.acceptable(&7);
1346        assert_eq!(acceptable.len(), 1);
1347        assert!(
1348            registry.contains(ZoneId(2)),
1349            "acceptance callbacks must be able to mutate the registry"
1350        );
1351        rsx! {}
1352    }
1353
1354    #[test]
1355    fn acceptance_callbacks_run_without_a_registry_borrow() {
1356        let mut dom = VirtualDom::new(reentrant_acceptance_probe);
1357        dom.rebuild_in_place();
1358    }
1359
1360    fn structural_borrow_probe() -> Element {
1361        let zones = use_signal(Vec::<ZoneRecord<u8>>::new);
1362        let mut registry = ZoneRegistry::from_signal(zones);
1363        let record = |id: u64| ZoneRecord {
1364            id: ZoneId(id),
1365            parent: None,
1366            label: None,
1367            on_drop: Callback::new(|_| {}),
1368            accepts: None,
1369            mounted: None,
1370            rect: Some(Rect::new(0.0, 0.0, 10.0, 10.0)),
1371        };
1372        registry.register(record(1));
1373
1374        // If the zone half is borrowed, registration changes neither half.
1375        {
1376            let zones = registry.zones;
1377            let _zones = zones.read();
1378            registry.register(record(2));
1379        }
1380        assert!(registry.get(ZoneId(2)).is_none());
1381        assert!(registry.current_registration(ZoneId(2), "test").is_none());
1382
1383        // If the generation half is borrowed, the already-acquired zone
1384        // guard must still be dropped without mutating either vector.
1385        {
1386            let registrations = registry.registrations;
1387            let _registrations = registrations.read();
1388            registry.register(record(3));
1389        }
1390        assert!(registry.get(ZoneId(3)).is_none());
1391        assert!(registry.current_registration(ZoneId(3), "test").is_none());
1392
1393        // Unregister has the same all-or-nothing structural contract.
1394        {
1395            let registrations = registry.registrations;
1396            let _registrations = registrations.read();
1397            registry.unregister(ZoneId(1));
1398        }
1399        assert!(registry.get(ZoneId(1)).is_some());
1400        assert!(registry.current_registration(ZoneId(1), "test").is_some());
1401        rsx! {}
1402    }
1403
1404    #[test]
1405    fn structural_borrow_failures_cannot_split_registry_state() {
1406        let mut dom = VirtualDom::new(structural_borrow_probe);
1407        dom.rebuild_in_place();
1408    }
1409}