Skip to main content

azul_css/props/basic/
geometry.rs

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
5use core::fmt;
6
7use 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)]
15pub struct LayoutPoint {
16    pub x: isize,
17    pub y: isize,
18}
19
20impl fmt::Debug for LayoutPoint {
21    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22        write!(f, "{self}")
23    }
24}
25impl fmt::Display for LayoutPoint {
26    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27        write!(f, "({}, {})", self.x, self.y)
28    }
29}
30
31impl LayoutPoint {
32    #[inline]
33    #[must_use]
34    pub const fn new(x: isize, y: isize) -> Self {
35        Self { x, y }
36    }
37    #[inline]
38    #[must_use]
39    pub const fn zero() -> Self {
40        Self::new(0, 0)
41    }
42}
43
44impl_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)]
53pub struct LayoutSize {
54    pub width: isize,
55    pub height: isize,
56}
57
58impl fmt::Debug for LayoutSize {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        write!(f, "{self}")
61    }
62}
63impl fmt::Display for LayoutSize {
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        write!(f, "{}x{}", self.width, self.height)
66    }
67}
68
69impl LayoutSize {
70    #[inline]
71    #[must_use]
72    pub const fn new(width: isize, height: isize) -> Self {
73        Self { width, height }
74    }
75    #[inline]
76    #[must_use]
77    pub const fn zero() -> Self {
78        Self::new(0, 0)
79    }
80    #[inline]
81    #[must_use]
82    pub fn round(width: f32, height: f32) -> Self {
83        Self {
84            width: crate::cast::f32_to_isize(libm::roundf(width)),
85            height: crate::cast::f32_to_isize(libm::roundf(height)),
86        }
87    }
88}
89
90impl_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)]
99pub struct LayoutRect {
100    pub origin: LayoutPoint,
101    pub size: LayoutSize,
102}
103
104impl_option!(
105    LayoutRect,
106    OptionLayoutRect,
107    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd]
108);
109impl_vec!(
110    LayoutRect,
111    LayoutRectVec,
112    LayoutRectVecDestructor,
113    LayoutRectVecDestructorType,
114    LayoutRectVecSlice,
115    OptionLayoutRect
116);
117impl_vec_clone!(LayoutRect, LayoutRectVec, LayoutRectVecDestructor);
118impl_vec_debug!(LayoutRect, LayoutRectVec);
119impl_vec_mut!(LayoutRect, LayoutRectVec);
120impl_vec_partialeq!(LayoutRect, LayoutRectVec);
121impl_vec_partialord!(LayoutRect, LayoutRectVec);
122
123impl fmt::Debug for LayoutRect {
124    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125        write!(f, "{self}")
126    }
127}
128impl fmt::Display for LayoutRect {
129    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130        write!(f, "{} @ {}", self.size, self.origin)
131    }
132}
133
134impl LayoutRect {
135    #[inline]
136    #[must_use]
137    pub const fn new(origin: LayoutPoint, size: LayoutSize) -> Self {
138        Self { origin, size }
139    }
140    #[inline]
141    #[must_use]
142    pub const fn zero() -> Self {
143        Self::new(LayoutPoint::zero(), LayoutSize::zero())
144    }
145    #[inline]
146    #[must_use]
147    pub const fn max_x(&self) -> isize {
148        self.origin.x.saturating_add(self.size.width)
149    }
150    #[inline]
151    #[must_use]
152    pub const fn min_x(&self) -> isize {
153        self.origin.x
154    }
155    #[inline]
156    #[must_use]
157    pub const fn max_y(&self) -> isize {
158        self.origin.y.saturating_add(self.size.height)
159    }
160    #[inline]
161    #[must_use]
162    pub const fn min_y(&self) -> isize {
163        self.origin.y
164    }
165    #[inline]
166    #[must_use]
167    pub const fn width(&self) -> isize {
168        self.size.width
169    }
170    #[inline]
171    #[must_use]
172    pub const fn height(&self) -> isize {
173        self.size.height
174    }
175
176    #[must_use]
177    pub const fn contains(&self, other: &LayoutPoint) -> bool {
178        self.min_x() <= other.x
179            && other.x < self.max_x()
180            && self.min_y() <= other.y
181            && other.y < self.max_y()
182    }
183
184    #[must_use]
185    pub fn contains_f32(&self, other_x: f32, other_y: f32) -> bool {
186        crate::cast::isize_to_f32(self.min_x()) <= other_x
187            && other_x < crate::cast::isize_to_f32(self.max_x())
188            && crate::cast::isize_to_f32(self.min_y()) <= other_y
189            && other_y < crate::cast::isize_to_f32(self.max_y())
190    }
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    pub const fn hit_test(&self, other: &LayoutPoint) -> Option<LayoutPoint> {
198        let dx_left_edge = other.x.saturating_sub(self.min_x());
199        let dx_right_edge = self.max_x().saturating_sub(other.x);
200        let dy_top_edge = other.y.saturating_sub(self.min_y());
201        let dy_bottom_edge = self.max_y().saturating_sub(other.y);
202        if dx_left_edge > 0 && dx_right_edge > 0 && dy_top_edge > 0 && dy_bottom_edge > 0 {
203            Some(LayoutPoint::new(dx_left_edge, dy_top_edge))
204        } else {
205            None
206        }
207    }
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    pub fn union(rects: LayoutRectVecSlice) -> OptionLayoutRect {
214        let mut iter = rects.as_slice().iter().copied();
215        let Some(first) = iter.next() else {
216            return OptionLayoutRect::None;
217        };
218
219        let mut min_x = first.origin.x;
220        let mut min_y = first.origin.y;
221        let mut max_x = first.origin.x.saturating_add(first.size.width);
222        let mut max_y = first.origin.y.saturating_add(first.size.height);
223
224        for Self {
225            origin: LayoutPoint { x, y },
226            size: LayoutSize { width, height },
227        } in iter
228        {
229            max_x = max_x.max(x.saturating_add(width));
230            max_y = max_y.max(y.saturating_add(height));
231            min_x = min_x.min(x);
232            min_y = min_y.min(y);
233        }
234
235        OptionLayoutRect::Some(Self {
236            origin: LayoutPoint { x: min_x, y: min_y },
237            size: LayoutSize {
238                width: max_x.saturating_sub(min_x),
239                height: max_y.saturating_sub(min_y),
240            },
241        })
242    }
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    pub const fn contains_rect(&self, b: &Self) -> bool {
252        let a = self;
253
254        let a_x = a.origin.x;
255        let a_y = a.origin.y;
256        let a_width = a.size.width;
257        let a_height = a.size.height;
258
259        let b_x = b.origin.x;
260        let b_y = b.origin.y;
261        let b_width = b.size.width;
262        let b_height = b.size.height;
263
264        b_x >= a_x
265            && b_y >= a_y
266            && b_x.saturating_add(b_width) <= a_x.saturating_add(a_width)
267            && b_y.saturating_add(b_height) <= a_y.saturating_add(a_height)
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274
275    fn rect(x: isize, y: isize, w: isize, h: isize) -> LayoutRect {
276        LayoutRect::new(LayoutPoint::new(x, y), LayoutSize::new(w, h))
277    }
278
279    #[test]
280    fn union_slice_returns_bounding_rect() {
281        let vec: LayoutRectVec =
282            alloc::vec![rect(0, 0, 10, 10), rect(20, -5, 5, 30), rect(-3, 15, 4, 4)].into();
283        let slice = vec.as_c_slice();
284
285        match LayoutRect::union(slice) {
286            OptionLayoutRect::Some(r) => {
287                assert_eq!(r, rect(-3, -5, 28, 30));
288            }
289            OptionLayoutRect::None => panic!("expected Some bounding rect"),
290        }
291    }
292
293    #[test]
294    fn union_empty_slice_returns_none() {
295        let vec: LayoutRectVec = LayoutRectVec::new();
296        let slice = vec.as_c_slice();
297        assert!(matches!(LayoutRect::union(slice), OptionLayoutRect::None));
298    }
299}
300
301#[cfg(test)]
302#[allow(
303    clippy::float_cmp,
304    clippy::unreadable_literal,
305    clippy::cognitive_complexity
306)]
307mod 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
1000    #[test]
1001    fn union_with_negative_size_rects_folds_them_into_a_smaller_box() {
1002        // A negative-size rect's "max" is *left of* its origin, so union tracks
1003        // (5, 5) as the far corner and never covers the origin at (10, 10).
1004        let vec = rect_vec(&[rect(10, 10, -5, -5), rect(0, 0, 2, 2)]);
1005        assert_eq!(
1006            LayoutRect::union(vec.as_c_slice()),
1007            OptionLayoutRect::Some(rect(0, 0, 5, 5))
1008        );
1009    }
1010
1011    #[test]
1012    fn union_handles_all_negative_coordinates() {
1013        let vec = rect_vec(&[rect(-10, -10, 2, 2), rect(-30, -5, 1, 1)]);
1014        assert_eq!(
1015            LayoutRect::union(vec.as_c_slice()),
1016            OptionLayoutRect::Some(rect(-30, -10, 22, 6))
1017        );
1018    }
1019
1020    #[test]
1021    fn union_survives_the_widest_non_overflowing_pair() {
1022        let vec = rect_vec(&[rect(isize::MIN, isize::MIN, 0, 0), rect(-1, -1, 0, 0)]);
1023        assert_eq!(
1024            LayoutRect::union(vec.as_c_slice()),
1025            OptionLayoutRect::Some(rect(isize::MIN, isize::MIN, isize::MAX, isize::MAX))
1026        );
1027    }
1028
1029    // `union` does three `isize` operations — `x + width` per rect and `max - min`
1030    // for the result extent — all saturating now, so a bounding box exceeding
1031    // isize::MAX clamps the extent instead of panicking (debug) / wrapping (release).
1032    #[test]
1033    fn union_spanning_the_whole_isize_range_saturates_the_extent() {
1034        let vec = rect_vec(&[rect(isize::MIN, 0, 0, 0), rect(isize::MAX, 0, 0, 0)]);
1035        assert_eq!(
1036            LayoutRect::union(vec.as_c_slice()),
1037            OptionLayoutRect::Some(rect(isize::MIN, 0, isize::MAX, 0))
1038        );
1039    }
1040
1041    #[test]
1042    fn union_of_a_rect_whose_far_edge_overflows_saturates() {
1043        let vec = rect_vec(&[rect(isize::MAX, 0, 1, 0)]);
1044        assert_eq!(
1045            LayoutRect::union(vec.as_c_slice()),
1046            OptionLayoutRect::Some(rect(isize::MAX, 0, 0, 0))
1047        );
1048    }
1049
1050    // =================================================== LayoutSize::round ===
1051
1052    #[test]
1053    fn round_of_zero_and_negative_zero_is_the_zero_size() {
1054        assert_eq!(LayoutSize::round(0.0, 0.0), LayoutSize::zero());
1055        assert_eq!(LayoutSize::round(-0.0, -0.0), LayoutSize::zero());
1056        assert_eq!(LayoutSize::round(0.0, -0.0), LayoutSize::zero());
1057    }
1058
1059    #[test]
1060    fn round_goes_half_away_from_zero_not_half_to_even() {
1061        assert_eq!(LayoutSize::round(0.5, -0.5), size(1, -1));
1062        assert_eq!(LayoutSize::round(1.5, -1.5), size(2, -2));
1063        // 2.5 -> 3 (away from zero), NOT 2 (banker's rounding).
1064        assert_eq!(LayoutSize::round(2.5, -2.5), size(3, -3));
1065        assert_eq!(LayoutSize::round(3.5, -3.5), size(4, -4));
1066    }
1067
1068    #[test]
1069    fn round_truncates_toward_zero_just_below_the_half() {
1070        // Largest f32 strictly below 0.5; must round to 0, not 1.
1071        let just_below_half = f32::from_bits(0x3eff_ffff);
1072        assert!(just_below_half < 0.5);
1073        assert_eq!(
1074            LayoutSize::round(just_below_half, -just_below_half),
1075            LayoutSize::zero()
1076        );
1077        assert_eq!(LayoutSize::round(0.49, -0.49), LayoutSize::zero());
1078        assert_eq!(LayoutSize::round(1.49, -1.49), size(1, -1));
1079    }
1080
1081    #[test]
1082    fn round_of_nan_is_zero_and_does_not_panic() {
1083        assert_eq!(LayoutSize::round(f32::NAN, f32::NAN), LayoutSize::zero());
1084        assert_eq!(LayoutSize::round(f32::NAN, 5.0), size(0, 5));
1085        assert_eq!(LayoutSize::round(5.0, -f32::NAN), size(5, 0));
1086        assert_eq!(
1087            LayoutSize::round(f32::from_bits(0x7fc0_1234), 1.0),
1088            size(0, 1)
1089        );
1090    }
1091
1092    #[test]
1093    fn round_saturates_the_infinities_to_the_isize_bounds() {
1094        assert_eq!(
1095            LayoutSize::round(f32::INFINITY, f32::NEG_INFINITY),
1096            size(isize::MAX, isize::MIN)
1097        );
1098        assert_eq!(
1099            LayoutSize::round(f32::NEG_INFINITY, f32::INFINITY),
1100            size(isize::MIN, isize::MAX)
1101        );
1102    }
1103
1104    #[test]
1105    fn round_saturates_out_of_range_finite_floats_rather_than_wrapping() {
1106        assert_eq!(
1107            LayoutSize::round(f32::MAX, f32::MIN),
1108            size(isize::MAX, isize::MIN)
1109        );
1110        assert_eq!(
1111            LayoutSize::round(1.0e30, -1.0e30),
1112            size(isize::MAX, isize::MIN)
1113        );
1114    }
1115
1116    #[test]
1117    fn round_flushes_subnormals_and_tiny_magnitudes_to_zero() {
1118        assert_eq!(
1119            LayoutSize::round(f32::MIN_POSITIVE, -f32::MIN_POSITIVE),
1120            LayoutSize::zero()
1121        );
1122        assert_eq!(
1123            LayoutSize::round(f32::EPSILON, f32::from_bits(1)),
1124            LayoutSize::zero()
1125        );
1126    }
1127
1128    #[test]
1129    fn round_is_exact_for_values_inside_the_f32_integer_range() {
1130        assert_eq!(
1131            LayoutSize::round(1.0e9, -1.0e9),
1132            size(1_000_000_000, -1_000_000_000)
1133        );
1134        assert_eq!(
1135            LayoutSize::round(16_777_216.0, -16_777_216.0),
1136            size(1 << 24, -(1 << 24))
1137        );
1138        assert_eq!(LayoutSize::round(-1.0, 1.0), size(-1, 1));
1139    }
1140
1141    #[test]
1142    fn round_agrees_with_roundf_then_cast_across_a_wide_sample() {
1143        let samples = [
1144            0.0,
1145            -0.0,
1146            0.5,
1147            -0.5,
1148            2.5,
1149            -2.5,
1150            1.4999999,
1151            -1.4999999,
1152            42.7,
1153            -42.7,
1154            16_777_215.5,
1155            -16_777_215.5,
1156            1.0e18,
1157            -1.0e18,
1158            f32::MAX,
1159            f32::MIN,
1160            f32::INFINITY,
1161            f32::NEG_INFINITY,
1162            f32::NAN,
1163            f32::MIN_POSITIVE,
1164        ];
1165        for w in samples {
1166            for h in samples {
1167                let got = LayoutSize::round(w, h);
1168                assert_eq!(got.width, f32_to_isize(libm::roundf(w)));
1169                assert_eq!(got.height, f32_to_isize(libm::roundf(h)));
1170            }
1171        }
1172    }
1173
1174    #[test]
1175    fn round_round_trips_through_f32_for_layout_sized_values() {
1176        // Everything a real layout produces is well below 2^24, so round() must be
1177        // an exact inverse of the isize->f32 cast there.
1178        let mut v: isize = -4_000_000;
1179        while v <= 4_000_000 {
1180            assert_eq!(
1181                LayoutSize::round(isize_to_f32(v), isize_to_f32(-v)),
1182                size(v, -v),
1183                "round-trip broke at {v}"
1184            );
1185            v += 40_009; // prime-ish stride, hits odd and even alike
1186        }
1187    }
1188
1189    // =================================================== derived traits ======
1190
1191    #[test]
1192    fn point_and_size_ordering_is_lexicographic_on_their_fields() {
1193        assert!(point(0, 1) < point(1, 0));
1194        assert!(point(1, 1) < point(1, 2));
1195        assert_eq!(point(1, 2).cmp(&point(1, 2)), core::cmp::Ordering::Equal);
1196        assert!(point(isize::MIN, isize::MAX) < point(isize::MAX, isize::MIN));
1197
1198        assert!(size(0, 1) < size(1, 0));
1199        assert!(size(-1, 0) < size(0, -1));
1200
1201        // LayoutRect only derives PartialOrd: origin first, then size.
1202        assert!(rect(0, 0, 1, 1) < rect(0, 0, 1, 2));
1203        assert!(rect(0, 0, 9, 9) < rect(0, 1, 0, 0));
1204    }
1205
1206    #[test]
1207    fn hash_agrees_with_eq_for_points_and_sizes() {
1208        assert_eq!(hash_of(&point(3, -4)), hash_of(&point(3, -4)));
1209        assert_eq!(hash_of(&size(3, -4)), hash_of(&size(3, -4)));
1210        // (x, y) and (y, x) must not collide — a field-order bug would show here.
1211        assert_ne!(hash_of(&point(3, -4)), hash_of(&point(-4, 3)));
1212        assert_ne!(hash_of(&point(0, 0)), hash_of(&point(0, 1)));
1213        assert_eq!(
1214            hash_of(&LayoutPoint::zero()),
1215            hash_of(&LayoutPoint::default())
1216        );
1217    }
1218
1219    #[test]
1220    fn option_wrappers_default_to_none_and_round_trip_through_core_option() {
1221        assert!(OptionLayoutPoint::default().is_none());
1222        assert!(OptionLayoutSize::default().is_none());
1223        assert!(OptionLayoutRect::default().is_none());
1224
1225        let r = rect(1, 2, 3, 4);
1226        let o: OptionLayoutRect = Some(r).into();
1227        assert!(o.is_some());
1228        assert_eq!(o.into_option(), Some(r));
1229        assert_eq!(Option::<LayoutRect>::from(OptionLayoutRect::None), None);
1230
1231        let p: OptionLayoutPoint = Some(point(-1, -2)).into();
1232        assert_eq!(p.as_ref(), Some(&point(-1, -2)));
1233    }
1234}