Skip to main content

dioxus_dnd/core/world/
settle.rs

1//! Settle-glide presentation routing: which window's overlay presents a
2//! cross-window drop's settle.
3
4use std::sync::atomic::{AtomicU64, Ordering};
5
6use dioxus::prelude::*;
7
8use super::geometry::WindowKey;
9use super::state::DndWorld;
10
11// Identity freshness only: Relaxed is sufficient because the counter carries
12// no synchronization. Correctness assumes this process-lifetime u64 never
13// wraps; do not narrow it.
14static NEXT_SETTLE_GENERATION: AtomicU64 = AtomicU64::new(1);
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub(super) struct SettleClaim {
18    presenter: WindowKey,
19    generation: u64,
20}
21
22impl<T: Clone + 'static> DndWorld<T> {
23    /// Elect `key` to present the next world settle. Custom world delivery
24    /// calls this before [`crate::core::DndContext::take_settling`]; built-in
25    /// delivery claims automatically.
26    ///
27    /// The claim is required, not advisory: in a joined world, only the
28    /// elected window's overlay presents and finishes a settle. A custom
29    /// source that calls `take_settling` without claiming gets no glide
30    /// anywhere; that claimless settle is only cleaned up when its origin
31    /// window closes or the next drag begins.
32    pub fn claim_settle(&self, key: WindowKey) {
33        let mut claim = self.settle_claim;
34        claim.set(Some(SettleClaim {
35            presenter: key,
36            generation: NEXT_SETTLE_GENERATION.fetch_add(1, Ordering::Relaxed),
37        }));
38    }
39
40    // Both token reads intersect the claim with the context's actual settle
41    // state (like `settling_in` does): custom code may cancel or reset the
42    // shared context mid-settle without world cleanup, and a lingering claim
43    // must not keep a `SettleSlot` hidden or an overlay in its settle state.
44
45    pub(crate) fn settle_token(&self, key: WindowKey) -> Option<u64> {
46        self.ctx.settling()?;
47        (*self.settle_claim.read())
48            .filter(|claim| claim.presenter == key)
49            .map(|claim| claim.generation)
50    }
51
52    pub(crate) fn peek_settle_token(&self, key: WindowKey) -> Option<u64> {
53        if !self.ctx.settling_peek() {
54            return None;
55        }
56        (*self.settle_claim.peek())
57            .filter(|claim| claim.presenter == key)
58            .map(|claim| claim.generation)
59    }
60
61    pub(super) fn settle_presenter_is(&self, key: WindowKey) -> bool {
62        self.peek_settle_token(key).is_some()
63    }
64
65    pub(super) fn settle_presenter(&self) -> Option<WindowKey> {
66        self.settle_claim
67            .peek()
68            .as_ref()
69            .map(|claim| claim.presenter)
70    }
71
72    /// Finish a custom or built-in settle from its elected window. Custom
73    /// world overlays should use this rather than finishing the shared
74    /// context directly, so world metadata is cleared with it.
75    pub fn finish_settle_from(&self, key: WindowKey) -> bool {
76        let Some(generation) = self.peek_settle_token(key) else {
77            return false;
78        };
79        self.finish_settle_generation(key, generation)
80    }
81
82    pub(crate) fn finish_settle_generation(&self, key: WindowKey, generation: u64) -> bool {
83        if *self.settle_claim.peek()
84            != Some(SettleClaim {
85                presenter: key,
86                generation,
87            })
88            || self.ctx.settling().is_none()
89        {
90            return false;
91        }
92        let mut ctx = self.ctx;
93        ctx.finish_settle();
94        self.clear_world_state();
95        true
96    }
97
98    /// The window elected to present the current settle glide.
99    pub fn settling_in(&self) -> Option<WindowKey> {
100        self.ctx
101            .settling()
102            .and_then(|_| (*self.settle_claim.read()).map(|claim| claim.presenter))
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use std::cell::RefCell;
109
110    use super::*;
111    use crate::core::{DragMode, DropEffect, Point, Rect};
112
113    thread_local! {
114        static WORLD: RefCell<Option<DndWorld<String>>> = const { RefCell::new(None) };
115    }
116
117    fn test_app() -> Element {
118        let world = use_hook(DndWorld::<String>::new);
119        WORLD.with_borrow_mut(|slot| *slot = Some(world));
120        rsx! {}
121    }
122
123    #[test]
124    fn stale_generation_cannot_finish_its_successor() {
125        let mut dom = VirtualDom::new(test_app);
126        dom.rebuild_in_place();
127        let world = WORLD.with_borrow(|slot| slot.expect("test world"));
128        dom.in_runtime(|| {
129            let origin = WindowKey::auto();
130            let presenter = WindowKey::auto();
131            let mut ctx = world.context();
132            ctx.start(
133                "payload".to_string(),
134                None,
135                Point::new(10.0, 10.0),
136                Point::default(),
137                DropEffect::Move,
138                DragMode::Pointer,
139            );
140            world.begin_from(origin);
141            world.claim_settle(presenter);
142            assert!(ctx.take_settling(Rect::new(0.0, 0.0, 10.0, 10.0)).is_some());
143            let stale = world.settle_token(presenter).unwrap();
144
145            // A successor may elect the same presenter. Only its fresh
146            // generation may finish the shared context.
147            world.claim_settle(presenter);
148            let successor = world.settle_token(presenter).unwrap();
149            assert_ne!(stale, successor);
150            assert!(!world.finish_settle_generation(presenter, stale));
151            assert!(ctx.settling().is_some());
152            assert!(world.finish_settle_generation(presenter, successor));
153            assert!(ctx.payload().is_none());
154        });
155    }
156
157    #[test]
158    fn cancelled_context_settle_retires_the_claim_tokens() {
159        let mut dom = VirtualDom::new(test_app);
160        dom.rebuild_in_place();
161        let world = WORLD.with_borrow(|slot| slot.expect("test world"));
162        dom.in_runtime(|| {
163            let origin = WindowKey::auto();
164            let presenter = WindowKey::auto();
165            let mut ctx = world.context();
166            ctx.start(
167                "payload".to_string(),
168                None,
169                Point::new(10.0, 10.0),
170                Point::default(),
171                DropEffect::Move,
172                DragMode::Pointer,
173            );
174            world.begin_from(origin);
175            world.claim_settle(presenter);
176            assert!(ctx.take_settling(Rect::new(0.0, 0.0, 10.0, 10.0)).is_some());
177            assert!(world.settle_token(presenter).is_some());
178
179            // Custom code may reset the shared context mid-settle without
180            // world cleanup. The claim must stop presenting immediately, so
181            // a `SettleSlot` cannot stay hidden on a settle that no longer
182            // exists.
183            ctx.cancel();
184            assert_eq!(world.settle_token(presenter), None);
185            assert_eq!(world.peek_settle_token(presenter), None);
186            assert_eq!(world.settling_in(), None);
187            assert!(!world.finish_settle_from(presenter));
188        });
189    }
190}