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
    #[must_use]
34
5325
    pub const fn new(x: isize, y: isize) -> Self {
35
5325
        Self { x, y }
36
5325
    }
37
    #[inline]
38
    #[must_use]
39
85
    pub const fn zero() -> Self {
40
85
        Self::new(0, 0)
41
85
    }
42
}
43

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

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

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

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

            
90
impl_option!(
91
    LayoutSize,
92
    OptionLayoutSize,
93
    [Debug, Copy, Clone, PartialEq, PartialOrd, Ord, Eq, Hash]
94
);
95

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

            
104
impl_option!(
105
    LayoutRect,
106
    OptionLayoutRect,
107
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd]
108
);
109
impl_vec!(
110
    LayoutRect,
111
    LayoutRectVec,
112
    LayoutRectVecDestructor,
113
    LayoutRectVecDestructorType,
114
    LayoutRectVecSlice,
115
    OptionLayoutRect
116
);
117
impl_vec_clone!(LayoutRect, LayoutRectVec, LayoutRectVecDestructor);
118
impl_vec_debug!(LayoutRect, LayoutRectVec);
119
impl_vec_mut!(LayoutRect, LayoutRectVec);
120
impl_vec_partialeq!(LayoutRect, LayoutRectVec);
121
impl_vec_partialord!(LayoutRect, LayoutRectVec);
122

            
123
impl fmt::Debug for LayoutRect {
124
82
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125
82
        write!(f, "{self}")
126
82
    }
127
}
128
impl fmt::Display for LayoutRect {
129
246
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130
246
        write!(f, "{} @ {}", self.size, self.origin)
131
246
    }
132
}
133

            
134
impl LayoutRect {
135
    #[inline]
136
    #[must_use]
137
413
    pub const fn new(origin: LayoutPoint, size: LayoutSize) -> Self {
138
413
        Self { origin, size }
139
413
    }
140
    #[inline]
141
    #[must_use]
142
7
    pub const fn zero() -> Self {
143
7
        Self::new(LayoutPoint::zero(), LayoutSize::zero())
144
7
    }
145
    #[inline]
146
    #[must_use]
147
7038
    pub const fn max_x(&self) -> isize {
148
7038
        self.origin.x.saturating_add(self.size.width)
149
7038
    }
150
    #[inline]
151
    #[must_use]
152
8734
    pub const fn min_x(&self) -> isize {
153
8734
        self.origin.x
154
8734
    }
155
    #[inline]
156
    #[must_use]
157
4483
    pub const fn max_y(&self) -> isize {
158
4483
        self.origin.y.saturating_add(self.size.height)
159
4483
    }
160
    #[inline]
161
    #[must_use]
162
4818
    pub const fn min_y(&self) -> isize {
163
4818
        self.origin.y
164
4818
    }
165
    #[inline]
166
    #[must_use]
167
113
    pub const fn width(&self) -> isize {
168
113
        self.size.width
169
113
    }
170
    #[inline]
171
    #[must_use]
172
113
    pub const fn height(&self) -> isize {
173
113
        self.size.height
174
113
    }
175

            
176
    #[must_use]
177
1692
    pub const fn contains(&self, other: &LayoutPoint) -> bool {
178
1692
        self.min_x() <= other.x
179
1160
            && other.x < self.max_x()
180
372
            && self.min_y() <= other.y
181
282
            && other.y < self.max_y()
182
1692
    }
183

            
184
    #[must_use]
185
1346
    pub fn contains_f32(&self, other_x: f32, other_y: f32) -> bool {
186
1346
        crate::cast::isize_to_f32(self.min_x()) <= other_x
187
940
            && other_x < crate::cast::isize_to_f32(self.max_x())
188
283
            && crate::cast::isize_to_f32(self.min_y()) <= other_y
189
192
            && other_y < crate::cast::isize_to_f32(self.max_y())
190
1346
    }
191

            
192
    /// Like `contains()`, but returns the (x, y) offset of the hit point
193
    /// relative to the rectangle origin. Unlike `contains()`, points exactly
194
    /// on the boundary are excluded (returns `None`).
195
    #[inline]
196
    #[must_use]
197
3819
    pub const fn hit_test(&self, other: &LayoutPoint) -> Option<LayoutPoint> {
198
3819
        let dx_left_edge = other.x.saturating_sub(self.min_x());
199
3819
        let dx_right_edge = self.max_x().saturating_sub(other.x);
200
3819
        let dy_top_edge = other.y.saturating_sub(self.min_y());
201
3819
        let dy_bottom_edge = self.max_y().saturating_sub(other.y);
202
3819
        if dx_left_edge > 0 && dx_right_edge > 0 && dy_top_edge > 0 && dy_bottom_edge > 0 {
203
177
            Some(LayoutPoint::new(dx_left_edge, dy_top_edge))
204
        } else {
205
3642
            None
206
        }
207
3819
    }
208

            
209
    /// Returns the bounding rectangle that covers every rectangle in the slice,
210
    /// or `OptionLayoutRect::None` if the slice is empty.
211
    #[inline]
212
    #[must_use]
213
21
    pub fn union(rects: LayoutRectVecSlice) -> OptionLayoutRect {
214
21
        let mut iter = rects.as_slice().iter().copied();
215
21
        let Some(first) = iter.next() else {
216
4
            return OptionLayoutRect::None;
217
        };
218

            
219
17
        let mut min_x = first.origin.x;
220
17
        let mut min_y = first.origin.y;
221
17
        let mut max_x = first.origin.x.saturating_add(first.size.width);
222
17
        let mut max_y = first.origin.y.saturating_add(first.size.height);
223

            
224
        for Self {
225
13
            origin: LayoutPoint { x, y },
226
13
            size: LayoutSize { width, height },
227
30
        } in iter
228
13
        {
229
13
            max_x = max_x.max(x.saturating_add(width));
230
13
            max_y = max_y.max(y.saturating_add(height));
231
13
            min_x = min_x.min(x);
232
13
            min_y = min_y.min(y);
233
13
        }
234

            
235
17
        OptionLayoutRect::Some(Self {
236
17
            origin: LayoutPoint { x: min_x, y: min_y },
237
17
            size: LayoutSize {
238
17
                width: max_x.saturating_sub(min_x),
239
17
                height: max_y.saturating_sub(min_y),
240
17
            },
241
17
        })
242
21
    }
243

            
244
    /// Returns true if `b` is fully contained inside `self`.
245
    #[inline]
246
    // clippy reads the symmetric containment test (`b.right <= a.right` /
247
    // `b.bottom <= a.bottom`) as a copy-paste slip and suggests `a_x + b_width`,
248
    // which would be the actual bug — the operands are intentional.
249
    #[allow(clippy::suspicious_operation_groupings)]
250
    #[must_use]
251
23
    pub const fn contains_rect(&self, b: &Self) -> bool {
252
23
        let a = self;
253

            
254
23
        let a_x = a.origin.x;
255
23
        let a_y = a.origin.y;
256
23
        let a_width = a.size.width;
257
23
        let a_height = a.size.height;
258

            
259
23
        let b_x = b.origin.x;
260
23
        let b_y = b.origin.y;
261
23
        let b_width = b.size.width;
262
23
        let b_height = b.size.height;
263

            
264
23
        b_x >= a_x
265
19
            && b_y >= a_y
266
18
            && b_x.saturating_add(b_width) <= a_x.saturating_add(a_width)
267
16
            && b_y.saturating_add(b_height) <= a_y.saturating_add(a_height)
268
23
    }
269
}
270

            
271
#[cfg(test)]
272
mod tests {
273
    use super::*;
274

            
275
4
    fn rect(x: isize, y: isize, w: isize, h: isize) -> LayoutRect {
276
4
        LayoutRect::new(LayoutPoint::new(x, y), LayoutSize::new(w, h))
277
4
    }
278

            
279
    #[test]
280
1
    fn union_slice_returns_bounding_rect() {
281
1
        let vec: LayoutRectVec =
282
1
            alloc::vec![rect(0, 0, 10, 10), rect(20, -5, 5, 30), rect(-3, 15, 4, 4)].into();
283
1
        let slice = vec.as_c_slice();
284

            
285
1
        match LayoutRect::union(slice) {
286
1
            OptionLayoutRect::Some(r) => {
287
1
                assert_eq!(r, rect(-3, -5, 28, 30));
288
            }
289
            OptionLayoutRect::None => panic!("expected Some bounding rect"),
290
        }
291
1
    }
292

            
293
    #[test]
294
1
    fn union_empty_slice_returns_none() {
295
1
        let vec: LayoutRectVec = LayoutRectVec::new();
296
1
        let slice = vec.as_c_slice();
297
1
        assert!(matches!(LayoutRect::union(slice), OptionLayoutRect::None));
298
1
    }
299
}
300

            
301
#[cfg(test)]
302
#[allow(
303
    clippy::float_cmp,
304
    clippy::unreadable_literal,
305
    clippy::cognitive_complexity
306
)]
307
mod autotest_generated {
308
    use core::hash::{Hash, Hasher};
309

            
310
    use super::*;
311
    use crate::cast::{f32_to_isize, isize_to_f32};
312

            
313
    // ------------------------------------------------------------- helpers ---
314

            
315
    fn point(x: isize, y: isize) -> LayoutPoint {
316
        LayoutPoint::new(x, y)
317
    }
318

            
319
    fn size(w: isize, h: isize) -> LayoutSize {
320
        LayoutSize::new(w, h)
321
    }
322

            
323
    fn rect(x: isize, y: isize, w: isize, h: isize) -> LayoutRect {
324
        LayoutRect::new(point(x, y), size(w, h))
325
    }
326

            
327
    fn rect_vec(rects: &[LayoutRect]) -> LayoutRectVec {
328
        rects.to_vec().into()
329
    }
330

            
331
    /// FNV-1a, so the Hash/Eq agreement checks need no `std` hasher.
332
    struct FnvHasher(u64);
333
    impl Hasher for FnvHasher {
334
        fn finish(&self) -> u64 {
335
            self.0
336
        }
337
        fn write(&mut self, bytes: &[u8]) {
338
            for b in bytes {
339
                self.0 ^= u64::from(*b);
340
                self.0 = self.0.wrapping_mul(0x0100_0000_01b3);
341
            }
342
        }
343
    }
344

            
345
    fn hash_of<T: Hash>(v: &T) -> u64 {
346
        let mut h = FnvHasher(0xcbf2_9ce4_8422_2325);
347
        v.hash(&mut h);
348
        h.finish()
349
    }
350

            
351
    // Inverse of the `Display` impls, used for the encode==decode round-trips.
352
    // `-` and digits never contain `x`, `(`, `)` or ` @ `, so the splits are
353
    // unambiguous for every `isize`, negatives and MIN/MAX included.
354
    fn parse_point(s: &str) -> LayoutPoint {
355
        let inner = s
356
            .strip_prefix('(')
357
            .and_then(|s| s.strip_suffix(')'))
358
            .expect("LayoutPoint should be parenthesised");
359
        let (x, y) = inner.split_once(", ").expect("LayoutPoint needs a `, `");
360
        LayoutPoint::new(x.parse().expect("x"), y.parse().expect("y"))
361
    }
362

            
363
    fn parse_size(s: &str) -> LayoutSize {
364
        let (w, h) = s.split_once('x').expect("LayoutSize needs an `x`");
365
        LayoutSize::new(w.parse().expect("width"), h.parse().expect("height"))
366
    }
367

            
368
    fn parse_rect(s: &str) -> LayoutRect {
369
        let (sz, origin) = s.split_once(" @ ").expect("LayoutRect needs a ` @ `");
370
        LayoutRect::new(parse_point(origin), parse_size(sz))
371
    }
372

            
373
    /// Every value that has ever broken an `isize` boundary check.
374
    const EXTREMES: [isize; 9] = [
375
        isize::MIN,
376
        isize::MIN + 1,
377
        -1_000_000,
378
        -1,
379
        0,
380
        1,
381
        1_000_000,
382
        isize::MAX - 1,
383
        isize::MAX,
384
    ];
385

            
386
    // =================================================== constructors ========
387

            
388
    #[test]
389
    fn point_new_stores_every_extreme_verbatim() {
390
        for x in EXTREMES {
391
            for y in EXTREMES {
392
                let p = LayoutPoint::new(x, y);
393
                assert_eq!(p.x, x);
394
                assert_eq!(p.y, y);
395
                assert_eq!(p, LayoutPoint::new(x, y), "construction is not stable");
396
            }
397
        }
398
    }
399

            
400
    #[test]
401
    fn size_new_stores_every_extreme_verbatim_including_negative_sizes() {
402
        // Nothing rejects a negative width/height: the type is a plain pair.
403
        for w in EXTREMES {
404
            for h in EXTREMES {
405
                let s = LayoutSize::new(w, h);
406
                assert_eq!(s.width, w);
407
                assert_eq!(s.height, h);
408
            }
409
        }
410
    }
411

            
412
    #[test]
413
    fn rect_new_stores_origin_and_size_verbatim() {
414
        let r = LayoutRect::new(point(isize::MIN, isize::MAX), size(isize::MAX, isize::MIN));
415
        assert_eq!(r.origin, point(isize::MIN, isize::MAX));
416
        assert_eq!(r.size, size(isize::MAX, isize::MIN));
417
        // The getters that cannot overflow must agree with the fields.
418
        assert_eq!(r.min_x(), isize::MIN);
419
        assert_eq!(r.min_y(), isize::MAX);
420
        assert_eq!(r.width(), isize::MAX);
421
        assert_eq!(r.height(), isize::MIN);
422
    }
423

            
424
    #[test]
425
    fn zero_constructors_are_the_neutral_element_and_match_default() {
426
        assert_eq!(LayoutPoint::zero(), LayoutPoint::new(0, 0));
427
        assert_eq!(LayoutPoint::zero(), LayoutPoint::default());
428
        assert_eq!(LayoutSize::zero(), LayoutSize::new(0, 0));
429
        assert_eq!(LayoutSize::zero(), LayoutSize::default());
430

            
431
        // LayoutRect has no `Default`, so `zero()` is the only neutral value.
432
        let z = LayoutRect::zero();
433
        assert_eq!(z.origin, LayoutPoint::zero());
434
        assert_eq!(z.size, LayoutSize::zero());
435
        assert_eq!(z.min_x(), 0);
436
        assert_eq!(z.max_x(), 0);
437
        assert_eq!(z.min_y(), 0);
438
        assert_eq!(z.max_y(), 0);
439
        assert_eq!(z.width(), 0);
440
        assert_eq!(z.height(), 0);
441
    }
442

            
443
    #[test]
444
    fn zero_rect_is_empty_it_contains_no_point_not_even_its_own_origin() {
445
        // max is exclusive, so a 0x0 rect is a true empty set for `contains`...
446
        let z = LayoutRect::zero();
447
        assert!(!z.contains(&LayoutPoint::zero()));
448
        assert!(!z.contains_f32(0.0, 0.0));
449
        assert_eq!(z.hit_test(&LayoutPoint::zero()), None);
450
        // ...but `contains_rect` uses inclusive edges, so it still contains itself.
451
        assert!(z.contains_rect(&z));
452
    }
453

            
454
    #[test]
455
    fn constructors_are_usable_in_const_context() {
456
        const P: LayoutPoint = LayoutPoint::new(isize::MIN, isize::MAX);
457
        const S: LayoutSize = LayoutSize::new(-1, -2);
458
        const R: LayoutRect = LayoutRect::new(P, S);
459
        const Z: LayoutRect = LayoutRect::zero();
460
        const W: isize = R.width();
461

            
462
        assert_eq!(P.x, isize::MIN);
463
        assert_eq!(S.height, -2);
464
        assert_eq!(R.origin, P);
465
        assert_eq!(W, -1);
466
        assert_eq!(Z, LayoutRect::new(LayoutPoint::zero(), LayoutSize::zero()));
467
    }
468

            
469
    // =================================================== serializers =========
470

            
471
    #[test]
472
    fn display_of_extremes_is_well_formed_and_debug_delegates_to_it() {
473
        for x in EXTREMES {
474
            for y in EXTREMES {
475
                let p = LayoutPoint::new(x, y);
476
                let s = LayoutSize::new(x, y);
477
                let r = LayoutRect::new(p, s);
478

            
479
                let p_str = alloc::format!("{p}");
480
                let s_str = alloc::format!("{s}");
481
                let r_str = alloc::format!("{r}");
482

            
483
                assert_eq!(p_str, alloc::format!("({x}, {y})"));
484
                assert_eq!(s_str, alloc::format!("{x}x{y}"));
485
                assert_eq!(r_str, alloc::format!("{x}x{y} @ ({x}, {y})"));
486

            
487
                assert!(!p_str.is_empty() && !s_str.is_empty() && !r_str.is_empty());
488
                // Debug is `write!(f, "{self}")` — it must not diverge from Display.
489
                assert_eq!(alloc::format!("{p:?}"), p_str);
490
                assert_eq!(alloc::format!("{s:?}"), s_str);
491
                assert_eq!(alloc::format!("{r:?}"), r_str);
492
            }
493
        }
494
    }
495

            
496
    #[test]
497
    fn display_of_the_zero_values_does_not_panic_and_is_canonical() {
498
        assert_eq!(alloc::format!("{}", LayoutPoint::zero()), "(0, 0)");
499
        assert_eq!(alloc::format!("{}", LayoutSize::zero()), "0x0");
500
        assert_eq!(alloc::format!("{}", LayoutRect::zero()), "0x0 @ (0, 0)");
501
        assert_eq!(alloc::format!("{:?}", LayoutRect::zero()), "0x0 @ (0, 0)");
502
    }
503

            
504
    #[test]
505
    fn display_ignores_format_flags_rather_than_panicking() {
506
        // The impls use `write!` and never forward width/precision; assert that
507
        // this is a no-op instead of a panic or a truncated/padded string.
508
        let p = point(1, -2);
509
        assert_eq!(alloc::format!("{p:>40}"), "(1, -2)");
510
        assert_eq!(alloc::format!("{p:.1}"), "(1, -2)");
511
        assert_eq!(alloc::format!("{:#?}", size(3, 4)), "3x4");
512
    }
513

            
514
    // =================================================== round-trip ==========
515

            
516
    #[test]
517
    fn display_round_trips_through_a_parser_for_every_extreme() {
518
        for a in EXTREMES {
519
            for b in EXTREMES {
520
                let p = LayoutPoint::new(a, b);
521
                let s = LayoutSize::new(a, b);
522
                let r = LayoutRect::new(p, s);
523

            
524
                assert_eq!(
525
                    parse_point(&alloc::format!("{p}")),
526
                    p,
527
                    "point {p} decoded wrong"
528
                );
529
                assert_eq!(
530
                    parse_size(&alloc::format!("{s}")),
531
                    s,
532
                    "size {s} decoded wrong"
533
                );
534
                assert_eq!(
535
                    parse_rect(&alloc::format!("{r}")),
536
                    r,
537
                    "rect {r} decoded wrong"
538
                );
539
            }
540
        }
541
    }
542

            
543
    #[test]
544
    fn display_round_trips_for_a_negative_size_rect_where_the_x_separator_is_ambiguous_looking() {
545
        // "-1x-2" must not be mis-split: only digits and `-` surround the `x`.
546
        let r = rect(-7, -8, -1, -2);
547
        assert_eq!(alloc::format!("{r}"), "-1x-2 @ (-7, -8)");
548
        assert_eq!(parse_rect("-1x-2 @ (-7, -8)"), r);
549
    }
550

            
551
    // =================================================== getters =============
552

            
553
    #[test]
554
    fn getters_return_the_construction_values() {
555
        let r = rect(3, -4, 10, 20);
556
        assert_eq!(r.min_x(), 3);
557
        assert_eq!(r.min_y(), -4);
558
        assert_eq!(r.max_x(), 13);
559
        assert_eq!(r.max_y(), 16);
560
        assert_eq!(r.width(), 10);
561
        assert_eq!(r.height(), 20);
562
    }
563

            
564
    #[test]
565
    fn max_minus_min_is_the_extent_whenever_the_sum_does_not_overflow() {
566
        for x in [isize::MIN, -1, 0, 1, isize::MAX] {
567
            for w in [-1_000, -1, 0, 1, 1_000] {
568
                // Skip the combinations that would overflow `origin + size`.
569
                let Some(expected_max) = x.checked_add(w) else {
570
                    continue;
571
                };
572
                let r = rect(x, x, w, w);
573
                assert_eq!(r.max_x(), expected_max);
574
                assert_eq!(r.max_y(), expected_max);
575
                assert_eq!(r.max_x() - r.min_x(), r.width());
576
                assert_eq!(r.max_y() - r.min_y(), r.height());
577
            }
578
        }
579
    }
580

            
581
    #[test]
582
    fn getters_survive_the_widest_non_overflowing_rect() {
583
        // origin = MIN, size = MAX => max = MIN + MAX = -1. This is the largest
584
        // rect representable without tripping the (unchecked) `origin + size` add.
585
        let r = rect(isize::MIN, isize::MIN, isize::MAX, isize::MAX);
586
        assert_eq!(r.min_x(), isize::MIN);
587
        assert_eq!(r.min_y(), isize::MIN);
588
        assert_eq!(r.max_x(), -1);
589
        assert_eq!(r.max_y(), -1);
590
        assert_eq!(r.width(), isize::MAX);
591
        assert_eq!(r.height(), isize::MAX);
592

            
593
        // It really does span (almost) the whole negative half-space...
594
        assert!(r.contains(&point(isize::MIN, isize::MIN)));
595
        assert!(r.contains(&point(-2, -2)));
596
        // ...and stops one short of zero, because max is exclusive.
597
        assert!(!r.contains(&point(-1, -1)));
598
        assert!(!r.contains(&point(0, 0)));
599
    }
600

            
601
    #[test]
602
    fn max_getters_do_not_overflow_when_the_size_is_zero() {
603
        let r = rect(isize::MAX, isize::MAX, 0, 0);
604
        assert_eq!(r.max_x(), isize::MAX);
605
        assert_eq!(r.max_y(), isize::MAX);
606

            
607
        let r = rect(isize::MIN, isize::MIN, 0, 0);
608
        assert_eq!(r.max_x(), isize::MIN);
609
        assert_eq!(r.max_y(), isize::MIN);
610
    }
611

            
612
    // KNOWN HAZARD (reported, not weakened): `max_x`/`max_y` are a plain `+` on
613
    // `isize`, so an out-of-range right/bottom edge now saturates instead of
614
    // panicking (debug) / wrapping (release). These two tests pin that.
615
    #[test]
616
    fn max_x_saturates_instead_of_overflowing() {
617
        let r = core::hint::black_box(rect(isize::MAX, 0, 1, 0));
618
        assert_eq!(r.max_x(), isize::MAX);
619
    }
620

            
621
    #[test]
622
    fn max_y_saturates_instead_of_overflowing() {
623
        let r = core::hint::black_box(rect(0, isize::MIN, 0, -1));
624
        assert_eq!(r.max_y(), isize::MIN);
625
    }
626

            
627
    // =================================================== contains ============
628

            
629
    #[test]
630
    fn contains_is_min_inclusive_and_max_exclusive_on_every_edge() {
631
        let r = rect(10, 20, 5, 5); // x in [10, 15), y in [20, 25)
632
        assert!(r.contains(&point(10, 20))); // top-left corner: inside
633
        assert!(r.contains(&point(14, 24))); // last interior cell
634
        assert!(!r.contains(&point(15, 24))); // right edge: outside
635
        assert!(!r.contains(&point(14, 25))); // bottom edge: outside
636
        assert!(!r.contains(&point(15, 25))); // bottom-right corner: outside
637
        assert!(!r.contains(&point(9, 20)));
638
        assert!(!r.contains(&point(10, 19)));
639
    }
640

            
641
    #[test]
642
    fn contains_handles_negative_coordinates_deterministically() {
643
        let r = rect(-10, -10, 5, 5); // x in [-10, -5)
644
        assert!(r.contains(&point(-10, -10)));
645
        assert!(r.contains(&point(-6, -6)));
646
        assert!(!r.contains(&point(-5, -5)));
647
        assert!(!r.contains(&point(-11, -10)));
648
    }
649

            
650
    #[test]
651
    fn a_negative_size_rect_contains_nothing() {
652
        // max < min, so the half-open interval is empty for every point.
653
        let r = rect(0, 0, -5, -5);
654
        for x in -8..8 {
655
            for y in -8..8 {
656
                assert!(
657
                    !r.contains(&point(x, y)),
658
                    "({x}, {y}) must not be inside {r}"
659
                );
660
                assert_eq!(r.hit_test(&point(x, y)), None);
661
            }
662
        }
663
    }
664

            
665
    #[test]
666
    fn contains_does_not_panic_at_the_isize_extremes_it_can_reach() {
667
        // `max_x()` is only evaluated once `min_x <= other.x`, so a rect anchored
668
        // at MAX short-circuits to false for every smaller point.
669
        let r = rect(isize::MAX, isize::MAX, 1, 1);
670
        assert!(!r.contains(&point(0, 0)));
671
        assert!(!r.contains(&point(isize::MIN, isize::MIN)));
672

            
673
        let r = rect(isize::MIN, isize::MIN, 1, 1);
674
        assert!(r.contains(&point(isize::MIN, isize::MIN)));
675
        assert!(!r.contains(&point(isize::MAX, isize::MAX)));
676
        assert!(!r.contains(&point(isize::MIN + 1, isize::MIN)));
677
    }
678

            
679
    // KNOWN HAZARD (reported): a rect wide enough that `origin.x + width`
680
    // overflows no longer makes `contains` panic: the saturating `max_x()` clamps
681
    // the right edge to isize::MAX, so an interior point is still inside.
682
    #[test]
683
    fn contains_does_not_panic_on_a_rect_whose_right_edge_overflows() {
684
        let r = core::hint::black_box(rect(1, 0, isize::MAX, 10));
685
        let p = core::hint::black_box(point(5, 5));
686
        assert!(r.contains(&p));
687
    }
688

            
689
    // =================================================== contains_f32 ========
690

            
691
    #[test]
692
    fn contains_f32_matches_contains_on_integer_coordinates() {
693
        for r in [rect(0, 0, 10, 10), rect(-5, -5, 3, 4), rect(0, 0, 0, 0)] {
694
            for x in -8..=12_isize {
695
                for y in -8..=12_isize {
696
                    assert_eq!(
697
                        r.contains_f32(isize_to_f32(x), isize_to_f32(y)),
698
                        r.contains(&point(x, y)),
699
                        "{r} disagrees about ({x}, {y})"
700
                    );
701
                }
702
            }
703
        }
704
    }
705

            
706
    #[test]
707
    fn contains_f32_is_min_inclusive_max_exclusive_for_fractional_points() {
708
        let r = rect(0, 0, 10, 10);
709
        assert!(r.contains_f32(0.0, 0.0));
710
        assert!(r.contains_f32(-0.0, -0.0)); // negative zero is still >= 0.0
711
        assert!(r.contains_f32(9.999_999, 9.999_999));
712
        assert!(!r.contains_f32(10.0, 5.0)); // exactly on max: excluded
713
        assert!(!r.contains_f32(-0.000_001, 5.0));
714
        assert!(!r.contains_f32(5.0, 10.0));
715
    }
716

            
717
    #[test]
718
    fn contains_f32_returns_false_for_nan_and_never_panics() {
719
        let r = rect(0, 0, 10, 10);
720
        // Every comparison against NaN is false, so NaN can never be "inside".
721
        assert!(!r.contains_f32(f32::NAN, 5.0));
722
        assert!(!r.contains_f32(5.0, f32::NAN));
723
        assert!(!r.contains_f32(f32::NAN, f32::NAN));
724
        assert!(!r.contains_f32(-f32::NAN, 5.0));
725
        assert!(!r.contains_f32(f32::from_bits(0x7fc0_1234), 5.0));
726
    }
727

            
728
    #[test]
729
    fn contains_f32_treats_infinities_as_outside() {
730
        let r = rect(0, 0, 10, 10);
731
        assert!(!r.contains_f32(f32::INFINITY, 5.0));
732
        assert!(!r.contains_f32(f32::NEG_INFINITY, 5.0));
733
        assert!(!r.contains_f32(5.0, f32::INFINITY));
734
        assert!(!r.contains_f32(5.0, f32::NEG_INFINITY));
735
        assert!(!r.contains_f32(f32::MAX, f32::MAX));
736
        assert!(!r.contains_f32(f32::MIN, f32::MIN));
737
    }
738

            
739
    #[test]
740
    fn contains_f32_survives_the_widest_non_overflowing_rect() {
741
        let r = rect(isize::MIN, isize::MIN, isize::MAX, isize::MAX);
742
        assert!(r.contains_f32(-1.0e18, -1.0e18));
743
        assert!(!r.contains_f32(0.0, 0.0));
744
        assert!(!r.contains_f32(f32::INFINITY, f32::INFINITY));
745
    }
746

            
747
    /// KNOWN DIVERGENCE (reported): `contains_f32` casts the edges to `f32`, so
748
    /// above 2^24 the edges snap to the nearest representable float and the
749
    /// predicate disagrees with the exact-integer `contains`.
750
    #[cfg(target_pointer_width = "64")]
751
    #[test]
752
    fn contains_f32_reports_a_point_left_of_the_rect_as_inside_past_2_pow_24() {
753
        const TWO_POW_40: isize = 1 << 40; // f32 spacing here is 2^17 = 131072
754

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

            
759
        assert!(
760
            !r.contains(&p),
761
            "exact integer math: the point is left of the rect"
762
        );
763
        assert!(
764
            r.contains_f32(isize_to_f32(TWO_POW_40), 1.0),
765
            "f32 math: the rounded-down left edge swallows the point"
766
        );
767
        // The rounding is what drives it: both edges land on the same float.
768
        assert_eq!(isize_to_f32(TWO_POW_40 + 1), isize_to_f32(TWO_POW_40));
769
    }
770

            
771
    // `contains_f32` shares the saturating `max_x()` with `contains`, so an
772
    // overflowing right edge no longer panics — an interior point is inside.
773
    #[test]
774
    fn contains_f32_does_not_panic_on_a_rect_whose_right_edge_overflows() {
775
        let r = core::hint::black_box(rect(1, 0, isize::MAX, 10));
776
        assert!(r.contains_f32(core::hint::black_box(5.0), 5.0));
777
    }
778

            
779
    // =================================================== hit_test ============
780

            
781
    #[test]
782
    fn hit_test_excludes_every_boundary_and_returns_the_origin_relative_offset() {
783
        let r = rect(10, 20, 5, 5); // strict interior: x in (10, 15), y in (20, 25)
784
        assert_eq!(r.hit_test(&point(11, 21)), Some(point(1, 1)));
785
        assert_eq!(r.hit_test(&point(14, 24)), Some(point(4, 4)));
786

            
787
        // The documented difference from `contains`: the min edge is excluded.
788
        assert!(r.contains(&point(10, 20)));
789
        assert_eq!(r.hit_test(&point(10, 20)), None);
790
        assert_eq!(r.hit_test(&point(10, 22)), None);
791
        assert_eq!(r.hit_test(&point(12, 20)), None);
792
        // ...and so is the max edge, which `contains` also excludes.
793
        assert_eq!(r.hit_test(&point(15, 22)), None);
794
        assert_eq!(r.hit_test(&point(12, 25)), None);
795
    }
796

            
797
    #[test]
798
    fn hit_test_some_always_implies_contains_and_the_offset_is_exact() {
799
        for r in [
800
            rect(0, 0, 10, 10),
801
            rect(-5, -5, 3, 4),
802
            rect(2, 2, 1, 1),
803
            rect(0, 0, 0, 0),
804
        ] {
805
            for x in -8..=12_isize {
806
                for y in -8..=12_isize {
807
                    let p = point(x, y);
808
                    let strictly_inside =
809
                        r.min_x() < x && x < r.max_x() && r.min_y() < y && y < r.max_y();
810
                    assert_eq!(
811
                        r.hit_test(&p).is_some(),
812
                        strictly_inside,
813
                        "{r} hit_test({p}) disagrees with the strict-interior predicate"
814
                    );
815
                    if let Some(offset) = r.hit_test(&p) {
816
                        assert_eq!(offset, point(x - r.min_x(), y - r.min_y()));
817
                        assert!(r.contains(&p), "hit_test hit a point outside contains()");
818
                        // The offset must be strictly inside the size, never negative.
819
                        assert!(offset.x > 0 && offset.x < r.width());
820
                        assert!(offset.y > 0 && offset.y < r.height());
821
                    }
822
                }
823
            }
824
        }
825
    }
826

            
827
    #[test]
828
    fn hit_test_of_a_one_by_one_rect_is_always_none_because_it_has_no_interior() {
829
        let r = rect(0, 0, 1, 1);
830
        assert!(r.contains(&point(0, 0)));
831
        for x in -2..=2 {
832
            for y in -2..=2 {
833
                assert_eq!(r.hit_test(&point(x, y)), None);
834
            }
835
        }
836
    }
837

            
838
    // `hit_test` computes all four edge deltas up front with saturating math, so
839
    // a far-away point or an overflowing right edge no longer panics. Hit-testing
840
    // is the mouse path — these were the two most reachable overflows in the file.
841
    #[test]
842
    fn hit_test_of_a_point_far_left_of_a_perfectly_ordinary_rect_is_none() {
843
        let r = core::hint::black_box(rect(0, 0, 10, 10));
844
        let p = core::hint::black_box(point(isize::MIN, 0));
845
        assert_eq!(r.hit_test(&p), None);
846
    }
847

            
848
    #[test]
849
    fn hit_test_of_a_rect_whose_right_edge_overflows_returns_the_interior_offset() {
850
        let r = core::hint::black_box(rect(1, 0, isize::MAX, 10));
851
        let p = core::hint::black_box(point(5, 5));
852
        assert_eq!(r.hit_test(&p), Some(point(4, 5)));
853
    }
854

            
855
    // =================================================== contains_rect =======
856

            
857
    #[test]
858
    fn contains_rect_is_reflexive_and_uses_inclusive_edges() {
859
        let a = rect(0, 0, 10, 10);
860
        assert!(a.contains_rect(&a));
861
        assert!(a.contains_rect(&rect(0, 0, 5, 5)));
862
        assert!(a.contains_rect(&rect(5, 5, 5, 5))); // flush with the far edge
863
        assert!(!a.contains_rect(&rect(5, 5, 6, 5))); // one past it
864
        assert!(!a.contains_rect(&rect(-1, 0, 5, 5)));
865
        assert!(!a.contains_rect(&rect(0, -1, 5, 5)));
866

            
867
        // Inclusive edges mean a degenerate rect *on* the far corner counts as
868
        // contained, even though `contains()` rejects that same corner point.
869
        assert!(a.contains_rect(&rect(10, 10, 0, 0)));
870
        assert!(!a.contains(&point(10, 10)));
871
    }
872

            
873
    #[test]
874
    fn contains_rect_is_not_symmetric() {
875
        let big = rect(0, 0, 10, 10);
876
        let small = rect(2, 2, 2, 2);
877
        assert!(big.contains_rect(&small));
878
        assert!(!small.contains_rect(&big));
879
    }
880

            
881
    #[test]
882
    fn contains_rect_wrongly_accepts_a_negative_size_rect_that_extends_far_outside() {
883
        // b's far edge is computed as b_x + b_width, which a negative width drags
884
        // *left* of a's left edge — so the "fully contained" check passes for a
885
        // rect that visually spans well outside `a`. Pinned, not endorsed.
886
        let a = rect(0, 0, 10, 10);
887
        let b = rect(5, 5, -100, -100);
888
        assert!(a.contains_rect(&b));
889
    }
890

            
891
    #[test]
892
    fn contains_rect_does_not_panic_on_the_extremes_it_can_reach() {
893
        let full = rect(0, 0, isize::MAX, isize::MAX);
894
        assert!(full.contains_rect(&full)); // 0 + MAX <= 0 + MAX
895
        assert!(full.contains_rect(&rect(0, 0, 0, 0)));
896
        assert!(!full.contains_rect(&rect(-1, 0, 0, 0)));
897

            
898
        // The MIN-anchored half-space does not contain the origin rect: its far
899
        // edge is MIN + MAX = -1, which is < 0.
900
        let half = rect(isize::MIN, isize::MIN, isize::MAX, isize::MAX);
901
        assert!(!half.contains_rect(&rect(0, 0, 0, 0)));
902
        assert!(half.contains_rect(&rect(isize::MIN, isize::MIN, 0, 0)));
903
    }
904

            
905
    // `b_x + b_width` and `a_x + a_width` now saturate, so an overflowing far
906
    // edge no longer panics: b saturates to the same isize::MAX edge as a.
907
    #[test]
908
    fn contains_rect_does_not_panic_when_the_inner_rects_far_edge_overflows() {
909
        let a = core::hint::black_box(rect(0, 0, isize::MAX, isize::MAX));
910
        let b = core::hint::black_box(rect(1, 1, isize::MAX, 1));
911
        assert!(a.contains_rect(&b));
912
    }
913

            
914
    // =================================================== union ===============
915

            
916
    #[test]
917
    fn union_of_a_single_rect_is_that_rect_even_at_the_extremes() {
918
        for r in [
919
            rect(0, 0, 0, 0),
920
            rect(-7, -8, 1, 2),
921
            rect(3, 4, -5, -6), // negative size survives the max-minus-min round-trip
922
            rect(isize::MIN, isize::MIN, isize::MAX, isize::MAX),
923
            rect(isize::MAX, isize::MAX, 0, 0),
924
        ] {
925
            let vec = rect_vec(&[r]);
926
            assert_eq!(
927
                LayoutRect::union(vec.as_c_slice()),
928
                OptionLayoutRect::Some(r),
929
                "union([{r}]) is not the identity"
930
            );
931
        }
932
    }
933

            
934
    #[test]
935
    fn union_is_idempotent_and_order_independent_for_well_formed_rects() {
936
        let a = rect(-3, 15, 4, 4);
937
        let b = rect(20, -5, 5, 30);
938

            
939
        let ab = rect_vec(&[a, b]);
940
        let ba = rect_vec(&[b, a]);
941
        assert_eq!(
942
            LayoutRect::union(ab.as_c_slice()),
943
            LayoutRect::union(ba.as_c_slice())
944
        );
945

            
946
        let aa = rect_vec(&[a, a, a]);
947
        assert_eq!(
948
            LayoutRect::union(aa.as_c_slice()),
949
            OptionLayoutRect::Some(a)
950
        );
951
    }
952

            
953
    #[test]
954
    fn union_covers_every_input_rect() {
955
        let rects = [rect(0, 0, 10, 10), rect(20, -5, 5, 30), rect(-3, 15, 4, 4)];
956
        let vec = rect_vec(&rects);
957
        let OptionLayoutRect::Some(u) = LayoutRect::union(vec.as_c_slice()) else {
958
            panic!("expected Some for a non-empty slice");
959
        };
960
        for r in rects {
961
            assert!(u.contains_rect(&r), "{u} does not cover {r}");
962
        }
963
        // ...and it is tight: shrinking it by one on any side breaks the cover.
964
        let tight = rect(u.min_x() + 1, u.min_y(), u.width() - 1, u.height());
965
        assert!(rects.iter().any(|r| !tight.contains_rect(r)));
966
    }
967

            
968
    #[test]
969
    fn union_only_reads_the_slice_it_was_given() {
970
        let vec = rect_vec(&[
971
            rect(0, 0, 1, 1),
972
            rect(100, 100, 1, 1),
973
            rect(-100, -100, 1, 1),
974
        ]);
975
        // A sub-range must not pull in the neighbouring rects.
976
        assert_eq!(
977
            LayoutRect::union(vec.as_c_slice_range(0, 1)),
978
            OptionLayoutRect::Some(rect(0, 0, 1, 1))
979
        );
980
        assert_eq!(
981
            LayoutRect::union(vec.as_c_slice_range(0, 2)),
982
            OptionLayoutRect::Some(rect(0, 0, 101, 101))
983
        );
984
        // An empty sub-range is the empty case, not a wild pointer read.
985
        assert!(LayoutRect::union(vec.as_c_slice_range(1, 1)).is_none());
986
    }
987

            
988
    #[test]
989
    fn union_of_an_empty_and_a_default_constructed_vec_is_none() {
990
        let empty = LayoutRectVec::new();
991
        assert!(empty.is_empty());
992
        assert_eq!(
993
            LayoutRect::union(empty.as_c_slice()),
994
            OptionLayoutRect::None
995
        );
996
        assert!(LayoutRect::union(LayoutRectVecSlice::empty()).is_none());
997
        assert_eq!(OptionLayoutRect::default(), OptionLayoutRect::None);
998
    }
999

            
    #[test]
    fn union_with_negative_size_rects_folds_them_into_a_smaller_box() {
        // A negative-size rect's "max" is *left of* its origin, so union tracks
        // (5, 5) as the far corner and never covers the origin at (10, 10).
        let vec = rect_vec(&[rect(10, 10, -5, -5), rect(0, 0, 2, 2)]);
        assert_eq!(
            LayoutRect::union(vec.as_c_slice()),
            OptionLayoutRect::Some(rect(0, 0, 5, 5))
        );
    }
    #[test]
    fn union_handles_all_negative_coordinates() {
        let vec = rect_vec(&[rect(-10, -10, 2, 2), rect(-30, -5, 1, 1)]);
        assert_eq!(
            LayoutRect::union(vec.as_c_slice()),
            OptionLayoutRect::Some(rect(-30, -10, 22, 6))
        );
    }
    #[test]
    fn union_survives_the_widest_non_overflowing_pair() {
        let vec = rect_vec(&[rect(isize::MIN, isize::MIN, 0, 0), rect(-1, -1, 0, 0)]);
        assert_eq!(
            LayoutRect::union(vec.as_c_slice()),
            OptionLayoutRect::Some(rect(isize::MIN, isize::MIN, isize::MAX, isize::MAX))
        );
    }
    // `union` does three `isize` operations — `x + width` per rect and `max - min`
    // for the result extent — all saturating now, so a bounding box exceeding
    // isize::MAX clamps the extent instead of panicking (debug) / wrapping (release).
    #[test]
    fn union_spanning_the_whole_isize_range_saturates_the_extent() {
        let vec = rect_vec(&[rect(isize::MIN, 0, 0, 0), rect(isize::MAX, 0, 0, 0)]);
        assert_eq!(
            LayoutRect::union(vec.as_c_slice()),
            OptionLayoutRect::Some(rect(isize::MIN, 0, isize::MAX, 0))
        );
    }
    #[test]
    fn union_of_a_rect_whose_far_edge_overflows_saturates() {
        let vec = rect_vec(&[rect(isize::MAX, 0, 1, 0)]);
        assert_eq!(
            LayoutRect::union(vec.as_c_slice()),
            OptionLayoutRect::Some(rect(isize::MAX, 0, 0, 0))
        );
    }
    // =================================================== LayoutSize::round ===
    #[test]
    fn round_of_zero_and_negative_zero_is_the_zero_size() {
        assert_eq!(LayoutSize::round(0.0, 0.0), LayoutSize::zero());
        assert_eq!(LayoutSize::round(-0.0, -0.0), LayoutSize::zero());
        assert_eq!(LayoutSize::round(0.0, -0.0), LayoutSize::zero());
    }
    #[test]
    fn round_goes_half_away_from_zero_not_half_to_even() {
        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)));
    }
}