1
//! Basic geometry primitives (`LayoutPoint`, `LayoutSize`, `LayoutRect`) for
2
//! layout calculations, using `isize` coordinates (as opposed to the `f32`-based
3
//! logical coordinates in `core::geom`).
4

            
5
use core::fmt;
6

            
7
use crate::{
8
    impl_option, impl_vec, impl_vec_clone, impl_vec_debug, impl_vec_mut, impl_vec_partialeq,
9
    impl_vec_partialord,
10
};
11

            
12
/// Only used for calculations: Point coordinate (x, y) in layout space.
13
#[derive(Copy, Default, Clone, PartialEq, PartialOrd, Ord, Eq, Hash)]
14
#[repr(C)]
15
pub struct LayoutPoint {
16
    pub x: isize,
17
    pub y: isize,
18
}
19

            
20
impl fmt::Debug for LayoutPoint {
21
81
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22
81
        write!(f, "{self}")
23
81
    }
24
}
25
impl fmt::Display for LayoutPoint {
26
492
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27
492
        write!(f, "({}, {})", self.x, self.y)
28
492
    }
29
}
30

            
31
impl LayoutPoint {
32
    #[inline]
33
5322
    #[must_use] pub const fn new(x: isize, y: isize) -> Self {
34
5322
        Self { x, y }
35
5322
    }
36
    #[inline]
37
85
    #[must_use] pub const fn zero() -> Self {
38
85
        Self::new(0, 0)
39
85
    }
40
}
41

            
42
impl_option!(
43
    LayoutPoint,
44
    OptionLayoutPoint,
45
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd]
46
);
47

            
48
/// Only used for calculations: Size (width, height) in layout space.
49
#[derive(Copy, Default, Clone, PartialEq, PartialOrd, Ord, Eq, Hash)]
50
#[repr(C)]
51
pub struct LayoutSize {
52
    pub width: isize,
53
    pub height: isize,
54
}
55

            
56
impl fmt::Debug for LayoutSize {
57
82
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58
82
        write!(f, "{self}")
59
82
    }
60
}
61
impl fmt::Display for LayoutSize {
62
491
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63
491
        write!(f, "{}x{}", self.width, self.height)
64
491
    }
65
}
66

            
67
impl LayoutSize {
68
    #[inline]
69
2060
    #[must_use] pub const fn new(width: isize, height: isize) -> Self {
70
2060
        Self { width, height }
71
2060
    }
72
    #[inline]
73
27
    #[must_use] pub const fn zero() -> Self {
74
27
        Self::new(0, 0)
75
27
    }
76
    #[inline]
77
623
    #[must_use] pub fn round(width: f32, height: f32) -> Self {
78
623
        Self {
79
623
            width: crate::cast::f32_to_isize(libm::roundf(width)),
80
623
            height: crate::cast::f32_to_isize(libm::roundf(height)),
81
623
        }
82
623
    }
83
}
84

            
85
impl_option!(
86
    LayoutSize,
87
    OptionLayoutSize,
88
    [Debug, Copy, Clone, PartialEq, PartialOrd, Ord, Eq, Hash]
89
);
90

            
91
/// Only used for calculations: Rectangle (x, y, width, height) in layout space.
92
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd)]
93
#[repr(C)]
94
pub struct LayoutRect {
95
    pub origin: LayoutPoint,
96
    pub size: LayoutSize,
97
}
98

            
99
impl_option!(
100
    LayoutRect,
101
    OptionLayoutRect,
102
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd]
103
);
104
impl_vec!(LayoutRect, LayoutRectVec, LayoutRectVecDestructor, LayoutRectVecDestructorType, LayoutRectVecSlice, OptionLayoutRect);
105
impl_vec_clone!(LayoutRect, LayoutRectVec, LayoutRectVecDestructor);
106
impl_vec_debug!(LayoutRect, LayoutRectVec);
107
impl_vec_mut!(LayoutRect, LayoutRectVec);
108
impl_vec_partialeq!(LayoutRect, LayoutRectVec);
109
impl_vec_partialord!(LayoutRect, LayoutRectVec);
110

            
111
impl fmt::Debug for LayoutRect {
112
82
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113
82
        write!(f, "{self}")
114
82
    }
115
}
116
impl fmt::Display for LayoutRect {
117
246
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118
246
        write!(f, "{} @ {}", self.size, self.origin)
119
246
    }
120
}
121

            
122
impl LayoutRect {
123
    #[inline]
124
413
    #[must_use] pub const fn new(origin: LayoutPoint, size: LayoutSize) -> Self {
125
413
        Self { origin, size }
126
413
    }
127
    #[inline]
128
7
    #[must_use] pub const fn zero() -> Self {
129
7
        Self::new(LayoutPoint::zero(), LayoutSize::zero())
130
7
    }
131
    #[inline]
132
7038
    #[must_use] pub const fn max_x(&self) -> isize {
133
7038
        self.origin.x.saturating_add(self.size.width)
134
7038
    }
135
    #[inline]
136
8734
    #[must_use] pub const fn min_x(&self) -> isize {
137
8734
        self.origin.x
138
8734
    }
139
    #[inline]
140
4483
    #[must_use] pub const fn max_y(&self) -> isize {
141
4483
        self.origin.y.saturating_add(self.size.height)
142
4483
    }
143
    #[inline]
144
4818
    #[must_use] pub const fn min_y(&self) -> isize {
145
4818
        self.origin.y
146
4818
    }
147
    #[inline]
148
113
    #[must_use] pub const fn width(&self) -> isize {
149
113
        self.size.width
150
113
    }
151
    #[inline]
152
113
    #[must_use] pub const fn height(&self) -> isize {
153
113
        self.size.height
154
113
    }
155

            
156
1692
    #[must_use] pub const fn contains(&self, other: &LayoutPoint) -> bool {
157
1692
        self.min_x() <= other.x
158
1160
            && other.x < self.max_x()
159
372
            && self.min_y() <= other.y
160
282
            && other.y < self.max_y()
161
1692
    }
162

            
163
1346
    #[must_use] pub fn contains_f32(&self, other_x: f32, other_y: f32) -> bool {
164
1346
        crate::cast::isize_to_f32(self.min_x()) <= other_x
165
940
            && other_x < crate::cast::isize_to_f32(self.max_x())
166
283
            && crate::cast::isize_to_f32(self.min_y()) <= other_y
167
192
            && other_y < crate::cast::isize_to_f32(self.max_y())
168
1346
    }
169

            
170
    /// Like `contains()`, but returns the (x, y) offset of the hit point
171
    /// relative to the rectangle origin. Unlike `contains()`, points exactly
172
    /// on the boundary are excluded (returns `None`).
173
    #[inline]
174
3819
    #[must_use] pub const fn hit_test(&self, other: &LayoutPoint) -> Option<LayoutPoint> {
175
3819
        let dx_left_edge = other.x.saturating_sub(self.min_x());
176
3819
        let dx_right_edge = self.max_x().saturating_sub(other.x);
177
3819
        let dy_top_edge = other.y.saturating_sub(self.min_y());
178
3819
        let dy_bottom_edge = self.max_y().saturating_sub(other.y);
179
3819
        if dx_left_edge > 0 && dx_right_edge > 0 && dy_top_edge > 0 && dy_bottom_edge > 0 {
180
177
            Some(LayoutPoint::new(dx_left_edge, dy_top_edge))
181
        } else {
182
3642
            None
183
        }
184
3819
    }
185

            
186
    /// Returns the bounding rectangle that covers every rectangle in the slice,
187
    /// or `OptionLayoutRect::None` if the slice is empty.
188
    #[inline]
189
21
    #[must_use] pub fn union(rects: LayoutRectVecSlice) -> OptionLayoutRect {
190
21
        let mut iter = rects.as_slice().iter().copied();
191
21
        let Some(first) = iter.next() else {
192
4
            return OptionLayoutRect::None;
193
        };
194

            
195
17
        let mut min_x = first.origin.x;
196
17
        let mut min_y = first.origin.y;
197
17
        let mut max_x = first.origin.x.saturating_add(first.size.width);
198
17
        let mut max_y = first.origin.y.saturating_add(first.size.height);
199

            
200
        for Self {
201
13
            origin: LayoutPoint { x, y },
202
13
            size: LayoutSize { width, height },
203
30
        } in iter
204
13
        {
205
13
            max_x = max_x.max(x.saturating_add(width));
206
13
            max_y = max_y.max(y.saturating_add(height));
207
13
            min_x = min_x.min(x);
208
13
            min_y = min_y.min(y);
209
13
        }
210

            
211
17
        OptionLayoutRect::Some(Self {
212
17
            origin: LayoutPoint { x: min_x, y: min_y },
213
17
            size: LayoutSize {
214
17
                width: max_x.saturating_sub(min_x),
215
17
                height: max_y.saturating_sub(min_y),
216
17
            },
217
17
        })
218
21
    }
219

            
220
    /// Returns true if `b` is fully contained inside `self`.
221
    #[inline]
222
    // clippy reads the symmetric containment test (`b.right <= a.right` /
223
    // `b.bottom <= a.bottom`) as a copy-paste slip and suggests `a_x + b_width`,
224
    // which would be the actual bug — the operands are intentional.
225
    #[allow(clippy::suspicious_operation_groupings)]
226
23
    #[must_use] pub const fn contains_rect(&self, b: &Self) -> bool {
227
23
        let a = self;
228

            
229
23
        let a_x = a.origin.x;
230
23
        let a_y = a.origin.y;
231
23
        let a_width = a.size.width;
232
23
        let a_height = a.size.height;
233

            
234
23
        let b_x = b.origin.x;
235
23
        let b_y = b.origin.y;
236
23
        let b_width = b.size.width;
237
23
        let b_height = b.size.height;
238

            
239
23
        b_x >= a_x
240
19
            && b_y >= a_y
241
18
            && b_x.saturating_add(b_width) <= a_x.saturating_add(a_width)
242
16
            && b_y.saturating_add(b_height) <= a_y.saturating_add(a_height)
243
23
    }
244
}
245

            
246
#[cfg(test)]
247
mod tests {
248
    use super::*;
249

            
250
4
    fn rect(x: isize, y: isize, w: isize, h: isize) -> LayoutRect {
251
4
        LayoutRect::new(LayoutPoint::new(x, y), LayoutSize::new(w, h))
252
4
    }
253

            
254
    #[test]
255
1
    fn union_slice_returns_bounding_rect() {
256
1
        let vec: LayoutRectVec =
257
1
            alloc::vec![rect(0, 0, 10, 10), rect(20, -5, 5, 30), rect(-3, 15, 4, 4)].into();
258
1
        let slice = vec.as_c_slice();
259

            
260
1
        match LayoutRect::union(slice) {
261
1
            OptionLayoutRect::Some(r) => {
262
1
                assert_eq!(r, rect(-3, -5, 28, 30));
263
            }
264
            OptionLayoutRect::None => panic!("expected Some bounding rect"),
265
        }
266
1
    }
267

            
268
    #[test]
269
1
    fn union_empty_slice_returns_none() {
270
1
        let vec: LayoutRectVec = LayoutRectVec::new();
271
1
        let slice = vec.as_c_slice();
272
1
        assert!(matches!(LayoutRect::union(slice), OptionLayoutRect::None));
273
1
    }
274
}
275

            
276
#[cfg(test)]
277
#[allow(clippy::float_cmp, clippy::unreadable_literal, clippy::cognitive_complexity)]
278
mod autotest_generated {
279
    use core::hash::{Hash, Hasher};
280

            
281
    use super::*;
282
    use crate::cast::{f32_to_isize, isize_to_f32};
283

            
284
    // ------------------------------------------------------------- helpers ---
285

            
286
    fn point(x: isize, y: isize) -> LayoutPoint {
287
        LayoutPoint::new(x, y)
288
    }
289

            
290
    fn size(w: isize, h: isize) -> LayoutSize {
291
        LayoutSize::new(w, h)
292
    }
293

            
294
    fn rect(x: isize, y: isize, w: isize, h: isize) -> LayoutRect {
295
        LayoutRect::new(point(x, y), size(w, h))
296
    }
297

            
298
    fn rect_vec(rects: &[LayoutRect]) -> LayoutRectVec {
299
        rects.to_vec().into()
300
    }
301

            
302
    /// FNV-1a, so the Hash/Eq agreement checks need no `std` hasher.
303
    struct FnvHasher(u64);
304
    impl Hasher for FnvHasher {
305
        fn finish(&self) -> u64 {
306
            self.0
307
        }
308
        fn write(&mut self, bytes: &[u8]) {
309
            for b in bytes {
310
                self.0 ^= u64::from(*b);
311
                self.0 = self.0.wrapping_mul(0x0100_0000_01b3);
312
            }
313
        }
314
    }
315

            
316
    fn hash_of<T: Hash>(v: &T) -> u64 {
317
        let mut h = FnvHasher(0xcbf2_9ce4_8422_2325);
318
        v.hash(&mut h);
319
        h.finish()
320
    }
321

            
322
    // Inverse of the `Display` impls, used for the encode==decode round-trips.
323
    // `-` and digits never contain `x`, `(`, `)` or ` @ `, so the splits are
324
    // unambiguous for every `isize`, negatives and MIN/MAX included.
325
    fn parse_point(s: &str) -> LayoutPoint {
326
        let inner = s
327
            .strip_prefix('(')
328
            .and_then(|s| s.strip_suffix(')'))
329
            .expect("LayoutPoint should be parenthesised");
330
        let (x, y) = inner.split_once(", ").expect("LayoutPoint needs a `, `");
331
        LayoutPoint::new(x.parse().expect("x"), y.parse().expect("y"))
332
    }
333

            
334
    fn parse_size(s: &str) -> LayoutSize {
335
        let (w, h) = s.split_once('x').expect("LayoutSize needs an `x`");
336
        LayoutSize::new(w.parse().expect("width"), h.parse().expect("height"))
337
    }
338

            
339
    fn parse_rect(s: &str) -> LayoutRect {
340
        let (sz, origin) = s.split_once(" @ ").expect("LayoutRect needs a ` @ `");
341
        LayoutRect::new(parse_point(origin), parse_size(sz))
342
    }
343

            
344
    /// Every value that has ever broken an `isize` boundary check.
345
    const EXTREMES: [isize; 9] = [
346
        isize::MIN,
347
        isize::MIN + 1,
348
        -1_000_000,
349
        -1,
350
        0,
351
        1,
352
        1_000_000,
353
        isize::MAX - 1,
354
        isize::MAX,
355
    ];
356

            
357
    // =================================================== constructors ========
358

            
359
    #[test]
360
    fn point_new_stores_every_extreme_verbatim() {
361
        for x in EXTREMES {
362
            for y in EXTREMES {
363
                let p = LayoutPoint::new(x, y);
364
                assert_eq!(p.x, x);
365
                assert_eq!(p.y, y);
366
                assert_eq!(p, LayoutPoint::new(x, y), "construction is not stable");
367
            }
368
        }
369
    }
370

            
371
    #[test]
372
    fn size_new_stores_every_extreme_verbatim_including_negative_sizes() {
373
        // Nothing rejects a negative width/height: the type is a plain pair.
374
        for w in EXTREMES {
375
            for h in EXTREMES {
376
                let s = LayoutSize::new(w, h);
377
                assert_eq!(s.width, w);
378
                assert_eq!(s.height, h);
379
            }
380
        }
381
    }
382

            
383
    #[test]
384
    fn rect_new_stores_origin_and_size_verbatim() {
385
        let r = LayoutRect::new(point(isize::MIN, isize::MAX), size(isize::MAX, isize::MIN));
386
        assert_eq!(r.origin, point(isize::MIN, isize::MAX));
387
        assert_eq!(r.size, size(isize::MAX, isize::MIN));
388
        // The getters that cannot overflow must agree with the fields.
389
        assert_eq!(r.min_x(), isize::MIN);
390
        assert_eq!(r.min_y(), isize::MAX);
391
        assert_eq!(r.width(), isize::MAX);
392
        assert_eq!(r.height(), isize::MIN);
393
    }
394

            
395
    #[test]
396
    fn zero_constructors_are_the_neutral_element_and_match_default() {
397
        assert_eq!(LayoutPoint::zero(), LayoutPoint::new(0, 0));
398
        assert_eq!(LayoutPoint::zero(), LayoutPoint::default());
399
        assert_eq!(LayoutSize::zero(), LayoutSize::new(0, 0));
400
        assert_eq!(LayoutSize::zero(), LayoutSize::default());
401

            
402
        // LayoutRect has no `Default`, so `zero()` is the only neutral value.
403
        let z = LayoutRect::zero();
404
        assert_eq!(z.origin, LayoutPoint::zero());
405
        assert_eq!(z.size, LayoutSize::zero());
406
        assert_eq!(z.min_x(), 0);
407
        assert_eq!(z.max_x(), 0);
408
        assert_eq!(z.min_y(), 0);
409
        assert_eq!(z.max_y(), 0);
410
        assert_eq!(z.width(), 0);
411
        assert_eq!(z.height(), 0);
412
    }
413

            
414
    #[test]
415
    fn zero_rect_is_empty_it_contains_no_point_not_even_its_own_origin() {
416
        // max is exclusive, so a 0x0 rect is a true empty set for `contains`...
417
        let z = LayoutRect::zero();
418
        assert!(!z.contains(&LayoutPoint::zero()));
419
        assert!(!z.contains_f32(0.0, 0.0));
420
        assert_eq!(z.hit_test(&LayoutPoint::zero()), None);
421
        // ...but `contains_rect` uses inclusive edges, so it still contains itself.
422
        assert!(z.contains_rect(&z));
423
    }
424

            
425
    #[test]
426
    fn constructors_are_usable_in_const_context() {
427
        const P: LayoutPoint = LayoutPoint::new(isize::MIN, isize::MAX);
428
        const S: LayoutSize = LayoutSize::new(-1, -2);
429
        const R: LayoutRect = LayoutRect::new(P, S);
430
        const Z: LayoutRect = LayoutRect::zero();
431
        const W: isize = R.width();
432

            
433
        assert_eq!(P.x, isize::MIN);
434
        assert_eq!(S.height, -2);
435
        assert_eq!(R.origin, P);
436
        assert_eq!(W, -1);
437
        assert_eq!(Z, LayoutRect::new(LayoutPoint::zero(), LayoutSize::zero()));
438
    }
439

            
440
    // =================================================== serializers =========
441

            
442
    #[test]
443
    fn display_of_extremes_is_well_formed_and_debug_delegates_to_it() {
444
        for x in EXTREMES {
445
            for y in EXTREMES {
446
                let p = LayoutPoint::new(x, y);
447
                let s = LayoutSize::new(x, y);
448
                let r = LayoutRect::new(p, s);
449

            
450
                let p_str = alloc::format!("{p}");
451
                let s_str = alloc::format!("{s}");
452
                let r_str = alloc::format!("{r}");
453

            
454
                assert_eq!(p_str, alloc::format!("({x}, {y})"));
455
                assert_eq!(s_str, alloc::format!("{x}x{y}"));
456
                assert_eq!(r_str, alloc::format!("{x}x{y} @ ({x}, {y})"));
457

            
458
                assert!(!p_str.is_empty() && !s_str.is_empty() && !r_str.is_empty());
459
                // Debug is `write!(f, "{self}")` — it must not diverge from Display.
460
                assert_eq!(alloc::format!("{p:?}"), p_str);
461
                assert_eq!(alloc::format!("{s:?}"), s_str);
462
                assert_eq!(alloc::format!("{r:?}"), r_str);
463
            }
464
        }
465
    }
466

            
467
    #[test]
468
    fn display_of_the_zero_values_does_not_panic_and_is_canonical() {
469
        assert_eq!(alloc::format!("{}", LayoutPoint::zero()), "(0, 0)");
470
        assert_eq!(alloc::format!("{}", LayoutSize::zero()), "0x0");
471
        assert_eq!(alloc::format!("{}", LayoutRect::zero()), "0x0 @ (0, 0)");
472
        assert_eq!(alloc::format!("{:?}", LayoutRect::zero()), "0x0 @ (0, 0)");
473
    }
474

            
475
    #[test]
476
    fn display_ignores_format_flags_rather_than_panicking() {
477
        // The impls use `write!` and never forward width/precision; assert that
478
        // this is a no-op instead of a panic or a truncated/padded string.
479
        let p = point(1, -2);
480
        assert_eq!(alloc::format!("{p:>40}"), "(1, -2)");
481
        assert_eq!(alloc::format!("{p:.1}"), "(1, -2)");
482
        assert_eq!(alloc::format!("{:#?}", size(3, 4)), "3x4");
483
    }
484

            
485
    // =================================================== round-trip ==========
486

            
487
    #[test]
488
    fn display_round_trips_through_a_parser_for_every_extreme() {
489
        for a in EXTREMES {
490
            for b in EXTREMES {
491
                let p = LayoutPoint::new(a, b);
492
                let s = LayoutSize::new(a, b);
493
                let r = LayoutRect::new(p, s);
494

            
495
                assert_eq!(parse_point(&alloc::format!("{p}")), p, "point {p} decoded wrong");
496
                assert_eq!(parse_size(&alloc::format!("{s}")), s, "size {s} decoded wrong");
497
                assert_eq!(parse_rect(&alloc::format!("{r}")), r, "rect {r} decoded wrong");
498
            }
499
        }
500
    }
501

            
502
    #[test]
503
    fn display_round_trips_for_a_negative_size_rect_where_the_x_separator_is_ambiguous_looking() {
504
        // "-1x-2" must not be mis-split: only digits and `-` surround the `x`.
505
        let r = rect(-7, -8, -1, -2);
506
        assert_eq!(alloc::format!("{r}"), "-1x-2 @ (-7, -8)");
507
        assert_eq!(parse_rect("-1x-2 @ (-7, -8)"), r);
508
    }
509

            
510
    // =================================================== getters =============
511

            
512
    #[test]
513
    fn getters_return_the_construction_values() {
514
        let r = rect(3, -4, 10, 20);
515
        assert_eq!(r.min_x(), 3);
516
        assert_eq!(r.min_y(), -4);
517
        assert_eq!(r.max_x(), 13);
518
        assert_eq!(r.max_y(), 16);
519
        assert_eq!(r.width(), 10);
520
        assert_eq!(r.height(), 20);
521
    }
522

            
523
    #[test]
524
    fn max_minus_min_is_the_extent_whenever_the_sum_does_not_overflow() {
525
        for x in [isize::MIN, -1, 0, 1, isize::MAX] {
526
            for w in [-1_000, -1, 0, 1, 1_000] {
527
                // Skip the combinations that would overflow `origin + size`.
528
                let Some(expected_max) = x.checked_add(w) else {
529
                    continue;
530
                };
531
                let r = rect(x, x, w, w);
532
                assert_eq!(r.max_x(), expected_max);
533
                assert_eq!(r.max_y(), expected_max);
534
                assert_eq!(r.max_x() - r.min_x(), r.width());
535
                assert_eq!(r.max_y() - r.min_y(), r.height());
536
            }
537
        }
538
    }
539

            
540
    #[test]
541
    fn getters_survive_the_widest_non_overflowing_rect() {
542
        // origin = MIN, size = MAX => max = MIN + MAX = -1. This is the largest
543
        // rect representable without tripping the (unchecked) `origin + size` add.
544
        let r = rect(isize::MIN, isize::MIN, isize::MAX, isize::MAX);
545
        assert_eq!(r.min_x(), isize::MIN);
546
        assert_eq!(r.min_y(), isize::MIN);
547
        assert_eq!(r.max_x(), -1);
548
        assert_eq!(r.max_y(), -1);
549
        assert_eq!(r.width(), isize::MAX);
550
        assert_eq!(r.height(), isize::MAX);
551

            
552
        // It really does span (almost) the whole negative half-space...
553
        assert!(r.contains(&point(isize::MIN, isize::MIN)));
554
        assert!(r.contains(&point(-2, -2)));
555
        // ...and stops one short of zero, because max is exclusive.
556
        assert!(!r.contains(&point(-1, -1)));
557
        assert!(!r.contains(&point(0, 0)));
558
    }
559

            
560
    #[test]
561
    fn max_getters_do_not_overflow_when_the_size_is_zero() {
562
        let r = rect(isize::MAX, isize::MAX, 0, 0);
563
        assert_eq!(r.max_x(), isize::MAX);
564
        assert_eq!(r.max_y(), isize::MAX);
565

            
566
        let r = rect(isize::MIN, isize::MIN, 0, 0);
567
        assert_eq!(r.max_x(), isize::MIN);
568
        assert_eq!(r.max_y(), isize::MIN);
569
    }
570

            
571
    // KNOWN HAZARD (reported, not weakened): `max_x`/`max_y` are a plain `+` on
572
    // `isize`, so an out-of-range right/bottom edge now saturates instead of
573
    // panicking (debug) / wrapping (release). These two tests pin that.
574
    #[test]
575
    fn max_x_saturates_instead_of_overflowing() {
576
        let r = core::hint::black_box(rect(isize::MAX, 0, 1, 0));
577
        assert_eq!(r.max_x(), isize::MAX);
578
    }
579

            
580
    #[test]
581
    fn max_y_saturates_instead_of_overflowing() {
582
        let r = core::hint::black_box(rect(0, isize::MIN, 0, -1));
583
        assert_eq!(r.max_y(), isize::MIN);
584
    }
585

            
586
    // =================================================== contains ============
587

            
588
    #[test]
589
    fn contains_is_min_inclusive_and_max_exclusive_on_every_edge() {
590
        let r = rect(10, 20, 5, 5); // x in [10, 15), y in [20, 25)
591
        assert!(r.contains(&point(10, 20))); // top-left corner: inside
592
        assert!(r.contains(&point(14, 24))); // last interior cell
593
        assert!(!r.contains(&point(15, 24))); // right edge: outside
594
        assert!(!r.contains(&point(14, 25))); // bottom edge: outside
595
        assert!(!r.contains(&point(15, 25))); // bottom-right corner: outside
596
        assert!(!r.contains(&point(9, 20)));
597
        assert!(!r.contains(&point(10, 19)));
598
    }
599

            
600
    #[test]
601
    fn contains_handles_negative_coordinates_deterministically() {
602
        let r = rect(-10, -10, 5, 5); // x in [-10, -5)
603
        assert!(r.contains(&point(-10, -10)));
604
        assert!(r.contains(&point(-6, -6)));
605
        assert!(!r.contains(&point(-5, -5)));
606
        assert!(!r.contains(&point(-11, -10)));
607
    }
608

            
609
    #[test]
610
    fn a_negative_size_rect_contains_nothing() {
611
        // max < min, so the half-open interval is empty for every point.
612
        let r = rect(0, 0, -5, -5);
613
        for x in -8..8 {
614
            for y in -8..8 {
615
                assert!(!r.contains(&point(x, y)), "({x}, {y}) must not be inside {r}");
616
                assert_eq!(r.hit_test(&point(x, y)), None);
617
            }
618
        }
619
    }
620

            
621
    #[test]
622
    fn contains_does_not_panic_at_the_isize_extremes_it_can_reach() {
623
        // `max_x()` is only evaluated once `min_x <= other.x`, so a rect anchored
624
        // at MAX short-circuits to false for every smaller point.
625
        let r = rect(isize::MAX, isize::MAX, 1, 1);
626
        assert!(!r.contains(&point(0, 0)));
627
        assert!(!r.contains(&point(isize::MIN, isize::MIN)));
628

            
629
        let r = rect(isize::MIN, isize::MIN, 1, 1);
630
        assert!(r.contains(&point(isize::MIN, isize::MIN)));
631
        assert!(!r.contains(&point(isize::MAX, isize::MAX)));
632
        assert!(!r.contains(&point(isize::MIN + 1, isize::MIN)));
633
    }
634

            
635
    // KNOWN HAZARD (reported): a rect wide enough that `origin.x + width`
636
    // overflows no longer makes `contains` panic: the saturating `max_x()` clamps
637
    // the right edge to isize::MAX, so an interior point is still inside.
638
    #[test]
639
    fn contains_does_not_panic_on_a_rect_whose_right_edge_overflows() {
640
        let r = core::hint::black_box(rect(1, 0, isize::MAX, 10));
641
        let p = core::hint::black_box(point(5, 5));
642
        assert!(r.contains(&p));
643
    }
644

            
645
    // =================================================== contains_f32 ========
646

            
647
    #[test]
648
    fn contains_f32_matches_contains_on_integer_coordinates() {
649
        for r in [rect(0, 0, 10, 10), rect(-5, -5, 3, 4), rect(0, 0, 0, 0)] {
650
            for x in -8..=12_isize {
651
                for y in -8..=12_isize {
652
                    assert_eq!(
653
                        r.contains_f32(isize_to_f32(x), isize_to_f32(y)),
654
                        r.contains(&point(x, y)),
655
                        "{r} disagrees about ({x}, {y})"
656
                    );
657
                }
658
            }
659
        }
660
    }
661

            
662
    #[test]
663
    fn contains_f32_is_min_inclusive_max_exclusive_for_fractional_points() {
664
        let r = rect(0, 0, 10, 10);
665
        assert!(r.contains_f32(0.0, 0.0));
666
        assert!(r.contains_f32(-0.0, -0.0)); // negative zero is still >= 0.0
667
        assert!(r.contains_f32(9.999_999, 9.999_999));
668
        assert!(!r.contains_f32(10.0, 5.0)); // exactly on max: excluded
669
        assert!(!r.contains_f32(-0.000_001, 5.0));
670
        assert!(!r.contains_f32(5.0, 10.0));
671
    }
672

            
673
    #[test]
674
    fn contains_f32_returns_false_for_nan_and_never_panics() {
675
        let r = rect(0, 0, 10, 10);
676
        // Every comparison against NaN is false, so NaN can never be "inside".
677
        assert!(!r.contains_f32(f32::NAN, 5.0));
678
        assert!(!r.contains_f32(5.0, f32::NAN));
679
        assert!(!r.contains_f32(f32::NAN, f32::NAN));
680
        assert!(!r.contains_f32(-f32::NAN, 5.0));
681
        assert!(!r.contains_f32(f32::from_bits(0x7fc0_1234), 5.0));
682
    }
683

            
684
    #[test]
685
    fn contains_f32_treats_infinities_as_outside() {
686
        let r = rect(0, 0, 10, 10);
687
        assert!(!r.contains_f32(f32::INFINITY, 5.0));
688
        assert!(!r.contains_f32(f32::NEG_INFINITY, 5.0));
689
        assert!(!r.contains_f32(5.0, f32::INFINITY));
690
        assert!(!r.contains_f32(5.0, f32::NEG_INFINITY));
691
        assert!(!r.contains_f32(f32::MAX, f32::MAX));
692
        assert!(!r.contains_f32(f32::MIN, f32::MIN));
693
    }
694

            
695
    #[test]
696
    fn contains_f32_survives_the_widest_non_overflowing_rect() {
697
        let r = rect(isize::MIN, isize::MIN, isize::MAX, isize::MAX);
698
        assert!(r.contains_f32(-1.0e18, -1.0e18));
699
        assert!(!r.contains_f32(0.0, 0.0));
700
        assert!(!r.contains_f32(f32::INFINITY, f32::INFINITY));
701
    }
702

            
703
    /// KNOWN DIVERGENCE (reported): `contains_f32` casts the edges to `f32`, so
704
    /// above 2^24 the edges snap to the nearest representable float and the
705
    /// predicate disagrees with the exact-integer `contains`.
706
    #[cfg(target_pointer_width = "64")]
707
    #[test]
708
    fn contains_f32_reports_a_point_left_of_the_rect_as_inside_past_2_pow_24() {
709
        const TWO_POW_40: isize = 1 << 40; // f32 spacing here is 2^17 = 131072
710

            
711
        // Left edge is one unit right of 2^40, but rounds *down* to 2^40 in f32.
712
        let r = rect(TWO_POW_40 + 1, 0, 1_000_000, 1_000_000);
713
        let p = point(TWO_POW_40, 1);
714

            
715
        assert!(!r.contains(&p), "exact integer math: the point is left of the rect");
716
        assert!(
717
            r.contains_f32(isize_to_f32(TWO_POW_40), 1.0),
718
            "f32 math: the rounded-down left edge swallows the point"
719
        );
720
        // The rounding is what drives it: both edges land on the same float.
721
        assert_eq!(isize_to_f32(TWO_POW_40 + 1), isize_to_f32(TWO_POW_40));
722
    }
723

            
724
    // `contains_f32` shares the saturating `max_x()` with `contains`, so an
725
    // overflowing right edge no longer panics — an interior point is inside.
726
    #[test]
727
    fn contains_f32_does_not_panic_on_a_rect_whose_right_edge_overflows() {
728
        let r = core::hint::black_box(rect(1, 0, isize::MAX, 10));
729
        assert!(r.contains_f32(core::hint::black_box(5.0), 5.0));
730
    }
731

            
732
    // =================================================== hit_test ============
733

            
734
    #[test]
735
    fn hit_test_excludes_every_boundary_and_returns_the_origin_relative_offset() {
736
        let r = rect(10, 20, 5, 5); // strict interior: x in (10, 15), y in (20, 25)
737
        assert_eq!(r.hit_test(&point(11, 21)), Some(point(1, 1)));
738
        assert_eq!(r.hit_test(&point(14, 24)), Some(point(4, 4)));
739

            
740
        // The documented difference from `contains`: the min edge is excluded.
741
        assert!(r.contains(&point(10, 20)));
742
        assert_eq!(r.hit_test(&point(10, 20)), None);
743
        assert_eq!(r.hit_test(&point(10, 22)), None);
744
        assert_eq!(r.hit_test(&point(12, 20)), None);
745
        // ...and so is the max edge, which `contains` also excludes.
746
        assert_eq!(r.hit_test(&point(15, 22)), None);
747
        assert_eq!(r.hit_test(&point(12, 25)), None);
748
    }
749

            
750
    #[test]
751
    fn hit_test_some_always_implies_contains_and_the_offset_is_exact() {
752
        for r in [rect(0, 0, 10, 10), rect(-5, -5, 3, 4), rect(2, 2, 1, 1), rect(0, 0, 0, 0)] {
753
            for x in -8..=12_isize {
754
                for y in -8..=12_isize {
755
                    let p = point(x, y);
756
                    let strictly_inside =
757
                        r.min_x() < x && x < r.max_x() && r.min_y() < y && y < r.max_y();
758
                    assert_eq!(
759
                        r.hit_test(&p).is_some(),
760
                        strictly_inside,
761
                        "{r} hit_test({p}) disagrees with the strict-interior predicate"
762
                    );
763
                    if let Some(offset) = r.hit_test(&p) {
764
                        assert_eq!(offset, point(x - r.min_x(), y - r.min_y()));
765
                        assert!(r.contains(&p), "hit_test hit a point outside contains()");
766
                        // The offset must be strictly inside the size, never negative.
767
                        assert!(offset.x > 0 && offset.x < r.width());
768
                        assert!(offset.y > 0 && offset.y < r.height());
769
                    }
770
                }
771
            }
772
        }
773
    }
774

            
775
    #[test]
776
    fn hit_test_of_a_one_by_one_rect_is_always_none_because_it_has_no_interior() {
777
        let r = rect(0, 0, 1, 1);
778
        assert!(r.contains(&point(0, 0)));
779
        for x in -2..=2 {
780
            for y in -2..=2 {
781
                assert_eq!(r.hit_test(&point(x, y)), None);
782
            }
783
        }
784
    }
785

            
786
    // `hit_test` computes all four edge deltas up front with saturating math, so
787
    // a far-away point or an overflowing right edge no longer panics. Hit-testing
788
    // is the mouse path — these were the two most reachable overflows in the file.
789
    #[test]
790
    fn hit_test_of_a_point_far_left_of_a_perfectly_ordinary_rect_is_none() {
791
        let r = core::hint::black_box(rect(0, 0, 10, 10));
792
        let p = core::hint::black_box(point(isize::MIN, 0));
793
        assert_eq!(r.hit_test(&p), None);
794
    }
795

            
796
    #[test]
797
    fn hit_test_of_a_rect_whose_right_edge_overflows_returns_the_interior_offset() {
798
        let r = core::hint::black_box(rect(1, 0, isize::MAX, 10));
799
        let p = core::hint::black_box(point(5, 5));
800
        assert_eq!(r.hit_test(&p), Some(point(4, 5)));
801
    }
802

            
803
    // =================================================== contains_rect =======
804

            
805
    #[test]
806
    fn contains_rect_is_reflexive_and_uses_inclusive_edges() {
807
        let a = rect(0, 0, 10, 10);
808
        assert!(a.contains_rect(&a));
809
        assert!(a.contains_rect(&rect(0, 0, 5, 5)));
810
        assert!(a.contains_rect(&rect(5, 5, 5, 5))); // flush with the far edge
811
        assert!(!a.contains_rect(&rect(5, 5, 6, 5))); // one past it
812
        assert!(!a.contains_rect(&rect(-1, 0, 5, 5)));
813
        assert!(!a.contains_rect(&rect(0, -1, 5, 5)));
814

            
815
        // Inclusive edges mean a degenerate rect *on* the far corner counts as
816
        // contained, even though `contains()` rejects that same corner point.
817
        assert!(a.contains_rect(&rect(10, 10, 0, 0)));
818
        assert!(!a.contains(&point(10, 10)));
819
    }
820

            
821
    #[test]
822
    fn contains_rect_is_not_symmetric() {
823
        let big = rect(0, 0, 10, 10);
824
        let small = rect(2, 2, 2, 2);
825
        assert!(big.contains_rect(&small));
826
        assert!(!small.contains_rect(&big));
827
    }
828

            
829
    #[test]
830
    fn contains_rect_wrongly_accepts_a_negative_size_rect_that_extends_far_outside() {
831
        // b's far edge is computed as b_x + b_width, which a negative width drags
832
        // *left* of a's left edge — so the "fully contained" check passes for a
833
        // rect that visually spans well outside `a`. Pinned, not endorsed.
834
        let a = rect(0, 0, 10, 10);
835
        let b = rect(5, 5, -100, -100);
836
        assert!(a.contains_rect(&b));
837
    }
838

            
839
    #[test]
840
    fn contains_rect_does_not_panic_on_the_extremes_it_can_reach() {
841
        let full = rect(0, 0, isize::MAX, isize::MAX);
842
        assert!(full.contains_rect(&full)); // 0 + MAX <= 0 + MAX
843
        assert!(full.contains_rect(&rect(0, 0, 0, 0)));
844
        assert!(!full.contains_rect(&rect(-1, 0, 0, 0)));
845

            
846
        // The MIN-anchored half-space does not contain the origin rect: its far
847
        // edge is MIN + MAX = -1, which is < 0.
848
        let half = rect(isize::MIN, isize::MIN, isize::MAX, isize::MAX);
849
        assert!(!half.contains_rect(&rect(0, 0, 0, 0)));
850
        assert!(half.contains_rect(&rect(isize::MIN, isize::MIN, 0, 0)));
851
    }
852

            
853
    // `b_x + b_width` and `a_x + a_width` now saturate, so an overflowing far
854
    // edge no longer panics: b saturates to the same isize::MAX edge as a.
855
    #[test]
856
    fn contains_rect_does_not_panic_when_the_inner_rects_far_edge_overflows() {
857
        let a = core::hint::black_box(rect(0, 0, isize::MAX, isize::MAX));
858
        let b = core::hint::black_box(rect(1, 1, isize::MAX, 1));
859
        assert!(a.contains_rect(&b));
860
    }
861

            
862
    // =================================================== union ===============
863

            
864
    #[test]
865
    fn union_of_a_single_rect_is_that_rect_even_at_the_extremes() {
866
        for r in [
867
            rect(0, 0, 0, 0),
868
            rect(-7, -8, 1, 2),
869
            rect(3, 4, -5, -6), // negative size survives the max-minus-min round-trip
870
            rect(isize::MIN, isize::MIN, isize::MAX, isize::MAX),
871
            rect(isize::MAX, isize::MAX, 0, 0),
872
        ] {
873
            let vec = rect_vec(&[r]);
874
            assert_eq!(
875
                LayoutRect::union(vec.as_c_slice()),
876
                OptionLayoutRect::Some(r),
877
                "union([{r}]) is not the identity"
878
            );
879
        }
880
    }
881

            
882
    #[test]
883
    fn union_is_idempotent_and_order_independent_for_well_formed_rects() {
884
        let a = rect(-3, 15, 4, 4);
885
        let b = rect(20, -5, 5, 30);
886

            
887
        let ab = rect_vec(&[a, b]);
888
        let ba = rect_vec(&[b, a]);
889
        assert_eq!(
890
            LayoutRect::union(ab.as_c_slice()),
891
            LayoutRect::union(ba.as_c_slice())
892
        );
893

            
894
        let aa = rect_vec(&[a, a, a]);
895
        assert_eq!(LayoutRect::union(aa.as_c_slice()), OptionLayoutRect::Some(a));
896
    }
897

            
898
    #[test]
899
    fn union_covers_every_input_rect() {
900
        let rects = [rect(0, 0, 10, 10), rect(20, -5, 5, 30), rect(-3, 15, 4, 4)];
901
        let vec = rect_vec(&rects);
902
        let OptionLayoutRect::Some(u) = LayoutRect::union(vec.as_c_slice()) else {
903
            panic!("expected Some for a non-empty slice");
904
        };
905
        for r in rects {
906
            assert!(u.contains_rect(&r), "{u} does not cover {r}");
907
        }
908
        // ...and it is tight: shrinking it by one on any side breaks the cover.
909
        let tight = rect(u.min_x() + 1, u.min_y(), u.width() - 1, u.height());
910
        assert!(rects.iter().any(|r| !tight.contains_rect(r)));
911
    }
912

            
913
    #[test]
914
    fn union_only_reads_the_slice_it_was_given() {
915
        let vec = rect_vec(&[rect(0, 0, 1, 1), rect(100, 100, 1, 1), rect(-100, -100, 1, 1)]);
916
        // A sub-range must not pull in the neighbouring rects.
917
        assert_eq!(
918
            LayoutRect::union(vec.as_c_slice_range(0, 1)),
919
            OptionLayoutRect::Some(rect(0, 0, 1, 1))
920
        );
921
        assert_eq!(
922
            LayoutRect::union(vec.as_c_slice_range(0, 2)),
923
            OptionLayoutRect::Some(rect(0, 0, 101, 101))
924
        );
925
        // An empty sub-range is the empty case, not a wild pointer read.
926
        assert!(LayoutRect::union(vec.as_c_slice_range(1, 1)).is_none());
927
    }
928

            
929
    #[test]
930
    fn union_of_an_empty_and_a_default_constructed_vec_is_none() {
931
        let empty = LayoutRectVec::new();
932
        assert!(empty.is_empty());
933
        assert_eq!(LayoutRect::union(empty.as_c_slice()), OptionLayoutRect::None);
934
        assert!(LayoutRect::union(LayoutRectVecSlice::empty()).is_none());
935
        assert_eq!(OptionLayoutRect::default(), OptionLayoutRect::None);
936
    }
937

            
938
    #[test]
939
    fn union_with_negative_size_rects_folds_them_into_a_smaller_box() {
940
        // A negative-size rect's "max" is *left of* its origin, so union tracks
941
        // (5, 5) as the far corner and never covers the origin at (10, 10).
942
        let vec = rect_vec(&[rect(10, 10, -5, -5), rect(0, 0, 2, 2)]);
943
        assert_eq!(
944
            LayoutRect::union(vec.as_c_slice()),
945
            OptionLayoutRect::Some(rect(0, 0, 5, 5))
946
        );
947
    }
948

            
949
    #[test]
950
    fn union_handles_all_negative_coordinates() {
951
        let vec = rect_vec(&[rect(-10, -10, 2, 2), rect(-30, -5, 1, 1)]);
952
        assert_eq!(
953
            LayoutRect::union(vec.as_c_slice()),
954
            OptionLayoutRect::Some(rect(-30, -10, 22, 6))
955
        );
956
    }
957

            
958
    #[test]
959
    fn union_survives_the_widest_non_overflowing_pair() {
960
        let vec = rect_vec(&[rect(isize::MIN, isize::MIN, 0, 0), rect(-1, -1, 0, 0)]);
961
        assert_eq!(
962
            LayoutRect::union(vec.as_c_slice()),
963
            OptionLayoutRect::Some(rect(isize::MIN, isize::MIN, isize::MAX, isize::MAX))
964
        );
965
    }
966

            
967
    // `union` does three `isize` operations — `x + width` per rect and `max - min`
968
    // for the result extent — all saturating now, so a bounding box exceeding
969
    // isize::MAX clamps the extent instead of panicking (debug) / wrapping (release).
970
    #[test]
971
    fn union_spanning_the_whole_isize_range_saturates_the_extent() {
972
        let vec = rect_vec(&[rect(isize::MIN, 0, 0, 0), rect(isize::MAX, 0, 0, 0)]);
973
        assert_eq!(
974
            LayoutRect::union(vec.as_c_slice()),
975
            OptionLayoutRect::Some(rect(isize::MIN, 0, isize::MAX, 0))
976
        );
977
    }
978

            
979
    #[test]
980
    fn union_of_a_rect_whose_far_edge_overflows_saturates() {
981
        let vec = rect_vec(&[rect(isize::MAX, 0, 1, 0)]);
982
        assert_eq!(
983
            LayoutRect::union(vec.as_c_slice()),
984
            OptionLayoutRect::Some(rect(isize::MAX, 0, 0, 0))
985
        );
986
    }
987

            
988
    // =================================================== LayoutSize::round ===
989

            
990
    #[test]
991
    fn round_of_zero_and_negative_zero_is_the_zero_size() {
992
        assert_eq!(LayoutSize::round(0.0, 0.0), LayoutSize::zero());
993
        assert_eq!(LayoutSize::round(-0.0, -0.0), LayoutSize::zero());
994
        assert_eq!(LayoutSize::round(0.0, -0.0), LayoutSize::zero());
995
    }
996

            
997
    #[test]
998
    fn round_goes_half_away_from_zero_not_half_to_even() {
999
        assert_eq!(LayoutSize::round(0.5, -0.5), size(1, -1));
        assert_eq!(LayoutSize::round(1.5, -1.5), size(2, -2));
        // 2.5 -> 3 (away from zero), NOT 2 (banker's rounding).
        assert_eq!(LayoutSize::round(2.5, -2.5), size(3, -3));
        assert_eq!(LayoutSize::round(3.5, -3.5), size(4, -4));
    }
    #[test]
    fn round_truncates_toward_zero_just_below_the_half() {
        // Largest f32 strictly below 0.5; must round to 0, not 1.
        let just_below_half = f32::from_bits(0x3eff_ffff);
        assert!(just_below_half < 0.5);
        assert_eq!(
            LayoutSize::round(just_below_half, -just_below_half),
            LayoutSize::zero()
        );
        assert_eq!(LayoutSize::round(0.49, -0.49), LayoutSize::zero());
        assert_eq!(LayoutSize::round(1.49, -1.49), size(1, -1));
    }
    #[test]
    fn round_of_nan_is_zero_and_does_not_panic() {
        assert_eq!(LayoutSize::round(f32::NAN, f32::NAN), LayoutSize::zero());
        assert_eq!(LayoutSize::round(f32::NAN, 5.0), size(0, 5));
        assert_eq!(LayoutSize::round(5.0, -f32::NAN), size(5, 0));
        assert_eq!(
            LayoutSize::round(f32::from_bits(0x7fc0_1234), 1.0),
            size(0, 1)
        );
    }
    #[test]
    fn round_saturates_the_infinities_to_the_isize_bounds() {
        assert_eq!(
            LayoutSize::round(f32::INFINITY, f32::NEG_INFINITY),
            size(isize::MAX, isize::MIN)
        );
        assert_eq!(
            LayoutSize::round(f32::NEG_INFINITY, f32::INFINITY),
            size(isize::MIN, isize::MAX)
        );
    }
    #[test]
    fn round_saturates_out_of_range_finite_floats_rather_than_wrapping() {
        assert_eq!(
            LayoutSize::round(f32::MAX, f32::MIN),
            size(isize::MAX, isize::MIN)
        );
        assert_eq!(LayoutSize::round(1.0e30, -1.0e30), size(isize::MAX, isize::MIN));
    }
    #[test]
    fn round_flushes_subnormals_and_tiny_magnitudes_to_zero() {
        assert_eq!(
            LayoutSize::round(f32::MIN_POSITIVE, -f32::MIN_POSITIVE),
            LayoutSize::zero()
        );
        assert_eq!(LayoutSize::round(f32::EPSILON, f32::from_bits(1)), LayoutSize::zero());
    }
    #[test]
    fn round_is_exact_for_values_inside_the_f32_integer_range() {
        assert_eq!(LayoutSize::round(1.0e9, -1.0e9), size(1_000_000_000, -1_000_000_000));
        assert_eq!(LayoutSize::round(16_777_216.0, -16_777_216.0), size(1 << 24, -(1 << 24)));
        assert_eq!(LayoutSize::round(-1.0, 1.0), size(-1, 1));
    }
    #[test]
    fn round_agrees_with_roundf_then_cast_across_a_wide_sample() {
        let samples = [
            0.0,
            -0.0,
            0.5,
            -0.5,
            2.5,
            -2.5,
            1.4999999,
            -1.4999999,
            42.7,
            -42.7,
            16_777_215.5,
            -16_777_215.5,
            1.0e18,
            -1.0e18,
            f32::MAX,
            f32::MIN,
            f32::INFINITY,
            f32::NEG_INFINITY,
            f32::NAN,
            f32::MIN_POSITIVE,
        ];
        for w in samples {
            for h in samples {
                let got = LayoutSize::round(w, h);
                assert_eq!(got.width, f32_to_isize(libm::roundf(w)));
                assert_eq!(got.height, f32_to_isize(libm::roundf(h)));
            }
        }
    }
    #[test]
    fn round_round_trips_through_f32_for_layout_sized_values() {
        // Everything a real layout produces is well below 2^24, so round() must be
        // an exact inverse of the isize->f32 cast there.
        let mut v: isize = -4_000_000;
        while v <= 4_000_000 {
            assert_eq!(
                LayoutSize::round(isize_to_f32(v), isize_to_f32(-v)),
                size(v, -v),
                "round-trip broke at {v}"
            );
            v += 40_009; // prime-ish stride, hits odd and even alike
        }
    }
    // =================================================== derived traits ======
    #[test]
    fn point_and_size_ordering_is_lexicographic_on_their_fields() {
        assert!(point(0, 1) < point(1, 0));
        assert!(point(1, 1) < point(1, 2));
        assert_eq!(point(1, 2).cmp(&point(1, 2)), core::cmp::Ordering::Equal);
        assert!(point(isize::MIN, isize::MAX) < point(isize::MAX, isize::MIN));
        assert!(size(0, 1) < size(1, 0));
        assert!(size(-1, 0) < size(0, -1));
        // LayoutRect only derives PartialOrd: origin first, then size.
        assert!(rect(0, 0, 1, 1) < rect(0, 0, 1, 2));
        assert!(rect(0, 0, 9, 9) < rect(0, 1, 0, 0));
    }
    #[test]
    fn hash_agrees_with_eq_for_points_and_sizes() {
        assert_eq!(hash_of(&point(3, -4)), hash_of(&point(3, -4)));
        assert_eq!(hash_of(&size(3, -4)), hash_of(&size(3, -4)));
        // (x, y) and (y, x) must not collide — a field-order bug would show here.
        assert_ne!(hash_of(&point(3, -4)), hash_of(&point(-4, 3)));
        assert_ne!(hash_of(&point(0, 0)), hash_of(&point(0, 1)));
        assert_eq!(hash_of(&LayoutPoint::zero()), hash_of(&LayoutPoint::default()));
    }
    #[test]
    fn option_wrappers_default_to_none_and_round_trip_through_core_option() {
        assert!(OptionLayoutPoint::default().is_none());
        assert!(OptionLayoutSize::default().is_none());
        assert!(OptionLayoutRect::default().is_none());
        let r = rect(1, 2, 3, 4);
        let o: OptionLayoutRect = Some(r).into();
        assert!(o.is_some());
        assert_eq!(o.into_option(), Some(r));
        assert_eq!(Option::<LayoutRect>::from(OptionLayoutRect::None), None);
        let p: OptionLayoutPoint = Some(point(-1, -2)).into();
        assert_eq!(p.as_ref(), Some(&point(-1, -2)));
    }
}