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