Skip to main content

dioxus_dnd/core/world/
joined.rs

1//! A provider's world membership and the joined-window handle: qualified
2//! zone resolution, foreign-window lookup, and overlay presentation.
3
4use dioxus::prelude::{ReadableExt, WritableExt};
5
6use crate::core::types::{Point, ZoneId};
7
8use super::geometry::{WindowGeometry, WindowKey};
9use super::state::{DndWorld, WindowRecord, ZoneLocation};
10
11/// This provider tree's world membership: which world it joined and as
12/// which window. Every provider provides one (with `None` inside when it
13/// created isolated state), so nested providers shadow their ancestors'
14/// membership exactly like they shadow drag contexts.
15pub(crate) struct WorldMembership<T: Clone + 'static>(pub(crate) Option<JoinedWindow<T>>);
16
17impl<T: Clone + 'static> Copy for WorldMembership<T> {}
18impl<T: Clone + 'static> Clone for WorldMembership<T> {
19    fn clone(&self) -> Self {
20        *self
21    }
22}
23
24/// A provider's handle to the world it joined: the world, this window's
25/// key, and this window's geometry - everything the pointer path needs to
26/// think cross-window.
27pub struct JoinedWindow<T: Clone + 'static> {
28    pub world: DndWorld<T>,
29    pub key: WindowKey,
30    pub geometry: WindowGeometry,
31}
32
33impl<T: Clone + 'static> Copy for JoinedWindow<T> {}
34impl<T: Clone + 'static> Clone for JoinedWindow<T> {
35    fn clone(&self) -> Self {
36        *self
37    }
38}
39impl<T: Clone + 'static> PartialEq for JoinedWindow<T> {
40    fn eq(&self, other: &Self) -> bool {
41        self.key == other.key && self.world == other.world
42    }
43}
44
45/// What the world made of a pointer position (client px of the joined
46/// window asking).
47pub(crate) enum WorldHit {
48    /// Some window's zone is under the pointer.
49    Zone(ZoneLocation),
50    /// A window is under the pointer, but no zone in it.
51    Window,
52    /// The world can't resolve the point (no geometry, or outside every
53    /// window) - fall back to window-local behavior.
54    Unresolved,
55}
56
57impl<T: Clone + 'static> JoinedWindow<T> {
58    /// Resolve a point in **this window's client px** to whichever window's
59    /// zone lies under it.
60    pub(crate) fn zone_under(&self, client: Point) -> WorldHit {
61        let Some(global) = self.geometry.to_global(client) else {
62            return WorldHit::Unresolved;
63        };
64        let mut global_pointer = self.world.global_pointer;
65        if *global_pointer.peek() != Some(global) {
66            global_pointer.set(Some(global));
67        }
68        let Some((rec, local)) = self.world.resolve_global(global) else {
69            return WorldHit::Unresolved;
70        };
71        match rec.registry.hit_test(local) {
72            Some(zone) => WorldHit::Zone(ZoneLocation {
73                window: rec.key,
74                zone,
75            }),
76            None => WorldHit::Window,
77        }
78    }
79
80    /// Qualify one of this window's local zone ids for world state.
81    pub fn location(&self, zone: ZoneId) -> ZoneLocation {
82        ZoneLocation {
83            window: self.key,
84            zone,
85        }
86    }
87
88    /// Mark a window-qualified zone as hovered. Custom world-aware sources
89    /// should use this rather than the legacy id-only context method.
90    pub fn enter(&self, location: ZoneLocation) {
91        self.world.enter_location(location);
92    }
93
94    /// Clear both qualified world hover and the legacy context hover.
95    pub fn clear_hover(&self) {
96        self.world.clear_hover();
97    }
98
99    /// Whether this exact window/zone pair owns the world hover.
100    pub fn is_over(&self, zone: ZoneId) -> bool {
101        self.world.over_location() == Some(self.location(zone))
102    }
103
104    /// Latest global pointer converted into this window's client CSS
105    /// coordinates. If geometry disappeared mid-gesture, the origin window
106    /// retains its established context-local fallback.
107    pub fn local_pointer(&self) -> Option<Point> {
108        if let Some(local) = self
109            .world
110            .global_pointer()
111            .and_then(|global| self.geometry.to_client(global))
112        {
113            return Some(local);
114        }
115        (self.world.origin_window() == Some(self.key)).then(|| self.world.ctx.pointer())
116    }
117
118    /// Resolve a point in this window's client px to a **foreign** window
119    /// (and the point in its client px). `None` for the own window, an
120    /// unresolvable point, or no window - callers then run the classic
121    /// local path, preserving single-window semantics exactly.
122    pub(crate) fn foreign_window_under(&self, client: Point) -> Option<(WindowRecord<T>, Point)> {
123        let global = self.geometry.to_global(client)?;
124        let (rec, local) = self.world.resolve_global(global)?;
125        (rec.key != self.key).then_some((rec, local))
126    }
127
128    /// Where this window's overlay should draw the ghost, if this window is
129    /// the presenting one: `Some((top-left in this window's client px,
130    /// origin-to-here scale ratio for size matching))`. `None` means
131    /// another window presents the ghost this frame.
132    ///
133    /// Presentation follows the pointer: whichever window contains the
134    /// global pointer presents; when none does (or no geometry exists), the
135    /// origin window keeps the ghost, anchored to its raw client coords.
136    /// During a settle, the window the drop landed in presents.
137    pub(crate) fn present_overlay(
138        &self,
139        pointer: Point,
140        grab: Point,
141        settling: bool,
142    ) -> Option<(Point, f64)> {
143        let raw = pointer - grab;
144        let Some(active) = self.world.active_drag() else {
145            // The drag didn't register an origin window (custom source):
146            // fall back to raw anchoring everywhere, as before worlds.
147            return Some((raw, 1.0));
148        };
149        let origin = self.world.record(active.origin);
150        let origin_scale = origin
151            .map(|record| record.geometry.scale())
152            .unwrap_or(active.origin_scale);
153        let global_anchor = origin
154            .and_then(|record| record.geometry.to_global(raw))
155            .or_else(|| {
156                self.world.global_pointer().map(|global| {
157                    Point::new(
158                        global.x - grab.x * origin_scale,
159                        global.y - grab.y * origin_scale,
160                    )
161                })
162            });
163        let Some(global_anchor) = global_anchor else {
164            // Origin geometry unknown: only the origin window can place it.
165            return (self.key == active.origin).then_some((raw, 1.0));
166        };
167        let presenting = if settling {
168            self.world.settling_in()?
169        } else {
170            let pointer_global = self
171                .world
172                .global_pointer()
173                .or_else(|| origin.and_then(|record| record.geometry.to_global(pointer)))
174                .unwrap_or(global_anchor);
175            self.world
176                .window_under(pointer_global)
177                .map(|r| r.key)
178                .unwrap_or(active.origin)
179        };
180        if presenting != self.key {
181            return None;
182        }
183        match self.geometry.to_client(global_anchor) {
184            Some(local) => {
185                let own_scale = self.geometry.scale();
186                let ratio = if own_scale > 0.0 {
187                    origin_scale / own_scale
188                } else {
189                    1.0
190                };
191                Some((local, ratio))
192            }
193            // Presenting window without geometry can only be the origin.
194            None => (self.key == active.origin).then_some((raw, 1.0)),
195        }
196    }
197}