Skip to main content

dioxus_dnd/core/world/
geometry.rs

1//! Window identity and placement: [`WindowKey`], the pure client-px <->
2//! global-physical-px conversion math, and the host-fed [`WindowGeometry`].
3
4use std::sync::atomic::{AtomicU64, Ordering};
5
6use dioxus::prelude::*;
7
8use crate::core::types::Point;
9
10// Identity/order freshness only: Relaxed is sufficient because these counters
11// carry no synchronization. Correctness assumes these process-lifetime u64
12// counters never wrap; do not narrow them.
13static NEXT_WINDOW_KEY: AtomicU64 = AtomicU64::new(1);
14/// Focus stamps start at 1 so a never-focused window's 0 always loses.
15static NEXT_FOCUS_STAMP: AtomicU64 = AtomicU64::new(1);
16
17/// Identifies one joined window within a [`DndWorld`](super::DndWorld). Process-unique.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
19pub struct WindowKey(pub u64);
20
21impl WindowKey {
22    /// Generate a process-unique window key.
23    pub fn auto() -> Self {
24        Self(NEXT_WINDOW_KEY.fetch_add(1, Ordering::Relaxed))
25    }
26}
27
28// --- pure conversion math (unit-tested; signals stay out of it) --------
29
30/// Client CSS px of a window -> global desktop physical px.
31pub(crate) fn client_to_global(client: Point, origin: Point, scale: f64) -> Point {
32    Point::new(origin.x + client.x * scale, origin.y + client.y * scale)
33}
34
35/// Global desktop physical px -> client CSS px of a window.
36pub(crate) fn global_to_client(global: Point, origin: Point, scale: f64) -> Point {
37    let s = if scale > 0.0 { scale } else { 1.0 };
38    Point::new((global.x - origin.x) / s, (global.y - origin.y) / s)
39}
40
41/// Is `global` inside a window whose client area starts at `origin` with
42/// `size`, both in physical px? Inclusive of edges, like [`crate::core::types::Rect`].
43pub(crate) fn window_contains(global: Point, origin: Point, size: (f64, f64)) -> bool {
44    global.x >= origin.x
45        && global.x <= origin.x + size.0
46        && global.y >= origin.y
47        && global.y <= origin.y + size.1
48}
49
50/// One window's placement on the desktop, as reactive signals the host
51/// feeds. Copy handle; create one per window (the provider creates an inert
52/// one when none is in context) and keep it updated from your windowing
53/// layer. Missing placement or host ineligibility makes it inert: the window
54/// still drags internally, but cannot take part in cross-window hit-testing.
55pub struct WindowGeometry {
56    /// Client-area top-left in global physical px (`inner_position()`).
57    origin: Signal<Option<Point>>,
58    /// Client-area size in physical px.
59    size: Signal<Option<(f64, f64)>>,
60    /// Window scale factor (physical px per CSS px).
61    scale: Signal<f64>,
62    /// Monotonic focus stamp; higher = more recently focused. Breaks ties
63    /// when overlapping windows both contain a point (no z-order queries
64    /// exist on desktop, so focus recency approximates it).
65    focused: Signal<u64>,
66    /// Whether the host currently considers this window eligible for global
67    /// hit-testing (visible, restored, and otherwise interactive).
68    eligible: Signal<bool>,
69}
70
71impl Copy for WindowGeometry {}
72impl Clone for WindowGeometry {
73    fn clone(&self) -> Self {
74        *self
75    }
76}
77impl PartialEq for WindowGeometry {
78    fn eq(&self, other: &Self) -> bool {
79        self.origin == other.origin
80    }
81}
82
83impl Default for WindowGeometry {
84    fn default() -> Self {
85        Self::new()
86    }
87}
88
89impl WindowGeometry {
90    /// A fresh, inert geometry owned by the current scope.
91    pub fn new() -> Self {
92        Self {
93            origin: Signal::new(None),
94            size: Signal::new(None),
95            scale: Signal::new(1.0),
96            focused: Signal::new(0),
97            // Existing hosts only feed placement, so eligibility defaults on
98            // and remains an additive capability gate.
99            eligible: Signal::new(true),
100        }
101    }
102
103    /// Update the window's placement. `origin` and `size` describe the
104    /// client area in global physical px; `scale` is the window's scale
105    /// factor. No-op writes are skipped, so this is safe to call from
106    /// high-frequency window events.
107    pub fn set(&self, origin: Point, size: (f64, f64), scale: f64) {
108        // try_write throughout (here and below): host feeds run from
109        // windowing-layer callbacks that can fire one event after the
110        // owning window's signals died - see the read-side note below.
111        let (mut o, mut sz, mut sc) = (self.origin, self.size, self.scale);
112        if matches!(o.try_peek().as_deref(), Ok(v) if *v != Some(origin)) {
113            if let Ok(mut w) = o.try_write() {
114                *w = Some(origin);
115            }
116        }
117        if matches!(sz.try_peek().as_deref(), Ok(v) if *v != Some(size)) {
118            if let Ok(mut w) = sz.try_write() {
119                *w = Some(size);
120            }
121        }
122        if matches!(sc.try_peek().as_deref(), Ok(v) if *v != scale) {
123            if let Ok(mut w) = sc.try_write() {
124                *w = scale;
125            }
126        }
127    }
128
129    /// Forget the placement (geometry became unavailable); the window keeps
130    /// working as a single-window drag surface.
131    pub fn clear(&self) {
132        let (mut o, mut sz) = (self.origin, self.size);
133        if matches!(o.try_peek().as_deref(), Ok(Some(_))) {
134            if let Ok(mut w) = o.try_write() {
135                *w = None;
136            }
137        }
138        if matches!(sz.try_peek().as_deref(), Ok(Some(_))) {
139            if let Ok(mut w) = sz.try_write() {
140                *w = None;
141            }
142        }
143    }
144
145    /// Include or exclude this window from global hit-testing without
146    /// discarding its last known placement.
147    pub fn set_eligible(&self, eligible: bool) {
148        let mut value = self.eligible;
149        if matches!(value.try_peek().as_deref(), Ok(current) if *current != eligible) {
150            if let Ok(mut writer) = value.try_write() {
151                *writer = eligible;
152            }
153        }
154    }
155
156    /// Whether the host currently allows this window to receive a global
157    /// drag. This is a subscribing, dead-safe read.
158    pub fn eligible(&self) -> bool {
159        self.eligible
160            .try_read()
161            .map(|value| *value)
162            .unwrap_or(false)
163    }
164
165    /// Record that this window was just focused (see `focused`).
166    pub fn mark_focused(&self) {
167        let mut f = self.focused;
168        if let Ok(mut w) = f.try_write() {
169            *w = NEXT_FOCUS_STAMP.fetch_add(1, Ordering::Relaxed);
170        };
171    }
172
173    // Reads below use try_peek and degrade to "geometry unknown" when the
174    // signals are gone. A geometry's signals are host-owned and usually
175    // window-scoped, so they die with their window's VirtualDom - but a
176    // copy inside a WindowRecord (or a handler closure) can race the
177    // pruning and be read one event late. On Windows that read happens
178    // inside a Win32 callback, where the resulting panic cannot unwind
179    // and kills the process with 0xc000041d (observed; the
180    // DioxusLabs/dioxus#4466 failure class). Stale geometry is already a
181    // modeled state (Wayland), so degrading is honest, not a mask.
182
183    /// Is the placement known and currently eligible for global hit-testing?
184    /// This is a subscribing, dead-safe read.
185    pub fn live(&self) -> bool {
186        // Read every input independently so a currently inert geometry still
187        // subscribes to each capability that can make it live later.
188        let has_origin = matches!(self.origin.try_read().as_deref(), Ok(Some(_)));
189        let has_size = matches!(self.size.try_read().as_deref(), Ok(Some(_)));
190        let eligible = self
191            .eligible
192            .try_read()
193            .map(|value| *value)
194            .unwrap_or(false);
195        has_origin && has_size && eligible
196    }
197
198    fn origin_scale(&self) -> Option<(Point, f64)> {
199        let origin = (*self.origin.try_peek().ok()?)?;
200        let scale = self.scale.try_peek().map(|s| *s).unwrap_or(1.0);
201        Some((origin, scale))
202    }
203
204    /// This window's client CSS px -> global physical px. `None` until the
205    /// placement is known.
206    pub fn to_global(&self, client: Point) -> Option<Point> {
207        let (origin, scale) = self.origin_scale()?;
208        Some(client_to_global(client, origin, scale))
209    }
210
211    /// Global physical px -> this window's client CSS px. `None` until the
212    /// placement is known.
213    pub fn to_client(&self, global: Point) -> Option<Point> {
214        let (origin, scale) = self.origin_scale()?;
215        Some(global_to_client(global, origin, scale))
216    }
217
218    /// Does this eligible window's client area contain `global` (physical
219    /// px)? Always false while placement is unknown or eligibility is off.
220    pub fn contains_global(&self, global: Point) -> bool {
221        // Imperative hit-testing must not subscribe its caller. Eligibility
222        // therefore peeks here even though the public status reads subscribe.
223        if !self
224            .eligible
225            .try_peek()
226            .map(|eligible| *eligible)
227            .unwrap_or(false)
228        {
229            return false;
230        }
231        let origin = self.origin.try_peek().ok().and_then(|o| *o);
232        let size = self.size.try_peek().ok().and_then(|s| *s);
233        match (origin, size) {
234            (Some(origin), Some(size)) => window_contains(global, origin, size),
235            _ => false,
236        }
237    }
238
239    /// The window's scale factor.
240    pub fn scale(&self) -> f64 {
241        self.scale.try_peek().map(|s| *s).unwrap_or(1.0)
242    }
243
244    /// The current focus stamp (0 = never focused).
245    pub fn focus_stamp(&self) -> u64 {
246        self.focused.try_peek().map(|f| *f).unwrap_or(0)
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253
254    #[test]
255    fn conversions_round_trip_under_mixed_scales() {
256        for scale in [1.0, 1.5, 2.0] {
257            let origin = Point::new(1200.0, 300.0);
258            let client = Point::new(80.0, 40.5);
259            let global = client_to_global(client, origin, scale);
260            assert_eq!(
261                global,
262                Point::new(1200.0 + 80.0 * scale, 300.0 + 40.5 * scale)
263            );
264            let back = global_to_client(global, origin, scale);
265            assert!((back.x - client.x).abs() < 1e-9);
266            assert!((back.y - client.y).abs() < 1e-9);
267        }
268    }
269
270    #[test]
271    fn degenerate_scale_does_not_divide_by_zero() {
272        let p = global_to_client(Point::new(10.0, 10.0), Point::new(0.0, 0.0), 0.0);
273        assert_eq!(p, Point::new(10.0, 10.0));
274    }
275
276    #[test]
277    fn window_containment_is_edge_inclusive() {
278        let origin = Point::new(100.0, 100.0);
279        let size = (800.0, 600.0);
280        assert!(window_contains(Point::new(100.0, 100.0), origin, size));
281        assert!(window_contains(Point::new(900.0, 700.0), origin, size));
282        assert!(!window_contains(Point::new(99.9, 100.0), origin, size));
283        assert!(!window_contains(Point::new(901.0, 300.0), origin, size));
284    }
285
286    #[test]
287    fn window_keys_are_unique() {
288        let a = WindowKey::auto();
289        let b = WindowKey::auto();
290        assert_ne!(a, b);
291    }
292}