1
//! Logical and physical coordinate types for the GUI toolkit.
2
//!
3
//! Provides DPI-independent (`Logical*`) and pixel-level (`Physical*`) geometry
4
//! types used throughout layout, rendering, windowing, and hit testing.
5
//! Logical coordinates are scaled by a DPI factor to produce physical coordinates.
6

            
7
// Re-export DragDelta from drag module (moved in code reorganization)
8
pub use crate::drag::{DragDelta, OptionDragDelta};
9

            
10
/// An axis-aligned rectangle in logical (DPI-independent) coordinates.
11
#[derive(Copy, Default, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
12
#[repr(C)]
13
pub struct LogicalRect {
14
    pub origin: LogicalPosition,
15
    pub size: LogicalSize,
16
}
17

            
18
impl core::fmt::Debug for LogicalRect {
19
1000880
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
20
1000880
        write!(f, "{} @ {}", self.size, self.origin)
21
1000880
    }
22
}
23

            
24
impl core::fmt::Display for LogicalRect {
25
68
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
26
68
        write!(f, "{} @ {}", self.size, self.origin)
27
68
    }
28
}
29

            
30
impl LogicalRect {
31
147099
    #[must_use] pub const fn zero() -> Self {
32
147099
        Self::new(LogicalPosition::zero(), LogicalSize::zero())
33
147099
    }
34
5090506
    #[must_use] pub const fn new(origin: LogicalPosition, size: LogicalSize) -> Self {
35
5090506
        Self { origin, size }
36
5090506
    }
37

            
38
    /// Scales all coordinates in-place by the given DPI scale factor.
39
    #[inline]
40
12
    pub fn scale_for_dpi(&mut self, scale_factor: f32) {
41
12
        self.origin.x *= scale_factor;
42
12
        self.origin.y *= scale_factor;
43
12
        self.size.width *= scale_factor;
44
12
        self.size.height *= scale_factor;
45
12
    }
46

            
47
    /// Returns the maximum x coordinate (origin.x + width).
48
    #[inline]
49
6878
    #[must_use] pub fn max_x(&self) -> f32 {
50
6878
        self.origin.x + self.size.width
51
6878
    }
52
    /// Returns the minimum x coordinate (origin.x).
53
    #[inline]
54
7124
    #[must_use] pub const fn min_x(&self) -> f32 {
55
7124
        self.origin.x
56
7124
    }
57
    /// Returns the maximum y coordinate (origin.y + height).
58
    #[inline]
59
6530
    #[must_use] pub fn max_y(&self) -> f32 {
60
6530
        self.origin.y + self.size.height
61
6530
    }
62
    /// Returns the minimum y coordinate (origin.y).
63
    #[inline]
64
6441
    #[must_use] pub const fn min_y(&self) -> f32 {
65
6441
        self.origin.y
66
6441
    }
67

            
68
    /// Returns whether this rectangle intersects with another rectangle
69
    #[inline]
70
105
    #[must_use] pub fn intersects(&self, other: Self) -> bool {
71
        // Check if one rectangle is to the left of the other
72
105
        if self.max_x() <= other.min_x() || other.max_x() <= self.min_x() {
73
59
            return false;
74
46
        }
75

            
76
        // Check if one rectangle is above the other
77
46
        if self.max_y() <= other.min_y() || other.max_y() <= self.min_y() {
78
            return false;
79
46
        }
80

            
81
        // If we got here, the rectangles must intersect
82
46
        true
83
105
    }
84

            
85
    /// Returns whether this rectangle contains the given point
86
    #[inline]
87
575
    #[must_use] pub fn contains(&self, point: LogicalPosition) -> bool {
88
575
        point.x >= self.min_x()
89
226
            && point.x < self.max_x()
90
75
            && point.y >= self.min_y()
91
53
            && point.y < self.max_y()
92
575
    }
93

            
94
    /// Same as `contains()`, but returns the (x, y) offset of the hit point
95
    ///
96
    /// On a regular computer this function takes ~3.2ns to run
97
    #[inline]
98
420
    #[must_use] pub fn hit_test(&self, other: &LogicalPosition) -> Option<LogicalPosition> {
99
420
        let dx_left_edge = other.x - self.min_x();
100
420
        let dx_right_edge = self.max_x() - other.x;
101
420
        let dy_top_edge = other.y - self.min_y();
102
420
        let dy_bottom_edge = self.max_y() - other.y;
103
        // Edge semantics must match `contains`: left/top inclusive (`>= min`),
104
        // right/bottom exclusive (`< max`). Previously all four edges were
105
        // exclusive, so a point exactly on the left/top edge hit-tested as a
106
        // miss even though `contains` reported it inside — dropping/duplicating
107
        // hits on shared edges between adjacent rects.
108
420
        if dx_left_edge >= 0.0 && dx_right_edge > 0.0 && dy_top_edge >= 0.0 && dy_bottom_edge > 0.0 {
109
48
            Some(LogicalPosition::new(dx_left_edge, dy_top_edge))
110
        } else {
111
372
            None
112
        }
113
420
    }
114

            
115
}
116

            
117
impl_vec!(LogicalRect, LogicalRectVec, LogicalRectVecDestructor, LogicalRectVecDestructorType, LogicalRectVecSlice, OptionLogicalRect);
118
impl_vec_clone!(LogicalRect, LogicalRectVec, LogicalRectVecDestructor);
119
impl_vec_debug!(LogicalRect, LogicalRectVec);
120
impl_vec_partialeq!(LogicalRect, LogicalRectVec);
121
impl_vec_partialord!(LogicalRect, LogicalRectVec);
122
impl_vec_ord!(LogicalRect, LogicalRectVec);
123
impl_vec_hash!(LogicalRect, LogicalRectVec);
124
impl_vec_eq!(LogicalRect, LogicalRectVec);
125

            
126
use core::{
127
    cmp::Ordering,
128
    hash::{Hash, Hasher},
129
    ops::{self, AddAssign, SubAssign},
130
};
131

            
132
use azul_css::props::layout::LayoutWritingMode;
133

            
134
/// A 2D position in logical (DPI-independent) coordinates.
135
// PartialEq is hand-implemented over `quantize()` (see below) so that equality
136
// agrees with the quantized `Ord`/`Hash`. A derived field-wise `PartialEq`
137
// compared raw f32, so `a == b` could be false while `a.cmp(b) == Equal`,
138
// breaking `BTreeMap`/`HashMap` lookups keyed on these types.
139
#[derive(Default, Copy, Clone)]
140
#[repr(C)]
141
pub struct LogicalPosition {
142
    pub x: f32,
143
    pub y: f32,
144
}
145

            
146
impl PartialEq for LogicalPosition {
147
867673
    fn eq(&self, other: &Self) -> bool {
148
867673
        quantize(self.x) == quantize(other.x) && quantize(self.y) == quantize(other.y)
149
867673
    }
150
}
151

            
152
impl LogicalPosition {
153
    /// Scales the position in-place by the given DPI scale factor.
154
22
    pub fn scale_for_dpi(&mut self, scale_factor: f32) {
155
22
        self.x *= scale_factor;
156
22
        self.y *= scale_factor;
157
22
    }
158
}
159

            
160
impl SubAssign<Self> for LogicalPosition {
161
1
    fn sub_assign(&mut self, other: Self) {
162
1
        self.x -= other.x;
163
1
        self.y -= other.y;
164
1
    }
165
}
166

            
167
impl AddAssign<Self> for LogicalPosition {
168
1
    fn add_assign(&mut self, other: Self) {
169
1
        self.x += other.x;
170
1
        self.y += other.y;
171
1
    }
172
}
173

            
174
impl core::fmt::Debug for LogicalPosition {
175
54459
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
176
54459
        write!(f, "({}, {})", self.x, self.y)
177
54459
    }
178
}
179

            
180
impl core::fmt::Display for LogicalPosition {
181
1000950
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
182
1000950
        write!(f, "({}, {})", self.x, self.y)
183
1000950
    }
184
}
185

            
186
impl ops::Add for LogicalPosition {
187
    type Output = Self;
188

            
189
    #[inline]
190
2
    fn add(self, other: Self) -> Self {
191
2
        Self {
192
2
            x: self.x + other.x,
193
2
            y: self.y + other.y,
194
2
        }
195
2
    }
196
}
197

            
198
impl ops::Sub for LogicalPosition {
199
    type Output = Self;
200

            
201
    #[inline]
202
1
    fn sub(self, other: Self) -> Self {
203
1
        Self {
204
1
            x: self.x - other.x,
205
1
            y: self.y - other.y,
206
1
        }
207
1
    }
208
}
209

            
210
/// Multiplier for converting f32 coordinates to integers in Ord/Hash impls.
211
/// Provides ~0.001 precision, sufficient for sub-pixel layout coordinates.
212
const DECIMAL_MULTIPLIER: f32 = 1000.0;
213

            
214
/// Quantizes an f32 coordinate to fixed-point for stable `Ord`/`Hash`/`PartialEq`
215
/// (comparing raw f32 bit patterns would be unstable / non-total).
216
// intentional fixed-point quantization: the truncation IS the rounding step.
217
#[allow(clippy::cast_possible_truncation)]
218
7943401
fn quantize(value: f32) -> i64 {
219
    // NaN has no meaningful position in a total order. Map it to a single fixed
220
    // sentinel (`i64::MIN`) so all NaNs compare equal to each other and sort
221
    // below every real value — and, critically, do NOT collide with `0.0`
222
    // (the old `NaN as isize == 0` behaviour aliased NaN onto the origin).
223
7943401
    if value.is_nan() {
224
103122
        return i64::MIN;
225
7840279
    }
226
    // `f32 as i64` saturates on overflow (since Rust 1.45), so an out-of-range
227
    // coordinate clamps to `i64::{MIN,MAX}` instead of wrapping. `isize` was
228
    // only 32-bit on wasm32, so a large coordinate overflowed there — `i64` is
229
    // wide enough on every target.
230
7840279
    (value * DECIMAL_MULTIPLIER) as i64
231
7943401
}
232

            
233
impl_option!(
234
    LogicalPosition,
235
    OptionLogicalPosition,
236
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd]
237
);
238

            
239
// PartialOrd delegates to the quantized Ord (the derived field-wise PartialOrd
240
// compared raw f32 and diverged from this quantized order — a latent bug).
241
impl PartialOrd for LogicalPosition {
242
4104
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
243
4104
        Some(self.cmp(other))
244
4104
    }
245
}
246
impl Ord for LogicalPosition {
247
183512
    fn cmp(&self, other: &Self) -> Ordering {
248
183512
        let self_x = quantize(self.x);
249
183512
        let self_y = quantize(self.y);
250
183512
        let other_x = quantize(other.x);
251
183512
        let other_y = quantize(other.y);
252
183512
        self_x.cmp(&other_x).then(self_y.cmp(&other_y))
253
183512
    }
254
}
255

            
256
impl Eq for LogicalPosition {}
257

            
258
impl Hash for LogicalPosition {
259
532
    fn hash<H>(&self, state: &mut H)
260
532
    where
261
532
        H: Hasher,
262
    {
263
532
        let self_x = quantize(self.x);
264
532
        let self_y = quantize(self.y);
265
532
        self_x.hash(state);
266
532
        self_y.hash(state);
267
532
    }
268
}
269

            
270
impl LogicalPosition {
271
    /// Returns the main-axis component for the given writing mode.
272
12706
    #[must_use] pub const fn main(&self, wm: LayoutWritingMode) -> f32 {
273
12706
        match wm {
274
12566
            LayoutWritingMode::HorizontalTb => self.y,
275
140
            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => self.x,
276
        }
277
12706
    }
278

            
279
    /// Returns the cross-axis component for the given writing mode.
280
12156
    #[must_use] pub const fn cross(&self, wm: LayoutWritingMode) -> f32 {
281
12156
        match wm {
282
12016
            LayoutWritingMode::HorizontalTb => self.x,
283
140
            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => self.y,
284
        }
285
12156
    }
286

            
287
    /// Creates a `LogicalPosition` from main and cross axis dimensions.
288
53217
    #[must_use] pub const fn from_main_cross(main: f32, cross: f32, wm: LayoutWritingMode) -> Self {
289
53217
        match wm {
290
53069
            LayoutWritingMode::HorizontalTb => Self::new(cross, main),
291
148
            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => Self::new(main, cross),
292
        }
293
53217
    }
294
}
295

            
296
/// A 2D size in logical (DPI-independent) coordinates.
297
// PartialEq is hand-implemented over `quantize()` to agree with the quantized
298
// `Ord`/`Hash` (see `LogicalPosition` for the rationale).
299
#[derive(Default, Copy, Clone)]
300
#[repr(C)]
301
pub struct LogicalSize {
302
    pub width: f32,
303
    pub height: f32,
304
}
305

            
306
impl PartialEq for LogicalSize {
307
937478
    fn eq(&self, other: &Self) -> bool {
308
937478
        quantize(self.width) == quantize(other.width)
309
936625
            && quantize(self.height) == quantize(other.height)
310
937478
    }
311
}
312

            
313
impl LogicalSize {
314
    /// Scales the size in-place by the given DPI scale factor and returns self.
315
    // Mutates in place; the returned copy is only for optional chaining, so callers
316
    // may legitimately discard it (e.g. ui_solver) — #[must_use] would be wrong here.
317
    #[allow(clippy::return_self_not_must_use)]
318
23
    pub fn scale_for_dpi(&mut self, scale_factor: f32) -> Self {
319
23
        self.width *= scale_factor;
320
23
        self.height *= scale_factor;
321
23
        *self
322
23
    }
323

            
324
    /// Creates a `LogicalSize` from main and cross axis dimensions.
325
584481
    #[must_use] pub const fn from_main_cross(main: f32, cross: f32, wm: LayoutWritingMode) -> Self {
326
584481
        match wm {
327
583961
            LayoutWritingMode::HorizontalTb => Self::new(cross, main),
328
520
            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => Self::new(main, cross),
329
        }
330
584481
    }
331
}
332

            
333
impl core::fmt::Debug for LogicalSize {
334
620277
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
335
620277
        write!(f, "{}x{}", self.width, self.height)
336
620277
    }
337
}
338

            
339
impl core::fmt::Display for LogicalSize {
340
1000950
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
341
1000950
        write!(f, "{}x{}", self.width, self.height)
342
1000950
    }
343
}
344

            
345
impl_option!(
346
    LogicalSize,
347
    OptionLogicalSize,
348
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd]
349
);
350

            
351
impl_option!(
352
    LogicalRect,
353
    OptionLogicalRect,
354
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd]
355
);
356

            
357
// PartialOrd delegates to the quantized Ord (the derived field-wise PartialOrd
358
// compared raw f32 and diverged from this quantized order — a latent bug).
359
impl PartialOrd for LogicalSize {
360
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
361
        Some(self.cmp(other))
362
    }
363
}
364
impl Ord for LogicalSize {
365
64
    fn cmp(&self, other: &Self) -> Ordering {
366
64
        let self_width = quantize(self.width);
367
64
        let self_height = quantize(self.height);
368
64
        let other_width = quantize(other.width);
369
64
        let other_height = quantize(other.height);
370
64
        self_width
371
64
            .cmp(&other_width)
372
64
            .then(self_height.cmp(&other_height))
373
64
    }
374
}
375

            
376
impl Eq for LogicalSize {}
377

            
378
impl Hash for LogicalSize {
379
132
    fn hash<H>(&self, state: &mut H)
380
132
    where
381
132
        H: Hasher,
382
    {
383
132
        let self_width = quantize(self.width);
384
132
        let self_height = quantize(self.height);
385
132
        self_width.hash(state);
386
132
        self_height.hash(state);
387
132
    }
388
}
389

            
390
impl LogicalSize {
391
    /// Returns the main-axis dimension for the given writing mode.
392
536902
    #[must_use] pub const fn main(&self, wm: LayoutWritingMode) -> f32 {
393
536902
        match wm {
394
536334
            LayoutWritingMode::HorizontalTb => self.height,
395
568
            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => self.width,
396
        }
397
536902
    }
398

            
399
    /// Returns the cross-axis dimension for the given writing mode.
400
583770
    #[must_use] pub const fn cross(&self, wm: LayoutWritingMode) -> f32 {
401
583770
        match wm {
402
583192
            LayoutWritingMode::HorizontalTb => self.width,
403
578
            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => self.height,
404
        }
405
583770
    }
406

            
407
    /// Returns a new `LogicalSize` with the main-axis dimension updated.
408
33072
    #[must_use] pub const fn with_main(self, wm: LayoutWritingMode, value: f32) -> Self {
409
33072
        match wm {
410
33044
            LayoutWritingMode::HorizontalTb => Self {
411
33044
                height: value,
412
33044
                ..self
413
33044
            },
414
28
            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => Self {
415
28
                width: value,
416
28
                ..self
417
28
            },
418
        }
419
33072
    }
420

            
421
    /// Returns a new `LogicalSize` with the cross-axis dimension updated.
422
27
    #[must_use] pub const fn with_cross(self, wm: LayoutWritingMode, value: f32) -> Self {
423
27
        match wm {
424
9
            LayoutWritingMode::HorizontalTb => Self {
425
9
                width: value,
426
9
                ..self
427
9
            },
428
18
            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => Self {
429
18
                height: value,
430
18
                ..self
431
18
            },
432
        }
433
27
    }
434
}
435

            
436
/// A 2D position in physical (pixel) coordinates.
437
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
438
#[repr(C)]
439
pub struct PhysicalPosition<T> {
440
    pub x: T,
441
    pub y: T,
442
}
443

            
444
impl<T: ::core::fmt::Display> ::core::fmt::Debug for PhysicalPosition<T> {
445
1
    fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
446
1
        write!(f, "({}, {})", self.x, self.y)
447
1
    }
448
}
449

            
450
pub type PhysicalPositionI32 = PhysicalPosition<i32>;
451
impl_option!(
452
    PhysicalPositionI32,
453
    OptionPhysicalPositionI32,
454
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd]
455
);
456

            
457
/// A 2D size in physical (pixel) coordinates.
458
#[derive(Ord, Hash, Eq, Copy, Clone, PartialEq, PartialOrd)]
459
#[repr(C)]
460
pub struct PhysicalSize<T> {
461
    pub width: T,
462
    pub height: T,
463
}
464

            
465
impl<T: ::core::fmt::Display> ::core::fmt::Debug for PhysicalSize<T> {
466
1
    fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
467
1
        write!(f, "{}x{}", self.width, self.height)
468
1
    }
469
}
470

            
471
pub type PhysicalSizeU32 = PhysicalSize<u32>;
472
impl_option!(
473
    PhysicalSizeU32,
474
    OptionPhysicalSizeU32,
475
    [Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash]
476
);
477
pub type PhysicalSizeF32 = PhysicalSize<f32>;
478
impl_option!(
479
    PhysicalSizeF32,
480
    OptionPhysicalSizeF32,
481
    [Debug, Copy, Clone, PartialEq, PartialOrd]
482
);
483

            
484
impl LogicalPosition {
485
    #[inline]
486
4717359
    #[must_use] pub const fn new(x: f32, y: f32) -> Self {
487
4717359
        Self { x, y }
488
4717359
    }
489
    #[inline]
490
477879
    #[must_use] pub const fn zero() -> Self {
491
477879
        Self::new(0.0, 0.0)
492
477879
    }
493
    /// Converts to physical pixel coordinates by multiplying by the DPI factor.
494
    #[inline]
495
    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
496
69
    #[must_use] pub fn to_physical(self, hidpi_factor: f32) -> PhysicalPosition<u32> {
497
69
        PhysicalPosition {
498
69
            x: libm::roundf(self.x * hidpi_factor) as u32,
499
69
            y: libm::roundf(self.y * hidpi_factor) as u32,
500
69
        }
501
69
    }
502
}
503

            
504
impl<T> PhysicalPosition<T> {
505
    #[inline]
506
121
    pub const fn new(x: T, y: T) -> Self {
507
121
        Self { x, y }
508
121
    }
509
}
510

            
511
impl PhysicalPosition<i32> {
512
    #[inline]
513
1
    #[must_use] pub const fn zero() -> Self {
514
1
        Self::new(0, 0)
515
1
    }
516
    /// Converts to logical coordinates by dividing by the DPI factor.
517
    #[inline]
518
    #[allow(clippy::cast_precision_loss)]
519
11
    #[must_use] pub fn to_logical(self, hidpi_factor: f32) -> LogicalPosition {
520
11
        LogicalPosition {
521
11
            x: self.x as f32 / hidpi_factor,
522
11
            y: self.y as f32 / hidpi_factor,
523
11
        }
524
11
    }
525
}
526

            
527
impl PhysicalPosition<f64> {
528
    #[inline]
529
1
    #[must_use] pub const fn zero() -> Self {
530
1
        Self::new(0.0, 0.0)
531
1
    }
532
    /// Converts to logical coordinates by dividing by the DPI factor.
533
    #[inline]
534
    #[allow(clippy::cast_possible_truncation)]
535
10
    #[must_use] pub fn to_logical(self, hidpi_factor: f32) -> LogicalPosition {
536
10
        LogicalPosition {
537
10
            x: self.x as f32 / hidpi_factor,
538
10
            y: self.y as f32 / hidpi_factor,
539
10
        }
540
10
    }
541
}
542

            
543
impl LogicalSize {
544
    #[inline]
545
5286644
    #[must_use] pub const fn new(width: f32, height: f32) -> Self {
546
5286644
        Self { width, height }
547
5286644
    }
548
    #[inline]
549
149176
    #[must_use] pub const fn zero() -> Self {
550
149176
        Self::new(0.0, 0.0)
551
149176
    }
552
    /// Converts to physical pixel size by multiplying by the DPI factor.
553
    #[inline]
554
    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
555
136
    #[must_use] pub fn to_physical(self, hidpi_factor: f32) -> PhysicalSize<u32> {
556
136
        PhysicalSize {
557
136
            width: libm::roundf(self.width * hidpi_factor) as u32,
558
136
            height: libm::roundf(self.height * hidpi_factor) as u32,
559
136
        }
560
136
    }
561
}
562

            
563
impl<T> PhysicalSize<T> {
564
    #[inline]
565
105
    pub const fn new(width: T, height: T) -> Self {
566
105
        Self { width, height }
567
105
    }
568
}
569

            
570
impl PhysicalSize<u32> {
571
    #[inline]
572
2
    #[must_use] pub const fn zero() -> Self {
573
2
        Self::new(0, 0)
574
2
    }
575
    /// Converts to logical coordinates by dividing by the DPI factor.
576
    #[inline]
577
    #[allow(clippy::cast_precision_loss)]
578
40
    #[must_use] pub fn to_logical(self, hidpi_factor: f32) -> LogicalSize {
579
40
        LogicalSize {
580
40
            width: self.width as f32 / hidpi_factor,
581
40
            height: self.height as f32 / hidpi_factor,
582
40
        }
583
40
    }
584
}
585

            
586
/// Marker enum documenting which coordinate space a geometric value is in.
587
///
588
/// This is for documentation and debugging purposes only — it does not enforce
589
/// type safety at compile time. Use comments like `[CoordinateSpace::Window]`
590
/// or `[CoordinateSpace::ScrollFrame]` in code to document coordinate contexts.
591
///
592
/// **Common bug pattern:** passing `Window`-space coordinates where
593
/// `ScrollFrame`-space is expected (or vice versa). The scroll frame creates a
594
/// new spatial node, so primitives must be offset by the frame origin.
595
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
596
#[repr(C)]
597
pub enum CoordinateSpace {
598
    /// Absolute coordinates from window top-left (0,0).
599
    /// Layout engine output is in this space.
600
    Window,
601
    
602
    /// Relative to scroll frame content origin.
603
    /// Transformation: `scroll_pos` = `window_pos` - `scroll_frame_origin`
604
    ScrollFrame,
605
    
606
    /// Relative to parent node's content box origin.
607
    Parent,
608
    
609
    /// Relative to a CSS transform reference frame origin.
610
    ReferenceFrame,
611
}
612

            
613

            
614
// =============================================================================
615
// Type-safe coordinate newtypes for API clarity
616
// =============================================================================
617

            
618
/// Position in screen coordinates (logical pixels, relative to primary monitor origin).
619
/// On Wayland: falls back to window-local since global coords are unavailable.
620
#[derive(Default, Debug, Copy, Clone, PartialEq, PartialOrd)]
621
#[repr(C)]
622
pub struct ScreenPosition {
623
    pub x: f32,
624
    pub y: f32,
625
}
626

            
627
impl ScreenPosition {
628
    #[inline]
629
65
    #[must_use] pub const fn new(x: f32, y: f32) -> Self {
630
65
        Self { x, y }
631
65
    }
632
    #[inline]
633
1
    #[must_use] pub const fn zero() -> Self {
634
1
        Self::new(0.0, 0.0)
635
1
    }
636
    /// Convert to a raw `LogicalPosition` (for interop with existing code).
637
    #[inline]
638
64
    #[must_use] pub const fn to_logical(self) -> LogicalPosition {
639
64
        LogicalPosition { x: self.x, y: self.y }
640
64
    }
641
    /// Create from a raw `LogicalPosition` that is known to be in screen space.
642
    #[inline]
643
64
    #[must_use] pub const fn from_logical(p: LogicalPosition) -> Self {
644
64
        Self { x: p.x, y: p.y }
645
64
    }
646
}
647

            
648
impl_option!(
649
    ScreenPosition,
650
    OptionScreenPosition,
651
    [Debug, Copy, Clone, PartialEq, PartialOrd]
652
);
653

            
654
/// Position relative to a DOM node's border box origin (logical pixels).
655
#[derive(Default, Debug, Copy, Clone, PartialEq, PartialOrd)]
656
#[repr(C)]
657
pub struct CursorNodePosition {
658
    pub x: f32,
659
    pub y: f32,
660
}
661

            
662
impl CursorNodePosition {
663
    #[inline]
664
86
    #[must_use] pub const fn new(x: f32, y: f32) -> Self {
665
86
        Self { x, y }
666
86
    }
667
    #[inline]
668
1
    #[must_use] pub const fn zero() -> Self {
669
1
        Self::new(0.0, 0.0)
670
1
    }
671
    #[inline]
672
64
    #[must_use] pub const fn to_logical(self) -> LogicalPosition {
673
64
        LogicalPosition { x: self.x, y: self.y }
674
64
    }
675
    #[inline]
676
2124
    #[must_use] pub const fn from_logical(p: LogicalPosition) -> Self {
677
2124
        Self { x: p.x, y: p.y }
678
2124
    }
679
}
680

            
681
impl_option!(
682
    CursorNodePosition,
683
    OptionCursorNodePosition,
684
    [Debug, Copy, Clone, PartialEq, PartialOrd]
685
);
686

            
687
#[cfg(test)]
688
mod tests {
689
    use super::*;
690
    use core::cmp::Ordering;
691

            
692
    #[test]
693
1
    fn hit_test_edges_match_contains() {
694
1
        let r = LogicalRect::new(LogicalPosition::new(10.0, 20.0), LogicalSize::new(30.0, 40.0));
695
        // left/top edge: inclusive in both
696
1
        let tl = LogicalPosition::new(10.0, 20.0);
697
1
        assert!(r.contains(tl));
698
1
        assert!(r.hit_test(&tl).is_some());
699
        // just inside
700
1
        let inside = LogicalPosition::new(11.0, 21.0);
701
1
        assert!(r.contains(inside));
702
1
        assert!(r.hit_test(&inside).is_some());
703
        // right/bottom edge: exclusive in both
704
1
        let br = LogicalPosition::new(40.0, 60.0);
705
1
        assert!(!r.contains(br));
706
1
        assert!(r.hit_test(&br).is_none());
707
        // outside left
708
1
        let out = LogicalPosition::new(9.0, 20.0);
709
1
        assert!(!r.contains(out));
710
1
        assert!(r.hit_test(&out).is_none());
711
1
    }
712

            
713
    #[test]
714
1
    fn hit_test_offset_is_from_top_left() {
715
1
        let r = LogicalRect::new(LogicalPosition::new(10.0, 20.0), LogicalSize::new(30.0, 40.0));
716
1
        let hit = r.hit_test(&LogicalPosition::new(15.0, 25.0)).unwrap();
717
1
        assert_eq!(hit, LogicalPosition::new(5.0, 5.0));
718
1
    }
719

            
720
    #[test]
721
1
    fn quantize_nan_is_distinct_from_zero() {
722
1
        assert_eq!(quantize(f32::NAN), i64::MIN);
723
1
        assert_ne!(quantize(f32::NAN), quantize(0.0));
724
1
    }
725

            
726
    #[test]
727
1
    fn partial_eq_agrees_with_ord_and_hash() {
728
        use core::hash::{Hash, Hasher};
729
        // Two values within the same quantization bucket must be == AND cmp==Equal.
730
1
        let a = LogicalPosition::new(1.00000, 2.00000);
731
1
        let b = LogicalPosition::new(1.00004, 2.00004); // < 0.001 apart
732
1
        assert_eq!(a, b);
733
1
        assert_eq!(a.cmp(&b), Ordering::Equal);
734

            
735
2
        let hash_of = |p: &LogicalPosition| {
736
2
            let mut h = std::collections::hash_map::DefaultHasher::new();
737
2
            p.hash(&mut h);
738
2
            h.finish()
739
2
        };
740
1
        assert_eq!(hash_of(&a), hash_of(&b));
741

            
742
        // NaN equals NaN under the quantized PartialEq (i64::MIN bucket) — this
743
        // is what stops the every-frame Resize loop upstream.
744
1
        let n1 = LogicalSize::new(f32::NAN, 1.0);
745
1
        let n2 = LogicalSize::new(f32::NAN, 1.0);
746
1
        assert_eq!(n1, n2);
747
1
    }
748

            
749
    #[test]
750
1
    fn quantize_saturates_instead_of_wrapping() {
751
        // Huge coordinate must saturate, not wrap to a small/negative bucket.
752
1
        assert_eq!(quantize(f32::INFINITY), i64::MAX);
753
1
        assert_eq!(quantize(f32::NEG_INFINITY), i64::MIN);
754
1
    }
755
}
756

            
757
#[cfg(test)]
758
#[allow(clippy::float_cmp)]
759
mod autotest_generated {
760
    use core::{
761
        cmp::Ordering,
762
        hash::{Hash, Hasher},
763
    };
764

            
765
    use azul_css::props::layout::LayoutWritingMode;
766

            
767
    use super::*;
768

            
769
    /// Hostile float grid: every class that can reach a coordinate field.
770
    const HOSTILE: [f32; 8] = [
771
        f32::NAN,
772
        f32::NEG_INFINITY,
773
        f32::MIN,
774
        -1.0,
775
        0.0,
776
        1.0,
777
        f32::MAX,
778
        f32::INFINITY,
779
    ];
780

            
781
    const WMS: [LayoutWritingMode; 3] = [
782
        LayoutWritingMode::HorizontalTb,
783
        LayoutWritingMode::VerticalRl,
784
        LayoutWritingMode::VerticalLr,
785
    ];
786

            
787
    fn hash_of<T: Hash>(v: &T) -> u64 {
788
        let mut h = std::collections::hash_map::DefaultHasher::new();
789
        v.hash(&mut h);
790
        h.finish()
791
    }
792

            
793
    // ---------------------------------------------------------------------
794
    // quantize: numeric / saturation / NaN
795
    // ---------------------------------------------------------------------
796

            
797
    #[test]
798
    fn quantize_zero_and_negative_zero_share_a_bucket() {
799
        assert_eq!(quantize(0.0), 0);
800
        assert_eq!(quantize(-0.0), 0);
801
        // Sign of zero must not split the bucket, or (0,0) and (-0,-0) would be
802
        // distinct HashMap keys for the same visual origin.
803
        assert_eq!(quantize(0.0), quantize(-0.0));
804
    }
805

            
806
    #[test]
807
    fn quantize_applies_the_decimal_multiplier() {
808
        assert_eq!(quantize(1.0), DECIMAL_MULTIPLIER as i64);
809
        assert_eq!(quantize(-1.0), -(DECIMAL_MULTIPLIER as i64));
810
        assert_eq!(quantize(1.5), 1500);
811
        assert_eq!(quantize(-1.5), -1500);
812
    }
813

            
814
    #[test]
815
    fn quantize_truncates_toward_zero_below_precision() {
816
        // Sub-millipixel deltas collapse into the same bucket (truncation IS the
817
        // documented rounding step) — and truncation is toward zero, not floor.
818
        assert_eq!(quantize(0.0004), 0);
819
        assert_eq!(quantize(-0.0004), 0);
820
        assert_eq!(quantize(1.0004), 1000);
821
        assert_eq!(quantize(-1.0004), -1000);
822
    }
823

            
824
    #[test]
825
    fn quantize_extremes_saturate_and_never_wrap() {
826
        // f32::MAX * 1000 overflows to +inf before the cast; the cast must clamp.
827
        assert_eq!(quantize(f32::MAX), i64::MAX);
828
        assert_eq!(quantize(f32::MIN), i64::MIN);
829
        assert_eq!(quantize(f32::INFINITY), i64::MAX);
830
        assert_eq!(quantize(f32::NEG_INFINITY), i64::MIN);
831
        // Denormal-ish tiny values must land on 0, not on a garbage bucket.
832
        assert_eq!(quantize(f32::MIN_POSITIVE), 0);
833
        assert_eq!(quantize(-f32::MIN_POSITIVE), 0);
834
    }
835

            
836
    #[test]
837
    fn quantize_nan_never_aliases_the_origin() {
838
        // The historical bug: `NaN as isize == 0` put NaN on top of (0.0, 0.0).
839
        assert_eq!(quantize(f32::NAN), i64::MIN);
840
        assert_eq!(quantize(-f32::NAN), i64::MIN);
841
        assert_ne!(quantize(f32::NAN), quantize(0.0));
842
    }
843

            
844
    #[test]
845
    fn quantize_saturation_aliases_nan_with_the_bottom_of_the_range() {
846
        // KNOWN, INTENTIONAL LOSSINESS: NaN, -inf and f32::MIN all collapse onto
847
        // the i64::MIN bucket, so they compare Equal. This keeps Ord/Eq total and
848
        // consistent (which is what the type contract needs), but callers cannot
849
        // use == to distinguish "no value" (NaN) from a huge negative coordinate.
850
        assert_eq!(quantize(f32::NAN), quantize(f32::NEG_INFINITY));
851
        assert_eq!(quantize(f32::NAN), quantize(f32::MIN));
852
        assert_eq!(
853
            LogicalPosition::new(f32::NAN, 0.0),
854
            LogicalPosition::new(f32::NEG_INFINITY, 0.0)
855
        );
856
    }
857

            
858
    #[test]
859
    fn quantize_is_monotonic_over_finite_inputs() {
860
        let ascending = [-1.0e6_f32, -1.0, -0.001, 0.0, 0.001, 1.0, 1.0e6];
861
        for w in ascending.windows(2) {
862
            assert!(
863
                quantize(w[0]) <= quantize(w[1]),
864
                "quantize inverted the order of {} and {}",
865
                w[0],
866
                w[1]
867
            );
868
        }
869
    }
870

            
871
    #[test]
872
    fn quantize_is_deterministic_across_calls() {
873
        for v in HOSTILE {
874
            assert_eq!(quantize(v), quantize(v));
875
        }
876
    }
877

            
878
    // ---------------------------------------------------------------------
879
    // Ord / Eq / Hash total-order contract over the hostile grid
880
    // ---------------------------------------------------------------------
881

            
882
    fn hostile_positions() -> [LogicalPosition; 64] {
883
        let mut out = [LogicalPosition::zero(); 64];
884
        let mut i = 0;
885
        for x in HOSTILE {
886
            for y in HOSTILE {
887
                out[i] = LogicalPosition::new(x, y);
888
                i += 1;
889
            }
890
        }
891
        out
892
    }
893

            
894
    #[test]
895
    fn ord_is_reflexive_and_antisymmetric_even_with_nan() {
896
        let grid = hostile_positions();
897
        for a in grid {
898
            // Eq requires reflexivity — raw f32 NaN would break it.
899
            assert_eq!(a.cmp(&a), Ordering::Equal);
900
            assert_eq!(a, a);
901
            for b in grid {
902
                assert_eq!(a.cmp(&b), b.cmp(&a).reverse());
903
            }
904
        }
905
    }
906

            
907
    #[test]
908
    fn ord_is_transitive_over_the_hostile_grid() {
909
        let grid = hostile_positions();
910
        for a in grid {
911
            for b in grid {
912
                if a.cmp(&b) != Ordering::Less {
913
                    continue;
914
                }
915
                for c in grid {
916
                    if b.cmp(&c) == Ordering::Less {
917
                        assert_eq!(a.cmp(&c), Ordering::Less);
918
                    }
919
                }
920
            }
921
        }
922
    }
923

            
924
    #[test]
925
    fn partial_eq_ord_and_hash_agree_over_the_hostile_grid() {
926
        let grid = hostile_positions();
927
        for a in grid {
928
            for b in grid {
929
                let eq = a == b;
930
                assert_eq!(eq, a.cmp(&b) == Ordering::Equal);
931
                assert_eq!(Some(a.cmp(&b)), a.partial_cmp(&b));
932
                if eq {
933
                    // Hash/Eq contract: equal keys MUST hash equal, or HashMap
934
                    // lookups silently miss.
935
                    assert_eq!(hash_of(&a), hash_of(&b));
936
                }
937
            }
938
        }
939
    }
940

            
941
    #[test]
942
    fn logical_size_eq_and_hash_agree_including_nan() {
943
        for w in HOSTILE {
944
            for h in HOSTILE {
945
                let a = LogicalSize::new(w, h);
946
                let b = LogicalSize::new(w, h);
947
                assert_eq!(a, b);
948
                assert_eq!(a.cmp(&b), Ordering::Equal);
949
                assert_eq!(hash_of(&a), hash_of(&b));
950
            }
951
        }
952
    }
953

            
954
    #[test]
955
    fn logical_rect_eq_and_hash_are_quantized_through_its_fields() {
956
        // LogicalRect's derived PartialEq/Hash must inherit the quantized field
957
        // impls — a NaN-sized rect has to be a stable HashMap key.
958
        let a = LogicalRect::new(
959
            LogicalPosition::new(f32::NAN, 1.0),
960
            LogicalSize::new(f32::NAN, 2.0),
961
        );
962
        let b = a;
963
        assert_eq!(a, b);
964
        assert_eq!(hash_of(&a), hash_of(&b));
965

            
966
        // Sub-millipixel jitter must not create a new key.
967
        let c = LogicalRect::new(
968
            LogicalPosition::new(1.0, 2.0),
969
            LogicalSize::new(3.0, 4.0),
970
        );
971
        let d = LogicalRect::new(
972
            LogicalPosition::new(1.00004, 2.00004),
973
            LogicalSize::new(3.00004, 4.00004),
974
        );
975
        assert_eq!(c, d);
976
        assert_eq!(hash_of(&c), hash_of(&d));
977
    }
978

            
979
    // ---------------------------------------------------------------------
980
    // Constructors / zero neutrality
981
    // ---------------------------------------------------------------------
982

            
983
    #[test]
984
    fn constructors_preserve_fields_for_extreme_arguments() {
985
        for x in HOSTILE {
986
            for y in HOSTILE {
987
                let p = LogicalPosition::new(x, y);
988
                assert_eq!(p.x.to_bits(), x.to_bits());
989
                assert_eq!(p.y.to_bits(), y.to_bits());
990

            
991
                let s = LogicalSize::new(x, y);
992
                assert_eq!(s.width.to_bits(), x.to_bits());
993
                assert_eq!(s.height.to_bits(), y.to_bits());
994

            
995
                let r = LogicalRect::new(p, s);
996
                assert_eq!(r.origin.x.to_bits(), x.to_bits());
997
                assert_eq!(r.size.height.to_bits(), y.to_bits());
998

            
999
                assert_eq!(ScreenPosition::new(x, y).x.to_bits(), x.to_bits());
                assert_eq!(CursorNodePosition::new(x, y).y.to_bits(), y.to_bits());
                assert_eq!(PhysicalPosition::new(x, y).x.to_bits(), x.to_bits());
                assert_eq!(PhysicalSize::new(x, y).height.to_bits(), y.to_bits());
            }
        }
    }
    #[test]
    fn zero_constructors_are_neutral_and_match_default() {
        assert_eq!(LogicalPosition::zero(), LogicalPosition::default());
        assert_eq!(LogicalSize::zero(), LogicalSize::default());
        assert_eq!(LogicalRect::zero(), LogicalRect::default());
        assert_eq!(LogicalRect::zero().origin, LogicalPosition::zero());
        assert_eq!(LogicalRect::zero().size, LogicalSize::zero());
        assert_eq!(ScreenPosition::zero(), ScreenPosition::default());
        assert_eq!(CursorNodePosition::zero(), CursorNodePosition::default());
        assert_eq!(PhysicalPosition::<i32>::zero(), PhysicalPosition::new(0, 0));
        assert_eq!(
            PhysicalPosition::<f64>::zero(),
            PhysicalPosition::new(0.0_f64, 0.0_f64)
        );
        assert_eq!(PhysicalSize::<u32>::zero(), PhysicalSize::new(0, 0));
        // A zero rect is degenerate: it contains no point at all, not even its
        // own origin, and it does not intersect itself.
        let z = LogicalRect::zero();
        assert!(!z.contains(LogicalPosition::zero()));
        assert!(!z.intersects(z));
        assert_eq!(z.min_x(), 0.0);
        assert_eq!(z.max_x(), 0.0);
        assert_eq!(z.min_y(), 0.0);
        assert_eq!(z.max_y(), 0.0);
    }
    // ---------------------------------------------------------------------
    // LogicalRect getters
    // ---------------------------------------------------------------------
    #[test]
    fn rect_getters_return_the_constructed_edges() {
        let r = LogicalRect::new(
            LogicalPosition::new(10.0, 20.0),
            LogicalSize::new(30.0, 40.0),
        );
        assert_eq!(r.min_x(), 10.0);
        assert_eq!(r.max_x(), 40.0);
        assert_eq!(r.min_y(), 20.0);
        assert_eq!(r.max_y(), 60.0);
    }
    #[test]
    fn rect_getters_do_not_panic_on_extreme_geometry() {
        for x in HOSTILE {
            for w in HOSTILE {
                let r = LogicalRect::new(
                    LogicalPosition::new(x, x),
                    LogicalSize::new(w, w),
                );
                // Pure reads: must never panic, whatever the float class.
                let _ = r.min_x();
                let _ = r.max_x();
                let _ = r.min_y();
                let _ = r.max_y();
            }
        }
        // inf + (-inf) is NaN — max_x has no guard, so it propagates NaN rather
        // than panicking. Assert the defined (non-panicking) result.
        let r = LogicalRect::new(
            LogicalPosition::new(f32::INFINITY, f32::INFINITY),
            LogicalSize::new(f32::NEG_INFINITY, f32::NEG_INFINITY),
        );
        assert!(r.max_x().is_nan());
        assert!(r.max_y().is_nan());
    }
    // ---------------------------------------------------------------------
    // contains / hit_test / intersects
    // ---------------------------------------------------------------------
    #[test]
    fn contains_is_half_open_left_top_inclusive_right_bottom_exclusive() {
        let r = LogicalRect::new(
            LogicalPosition::new(10.0, 20.0),
            LogicalSize::new(30.0, 40.0),
        );
        assert!(r.contains(LogicalPosition::new(10.0, 20.0))); // top-left: in
        assert!(!r.contains(LogicalPosition::new(40.0, 59.0))); // right edge: out
        assert!(!r.contains(LogicalPosition::new(39.0, 60.0))); // bottom edge: out
        assert!(!r.contains(LogicalPosition::new(40.0, 60.0))); // bottom-right: out
        assert!(r.contains(LogicalPosition::new(39.999, 59.999)));
    }
    #[test]
    fn contains_and_hit_test_agree_on_the_hostile_grid() {
        // The invariant the hit_test comment promises: identical edge semantics.
        let rects = [
            LogicalRect::zero(),
            LogicalRect::new(LogicalPosition::new(10.0, 20.0), LogicalSize::new(30.0, 40.0)),
            LogicalRect::new(LogicalPosition::new(-5.0, -5.0), LogicalSize::new(10.0, 10.0)),
            // Negative extent: max < min, so it can never contain anything.
            LogicalRect::new(LogicalPosition::new(0.0, 0.0), LogicalSize::new(-10.0, -10.0)),
            LogicalRect::new(
                LogicalPosition::new(f32::NAN, f32::NAN),
                LogicalSize::new(f32::NAN, f32::NAN),
            ),
            LogicalRect::new(
                LogicalPosition::zero(),
                LogicalSize::new(f32::INFINITY, f32::INFINITY),
            ),
        ];
        for r in rects {
            for x in HOSTILE {
                for y in HOSTILE {
                    let p = LogicalPosition::new(x, y);
                    assert_eq!(
                        r.contains(p),
                        r.hit_test(&p).is_some(),
                        "contains/hit_test disagree for {r:?} at {p:?}"
                    );
                }
            }
        }
    }
    #[test]
    fn contains_rejects_nan_points_and_nan_rects() {
        let r = LogicalRect::new(
            LogicalPosition::new(0.0, 0.0),
            LogicalSize::new(100.0, 100.0),
        );
        // Every comparison against NaN is false, so a NaN point is never inside.
        assert!(!r.contains(LogicalPosition::new(f32::NAN, 50.0)));
        assert!(!r.contains(LogicalPosition::new(50.0, f32::NAN)));
        assert!(!r.contains(LogicalPosition::new(f32::NAN, f32::NAN)));
        let nan_rect = LogicalRect::new(
            LogicalPosition::new(f32::NAN, f32::NAN),
            LogicalSize::new(f32::NAN, f32::NAN),
        );
        assert!(!nan_rect.contains(LogicalPosition::zero()));
        assert!(nan_rect.hit_test(&LogicalPosition::zero()).is_none());
    }
    #[test]
    fn contains_handles_negative_extent_rects_without_panicking() {
        // A negative width puts max_x below min_x: nothing can satisfy both bounds.
        let r = LogicalRect::new(
            LogicalPosition::new(0.0, 0.0),
            LogicalSize::new(-10.0, -10.0),
        );
        assert!(!r.contains(LogicalPosition::zero()));
        assert!(!r.contains(LogicalPosition::new(-5.0, -5.0)));
        assert!(r.hit_test(&LogicalPosition::new(-5.0, -5.0)).is_none());
    }
    #[test]
    fn contains_at_the_coordinate_extremes() {
        let huge = LogicalRect::new(
            LogicalPosition::new(f32::MIN, f32::MIN),
            LogicalSize::new(f32::MAX, f32::MAX),
        );
        // f32::MIN + f32::MAX == 0.0 exactly, so the rect spans [MIN, 0).
        assert_eq!(huge.max_x(), 0.0);
        assert!(huge.contains(LogicalPosition::new(-1.0, -1.0)));
        assert!(!huge.contains(LogicalPosition::zero()));
        let unbounded = LogicalRect::new(
            LogicalPosition::new(f32::NEG_INFINITY, f32::NEG_INFINITY),
            LogicalSize::new(f32::INFINITY, f32::INFINITY),
        );
        // -inf + inf == NaN, so the "infinite" rect contains nothing. Surprising,
        // but defined and panic-free.
        assert!(unbounded.max_x().is_nan());
        assert!(!unbounded.contains(LogicalPosition::zero()));
    }
    #[test]
    fn hit_test_returns_the_offset_from_the_top_left_corner() {
        let r = LogicalRect::new(
            LogicalPosition::new(10.0, 20.0),
            LogicalSize::new(30.0, 40.0),
        );
        assert_eq!(
            r.hit_test(&LogicalPosition::new(10.0, 20.0)),
            Some(LogicalPosition::new(0.0, 0.0))
        );
        assert_eq!(
            r.hit_test(&LogicalPosition::new(25.0, 45.0)),
            Some(LogicalPosition::new(15.0, 25.0))
        );
        // Right/bottom edges are exclusive.
        assert_eq!(r.hit_test(&LogicalPosition::new(40.0, 30.0)), None);
        assert_eq!(r.hit_test(&LogicalPosition::new(30.0, 60.0)), None);
    }
    #[test]
    fn hit_test_offset_is_always_non_negative_when_it_hits() {
        let r = LogicalRect::new(
            LogicalPosition::new(-100.0, -100.0),
            LogicalSize::new(200.0, 200.0),
        );
        // Dyadic values only: `origin + offset` must reconstruct the point exactly,
        // so the assertion tests hit_test's arithmetic and not f32 rounding.
        for x in [-100.0_f32, -50.0, 0.0, 50.0, 99.5] {
            for y in [-100.0_f32, -50.0, 0.0, 50.0, 99.5] {
                let hit = r.hit_test(&LogicalPosition::new(x, y)).expect("inside");
                assert!(hit.x >= 0.0 && hit.y >= 0.0, "negative offset {hit:?}");
                assert_eq!(r.origin.x + hit.x, x);
                assert_eq!(r.origin.y + hit.y, y);
            }
        }
    }
    #[test]
    fn intersects_is_symmetric_even_for_degenerate_and_nan_rects() {
        let rects = [
            LogicalRect::zero(),
            LogicalRect::new(LogicalPosition::new(0.0, 0.0), LogicalSize::new(10.0, 10.0)),
            LogicalRect::new(LogicalPosition::new(5.0, 5.0), LogicalSize::new(10.0, 10.0)),
            LogicalRect::new(LogicalPosition::new(10.0, 0.0), LogicalSize::new(10.0, 10.0)),
            LogicalRect::new(LogicalPosition::new(0.0, 0.0), LogicalSize::new(-10.0, -10.0)),
            LogicalRect::new(
                LogicalPosition::new(f32::NAN, f32::NAN),
                LogicalSize::new(f32::NAN, f32::NAN),
            ),
            LogicalRect::new(
                LogicalPosition::new(f32::MIN, f32::MIN),
                LogicalSize::new(f32::MAX, f32::MAX),
            ),
        ];
        for a in rects {
            for b in rects {
                assert_eq!(
                    a.intersects(b),
                    b.intersects(a),
                    "intersects is asymmetric for {a:?} / {b:?}"
                );
            }
        }
    }
    #[test]
    fn intersects_touching_edges_do_not_count_as_overlap() {
        let a = LogicalRect::new(LogicalPosition::new(0.0, 0.0), LogicalSize::new(10.0, 10.0));
        let touching = LogicalRect::new(
            LogicalPosition::new(10.0, 0.0),
            LogicalSize::new(10.0, 10.0),
        );
        let overlapping = LogicalRect::new(
            LogicalPosition::new(9.99, 0.0),
            LogicalSize::new(10.0, 10.0),
        );
        assert!(!a.intersects(touching));
        assert!(a.intersects(overlapping));
        assert!(a.intersects(a));
        // Zero-area rects never overlap anything, including themselves.
        assert!(!LogicalRect::zero().intersects(a));
    }
    #[test]
    fn intersects_with_nan_rect_is_permissive_current_behavior() {
        // DOCUMENTS A REAL QUIRK (reported, not worked around): every `<=` guard
        // in `intersects` is false against NaN, so all four early-outs are skipped
        // and a fully-NaN rect reports that it intersects EVERYTHING — while
        // `contains` on the same rect correctly reports false for every point.
        // Locked down here so a future fix has to change this deliberately.
        let nan_rect = LogicalRect::new(
            LogicalPosition::new(f32::NAN, f32::NAN),
            LogicalSize::new(f32::NAN, f32::NAN),
        );
        let normal = LogicalRect::new(
            LogicalPosition::new(0.0, 0.0),
            LogicalSize::new(10.0, 10.0),
        );
        assert!(nan_rect.intersects(normal));
        assert!(normal.intersects(nan_rect));
        assert!(!nan_rect.contains(LogicalPosition::zero()));
    }
    // ---------------------------------------------------------------------
    // scale_for_dpi
    // ---------------------------------------------------------------------
    #[test]
    fn scale_for_dpi_by_one_is_the_identity() {
        let mut p = LogicalPosition::new(1.5, -2.5);
        p.scale_for_dpi(1.0);
        assert_eq!(p, LogicalPosition::new(1.5, -2.5));
        let mut s = LogicalSize::new(3.5, 4.5);
        assert_eq!(s.scale_for_dpi(1.0), LogicalSize::new(3.5, 4.5));
        let mut r = LogicalRect::new(
            LogicalPosition::new(1.0, 2.0),
            LogicalSize::new(3.0, 4.0),
        );
        r.scale_for_dpi(1.0);
        assert_eq!(
            r,
            LogicalRect::new(LogicalPosition::new(1.0, 2.0), LogicalSize::new(3.0, 4.0))
        );
    }
    #[test]
    fn scale_for_dpi_by_zero_collapses_to_the_origin() {
        let mut r = LogicalRect::new(
            LogicalPosition::new(10.0, 20.0),
            LogicalSize::new(30.0, 40.0),
        );
        r.scale_for_dpi(0.0);
        assert_eq!(r, LogicalRect::zero());
    }
    #[test]
    fn scale_for_dpi_by_negative_factor_mirrors_deterministically() {
        let mut r = LogicalRect::new(
            LogicalPosition::new(10.0, 20.0),
            LogicalSize::new(30.0, 40.0),
        );
        r.scale_for_dpi(-2.0);
        assert_eq!(
            r,
            LogicalRect::new(
                LogicalPosition::new(-20.0, -40.0),
                LogicalSize::new(-60.0, -80.0)
            )
        );
        // A mirrored rect has an inverted extent, so it contains nothing.
        assert!(!r.contains(LogicalPosition::new(-30.0, -50.0)));
    }
    #[test]
    fn scale_for_dpi_overflows_to_infinity_rather_than_panicking() {
        let mut s = LogicalSize::new(f32::MAX, f32::MAX);
        let out = s.scale_for_dpi(2.0);
        assert!(out.width.is_infinite() && out.width.is_sign_positive());
        assert!(out.height.is_infinite());
        // scale_for_dpi mutates in place AND returns a copy: they must match.
        assert_eq!(out, s);
    }
    #[test]
    fn scale_for_dpi_with_nan_or_inf_does_not_panic() {
        for factor in HOSTILE {
            let mut p = LogicalPosition::new(1.0, -1.0);
            p.scale_for_dpi(factor);
            let mut s = LogicalSize::new(1.0, -1.0);
            let _ = s.scale_for_dpi(factor);
            let mut r = LogicalRect::new(
                LogicalPosition::new(1.0, -1.0),
                LogicalSize::new(2.0, -2.0),
            );
            r.scale_for_dpi(factor);
        }
        // 0.0 * inf is the classic NaN trap: a zero-origin rect scaled by an
        // infinite DPI factor yields NaN coordinates, not zeros.
        let mut r = LogicalRect::new(LogicalPosition::zero(), LogicalSize::new(1.0, 1.0));
        r.scale_for_dpi(f32::INFINITY);
        assert!(r.origin.x.is_nan());
        assert!(r.size.width.is_infinite());
    }
    // ---------------------------------------------------------------------
    // DPI conversion: to_physical / to_logical
    // ---------------------------------------------------------------------
    #[test]
    fn to_physical_rounds_half_away_from_zero() {
        assert_eq!(
            LogicalPosition::new(0.5, 1.5).to_physical(1.0),
            PhysicalPosition::new(1, 2)
        );
        // 2.5 -> 3 (round-half-away), NOT 2 (banker's rounding).
        assert_eq!(
            LogicalSize::new(2.5, 3.5).to_physical(1.0),
            PhysicalSize::new(3, 4)
        );
    }
    #[test]
    fn to_physical_clamps_negatives_to_zero_instead_of_wrapping() {
        // `as u32` saturates (Rust >= 1.45), so -1.0 must become 0, NOT u32::MAX.
        assert_eq!(
            LogicalPosition::new(-1.0, -1000.0).to_physical(1.0),
            PhysicalPosition::new(0, 0)
        );
        assert_eq!(
            LogicalSize::new(-0.6, -1.0).to_physical(2.0),
            PhysicalSize::new(0, 0)
        );
        assert_eq!(
            LogicalPosition::new(1.0, 1.0).to_physical(-1.0),
            PhysicalPosition::new(0, 0)
        );
    }
    #[test]
    fn to_physical_saturates_at_u32_max_on_overflow() {
        assert_eq!(
            LogicalSize::new(f32::MAX, f32::INFINITY).to_physical(1.0),
            PhysicalSize::new(u32::MAX, u32::MAX)
        );
        // x: 1e30 * 1e30 overflows f32 to +inf, then saturates at the cast.
        // y: 0.0 * 1e30 stays 0 — saturation must not smear across components.
        assert_eq!(
            LogicalPosition::new(1.0e30, 0.0).to_physical(1.0e30),
            PhysicalPosition::new(u32::MAX, 0)
        );
    }
    #[test]
    fn to_physical_maps_nan_to_zero() {
        // `NaN as u32` == 0 by the saturating-cast rules. Defined, not UB.
        assert_eq!(
            LogicalPosition::new(f32::NAN, f32::NAN).to_physical(1.0),
            PhysicalPosition::new(0, 0)
        );
        assert_eq!(
            LogicalSize::new(f32::NAN, 5.0).to_physical(f32::NAN),
            PhysicalSize::new(0, 0)
        );
        // 0.0 * inf == NaN -> 0
        assert_eq!(
            LogicalSize::new(0.0, 0.0).to_physical(f32::INFINITY),
            PhysicalSize::new(0, 0)
        );
    }
    #[test]
    fn to_physical_never_panics_on_the_hostile_grid() {
        for v in HOSTILE {
            for f in HOSTILE {
                let _ = LogicalPosition::new(v, v).to_physical(f);
                let _ = LogicalSize::new(v, v).to_physical(f);
            }
        }
    }
    #[test]
    fn to_logical_divides_by_the_dpi_factor() {
        assert_eq!(
            PhysicalSize::new(200_u32, 100).to_logical(2.0),
            LogicalSize::new(100.0, 50.0)
        );
        assert_eq!(
            PhysicalPosition::new(-10_i32, 20).to_logical(2.0),
            LogicalPosition::new(-5.0, 10.0)
        );
        assert_eq!(
            PhysicalPosition::new(-10.0_f64, 20.0).to_logical(2.0),
            LogicalPosition::new(-5.0, 10.0)
        );
    }
    #[test]
    fn to_logical_with_zero_dpi_yields_infinity_not_a_panic() {
        // Float division by zero is defined: no divide-by-zero panic here.
        let s = PhysicalSize::new(100_u32, 100).to_logical(0.0);
        assert!(s.width.is_infinite() && s.width.is_sign_positive());
        // 0 / 0 == NaN.
        let z = PhysicalSize::<u32>::zero().to_logical(0.0);
        assert!(z.width.is_nan() && z.height.is_nan());
        let p = PhysicalPosition::new(-5_i32, 5).to_logical(0.0);
        assert!(p.x.is_infinite() && p.x.is_sign_negative());
        assert!(p.y.is_infinite() && p.y.is_sign_positive());
    }
    #[test]
    fn to_logical_at_the_integer_limits() {
        let p = PhysicalPosition::new(i32::MIN, i32::MAX).to_logical(1.0);
        assert_eq!(p.x, i32::MIN as f32);
        assert_eq!(p.y, i32::MAX as f32);
        let s = PhysicalSize::new(u32::MAX, 0_u32).to_logical(1.0);
        assert_eq!(s.width, u32::MAX as f32);
        assert_eq!(s.height, 0.0);
        // f64 -> f32 narrowing saturates to inf rather than wrapping.
        let big = PhysicalPosition::new(f64::MAX, f64::MIN).to_logical(1.0);
        assert!(big.x.is_infinite() && big.x.is_sign_positive());
        assert!(big.y.is_infinite() && big.y.is_sign_negative());
    }
    #[test]
    fn to_logical_never_panics_for_hostile_dpi_factors() {
        for f in HOSTILE {
            let _ = PhysicalPosition::new(i32::MIN, i32::MAX).to_logical(f);
            let _ = PhysicalPosition::new(f64::MAX, f64::MIN).to_logical(f);
            let _ = PhysicalSize::new(u32::MAX, 0_u32).to_logical(f);
        }
    }
    // ---------------------------------------------------------------------
    // Round-trips
    // ---------------------------------------------------------------------
    #[test]
    fn logical_size_physical_round_trip_is_lossless_for_integral_pixels() {
        for factor in [1.0_f32, 2.0, 4.0] {
            for (w, h) in [(0.0_f32, 0.0_f32), (1.0, 1.0), (100.0, 50.0), (1920.0, 1080.0)] {
                let original = LogicalSize::new(w, h);
                let round_tripped = original.to_physical(factor).to_logical(factor);
                assert_eq!(
                    original, round_tripped,
                    "round-trip lost {original:?} at dpi {factor}"
                );
            }
        }
    }
    #[test]
    fn physical_size_logical_round_trip_preserves_the_pixel_count() {
        for factor in [1.0_f32, 1.5, 2.0, 3.0] {
            for (w, h) in [(0_u32, 0_u32), (1, 1), (1920, 1080), (3840, 2160)] {
                let original = PhysicalSize::new(w, h);
                let round_tripped = original.to_logical(factor).to_physical(factor);
                assert_eq!(
                    original, round_tripped,
                    "round-trip lost {original:?} at dpi {factor}"
                );
            }
        }
    }
    #[test]
    fn screen_and_cursor_position_logical_round_trip_bit_for_bit() {
        for x in HOSTILE {
            for y in HOSTILE {
                let p = LogicalPosition::new(x, y);
                let screen = ScreenPosition::from_logical(p).to_logical();
                assert_eq!(screen.x.to_bits(), x.to_bits());
                assert_eq!(screen.y.to_bits(), y.to_bits());
                let cursor = CursorNodePosition::from_logical(p).to_logical();
                assert_eq!(cursor.x.to_bits(), x.to_bits());
                assert_eq!(cursor.y.to_bits(), y.to_bits());
            }
        }
    }
    #[test]
    fn add_sub_are_inverse_for_finite_positions() {
        let a = LogicalPosition::new(10.0, -20.0);
        let b = LogicalPosition::new(2.5, 7.5);
        assert_eq!((a + b) - b, a);
        let mut c = a;
        c += b;
        assert_eq!(c, a + b);
        c -= b;
        assert_eq!(c, a);
    }
    // ---------------------------------------------------------------------
    // Writing-mode axis mapping
    // ---------------------------------------------------------------------
    #[test]
    fn position_main_cross_round_trip_for_every_writing_mode() {
        for wm in WMS {
            for main in HOSTILE {
                for cross in HOSTILE {
                    let p = LogicalPosition::from_main_cross(main, cross, wm);
                    assert_eq!(p.main(wm).to_bits(), main.to_bits());
                    assert_eq!(p.cross(wm).to_bits(), cross.to_bits());
                }
            }
        }
    }
    #[test]
    fn size_main_cross_round_trip_for_every_writing_mode() {
        for wm in WMS {
            for main in HOSTILE {
                for cross in HOSTILE {
                    let s = LogicalSize::from_main_cross(main, cross, wm);
                    assert_eq!(s.main(wm).to_bits(), main.to_bits());
                    assert_eq!(s.cross(wm).to_bits(), cross.to_bits());
                }
            }
        }
    }
    #[test]
    fn horizontal_tb_maps_main_to_the_block_axis() {
        // In horizontal-tb the block (main) axis is vertical: main == y / height.
        let wm = LayoutWritingMode::HorizontalTb;
        let p = LogicalPosition::new(3.0, 7.0);
        assert_eq!(p.main(wm), 7.0);
        assert_eq!(p.cross(wm), 3.0);
        let s = LogicalSize::new(30.0, 70.0);
        assert_eq!(s.main(wm), 70.0);
        assert_eq!(s.cross(wm), 30.0);
    }
    #[test]
    fn vertical_modes_map_main_to_the_horizontal_axis() {
        for wm in [LayoutWritingMode::VerticalRl, LayoutWritingMode::VerticalLr] {
            let p = LogicalPosition::new(3.0, 7.0);
            assert_eq!(p.main(wm), 3.0);
            assert_eq!(p.cross(wm), 7.0);
            let s = LogicalSize::new(30.0, 70.0);
            assert_eq!(s.main(wm), 30.0);
            assert_eq!(s.cross(wm), 70.0);
        }
    }
    #[test]
    fn with_main_and_with_cross_only_touch_their_own_axis() {
        for wm in WMS {
            for v in HOSTILE {
                let s = LogicalSize::new(10.0, 20.0);
                let m = s.with_main(wm, v);
                assert_eq!(m.main(wm).to_bits(), v.to_bits());
                assert_eq!(m.cross(wm), s.cross(wm), "with_main clobbered the cross axis");
                let c = s.with_cross(wm, v);
                assert_eq!(c.cross(wm).to_bits(), v.to_bits());
                assert_eq!(c.main(wm), s.main(wm), "with_cross clobbered the main axis");
            }
        }
    }
    #[test]
    fn with_main_then_with_cross_reconstructs_from_main_cross() {
        for wm in WMS {
            let built = LogicalSize::zero().with_main(wm, 5.0).with_cross(wm, 9.0);
            assert_eq!(built, LogicalSize::from_main_cross(5.0, 9.0, wm));
        }
    }
    // ---------------------------------------------------------------------
    // Display / Debug (serializers)
    // ---------------------------------------------------------------------
    #[test]
    fn display_formats_are_well_formed_for_representative_values() {
        let p = LogicalPosition::new(1.5, -2.5);
        assert_eq!(format!("{p}"), "(1.5, -2.5)");
        assert_eq!(format!("{p:?}"), "(1.5, -2.5)");
        let s = LogicalSize::new(30.0, 40.0);
        assert_eq!(format!("{s}"), "30x40");
        assert_eq!(format!("{s:?}"), "30x40");
        let r = LogicalRect::new(p, s);
        assert_eq!(format!("{r}"), "30x40 @ (1.5, -2.5)");
        assert_eq!(format!("{r:?}"), "30x40 @ (1.5, -2.5)");
        assert_eq!(format!("{:?}", PhysicalPosition::new(1_i32, 2)), "(1, 2)");
        assert_eq!(format!("{:?}", PhysicalSize::new(1_u32, 2)), "1x2");
    }
    #[test]
    fn display_of_zero_values_is_non_empty() {
        assert!(!format!("{}", LogicalPosition::zero()).is_empty());
        assert!(!format!("{}", LogicalSize::zero()).is_empty());
        assert!(!format!("{}", LogicalRect::zero()).is_empty());
        assert_eq!(format!("{}", LogicalRect::zero()), "0x0 @ (0, 0)");
    }
    #[test]
    fn display_does_not_panic_on_nan_or_infinite_coordinates() {
        for x in HOSTILE {
            for y in HOSTILE {
                let r = LogicalRect::new(
                    LogicalPosition::new(x, y),
                    LogicalSize::new(x, y),
                );
                let shown = format!("{r}");
                assert!(!shown.is_empty());
                assert_eq!(shown, format!("{r:?}"));
            }
        }
        let nan = LogicalRect::new(
            LogicalPosition::new(f32::NAN, f32::INFINITY),
            LogicalSize::new(f32::NEG_INFINITY, f32::NAN),
        );
        assert_eq!(format!("{nan}"), "-infxNaN @ (NaN, inf)");
    }
}