Skip to main content

valo_geometry/
rect.rs

1use crate::{Point, Size};
2
3/// Axis-aligned rectangle, `(x, y)` = top-left, y-down.
4#[derive(Clone, Copy, Debug, Default, PartialEq)]
5#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
6pub struct Rect {
7    pub x: f32,
8    pub y: f32,
9    pub width: f32,
10    pub height: f32,
11}
12
13impl Rect {
14    pub const fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
15        Self {
16            x,
17            y,
18            width,
19            height,
20        }
21    }
22
23    pub fn from_ltrb(l: f32, t: f32, r: f32, b: f32) -> Self {
24        Self {
25            x: l,
26            y: t,
27            width: r - l,
28            height: b - t,
29        }
30    }
31
32    pub fn from_origin_size(origin: Point, size: Size) -> Self {
33        Self {
34            x: origin.x,
35            y: origin.y,
36            width: size.width,
37            height: size.height,
38        }
39    }
40
41    pub fn right(&self) -> f32 {
42        self.x + self.width
43    }
44
45    pub fn bottom(&self) -> f32 {
46        self.y + self.height
47    }
48
49    pub fn origin(&self) -> Point {
50        Point::new(self.x, self.y)
51    }
52
53    pub fn size(&self) -> Size {
54        Size::new(self.width, self.height)
55    }
56
57    pub fn is_empty(&self) -> bool {
58        self.width <= 0.0 || self.height <= 0.0
59    }
60
61    /// Smallest rect containing both (empty rects are identity).
62    pub fn union(&self, other: &Rect) -> Rect {
63        if self.is_empty() {
64            return *other;
65        }
66        if other.is_empty() {
67            return *self;
68        }
69        Rect::from_ltrb(
70            self.x.min(other.x),
71            self.y.min(other.y),
72            self.right().max(other.right()),
73            self.bottom().max(other.bottom()),
74        )
75    }
76
77    /// `None` when disjoint (or either is empty).
78    pub fn intersect(&self, other: &Rect) -> Option<Rect> {
79        let r = Rect::from_ltrb(
80            self.x.max(other.x),
81            self.y.max(other.y),
82            self.right().min(other.right()),
83            self.bottom().min(other.bottom()),
84        );
85        (!r.is_empty()).then_some(r)
86    }
87
88    pub fn intersects(&self, other: &Rect) -> bool {
89        !self.is_empty()
90            && !other.is_empty()
91            && self.x < other.right()
92            && other.x < self.right()
93            && self.y < other.bottom()
94            && other.y < self.bottom()
95    }
96
97    pub fn contains(&self, p: Point) -> bool {
98        p.x >= self.x && p.x < self.right() && p.y >= self.y && p.y < self.bottom()
99    }
100
101    /// Like [`Self::contains`] but with the far edges included — Skia's
102    /// `contains_inclusive`. Hit-testing wants this: a rect path's bounds ARE
103    /// its outline, and a point on the outline counts as inside.
104    pub fn contains_inclusive(&self, p: Point) -> bool {
105        p.x >= self.x && p.x <= self.right() && p.y >= self.y && p.y <= self.bottom()
106    }
107
108    pub fn expand(&self, d: f32) -> Rect {
109        Rect::new(
110            self.x - d,
111            self.y - d,
112            self.width + 2.0 * d,
113            self.height + 2.0 * d,
114        )
115    }
116
117    /// The conservative "cannot bound this" rect: content whose transform
118    /// reaches the eye plane maps here — culling never rejects it, layers
119    /// clamp to their clip instead.
120    pub const EVERYTHING: Rect = Rect {
121        x: -1.0e9,
122        y: -1.0e9,
123        width: 2.0e9,
124        height: 2.0e9,
125    };
126
127    pub fn corners(&self) -> [Point; 4] {
128        [
129            Point::new(self.x, self.y),
130            Point::new(self.right(), self.y),
131            Point::new(self.right(), self.bottom()),
132            Point::new(self.x, self.bottom()),
133        ]
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn union_ignores_empty() {
143        let a = Rect::new(0.0, 0.0, 10.0, 10.0);
144        assert_eq!(Rect::default().union(&a), a);
145        assert_eq!(a.union(&Rect::default()), a);
146    }
147
148    #[test]
149    fn intersect_disjoint_is_none() {
150        let a = Rect::new(0.0, 0.0, 10.0, 10.0);
151        let b = Rect::new(20.0, 0.0, 10.0, 10.0);
152        assert_eq!(a.intersect(&b), None);
153        assert!(!a.intersects(&b));
154    }
155
156    #[test]
157    fn intersect_overlap() {
158        let a = Rect::new(0.0, 0.0, 10.0, 10.0);
159        let b = Rect::new(5.0, 5.0, 10.0, 10.0);
160        assert_eq!(a.intersect(&b), Some(Rect::new(5.0, 5.0, 5.0, 5.0)));
161    }
162}