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