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