Skip to main content

vizia_core/layout/
bounds.rs

1use skia_safe::Rect;
2
3/// Represents an axis-aligned bounding box.
4#[derive(Clone, Copy, Debug, PartialEq)]
5pub struct BoundingBox {
6    /// The horizontal x position of the bounding box.
7    pub x: f32,
8    /// The vertical y position of the bounding box.
9    pub y: f32,
10    /// The width of the bounding box.
11    pub w: f32,
12    /// The height of the bounding box.
13    pub h: f32,
14}
15
16impl std::fmt::Display for BoundingBox {
17    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18        write!(f, "{{ x: {:?}, y: {:?}, w: {:?}, h:{:?} }}", self.x, self.y, self.w, self.h)
19    }
20}
21
22impl Default for BoundingBox {
23    fn default() -> Self {
24        Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0 }
25    }
26}
27
28impl BoundingBox {
29    /// Construct a [`BoundingBox`] from checked minimum and maximum values.
30    #[inline(always)]
31    pub fn from_min_max(min_x: f32, min_y: f32, max_x: f32, max_y: f32) -> BoundingBox {
32        BoundingBox { x: min_x, y: min_y, w: max_x - min_x, h: max_y - min_y }
33    }
34
35    /// Left side of bounds equivalent to `x`.
36    #[inline(always)]
37    pub fn left(&self) -> f32 {
38        self.x
39    }
40
41    /// Top of bounds equivalent to `y`.
42    #[inline(always)]
43    pub fn top(&self) -> f32 {
44        self.y
45    }
46
47    /// Bounds width equivalent to `w`.
48    #[inline(always)]
49    pub fn width(&self) -> f32 {
50        self.w
51    }
52
53    /// Bounds height equivalent to `h`.
54    #[inline(always)]
55    pub fn height(&self) -> f32 {
56        self.h
57    }
58
59    /// Right side of bounds.
60    #[inline(always)]
61    pub fn right(&self) -> f32 {
62        self.left() + self.width()
63    }
64
65    /// Bottom side of bounds.
66    #[inline(always)]
67    pub fn bottom(&self) -> f32 {
68        self.top() + self.height()
69    }
70
71    /// Horizontal and vertical center of bounds.
72    #[inline(always)]
73    pub fn center(&self) -> (f32, f32) {
74        ((self.width() / 2f32) + self.x, (self.height() / 2f32) + self.y)
75    }
76
77    /// Left center of bounds.
78    #[inline(always)]
79    pub fn center_left(&self) -> (f32, f32) {
80        (self.left(), (self.height() / 2f32) + self.top())
81    }
82
83    /// Right center of bounds.
84    #[inline(always)]
85    pub fn center_right(&self) -> (f32, f32) {
86        (self.right(), (self.height() / 2f32) + self.top())
87    }
88
89    /// Top center of bounds.
90    #[inline(always)]
91    pub fn center_top(&self) -> (f32, f32) {
92        ((self.width() / 2f32) + self.left(), self.top())
93    }
94
95    /// Bottom center of bounds.
96    #[inline(always)]
97    pub fn center_bottom(&self) -> (f32, f32) {
98        ((self.width() / 2f32) + self.left(), self.bottom())
99    }
100
101    /// Bottom left point of bounds.
102    #[inline(always)]
103    pub fn bottom_left(&self) -> (f32, f32) {
104        (self.left(), self.bottom())
105    }
106
107    /// Bottom right point of bounds.
108    #[inline(always)]
109    pub fn bottom_right(&self) -> (f32, f32) {
110        (self.right(), self.bottom())
111    }
112
113    /// Top left point of bounds.
114    #[inline(always)]
115    pub fn top_left(&self) -> (f32, f32) {
116        (self.left(), self.top())
117    }
118
119    /// Top right point of bounds.
120    #[inline(always)]
121    pub fn top_right(&self) -> (f32, f32) {
122        (self.right(), self.top())
123    }
124
125    /// Shrinks by some `amount` in both directions and returns a new [`BoundingBox`].
126    #[inline(always)]
127    #[must_use]
128    pub fn shrink(&self, amount: f32) -> BoundingBox {
129        BoundingBox::from_min_max(
130            self.left() + amount,
131            self.top() + amount,
132            self.right() - amount,
133            self.bottom() - amount,
134        )
135    }
136
137    /// Shrinks by some `amount` horizontally and returns a new [`BoundingBox`].
138    #[inline(always)]
139    #[must_use]
140    pub fn shrink_horizontal(&self, amount: f32) -> BoundingBox {
141        BoundingBox::from_min_max(
142            self.left() + amount,
143            self.top(),
144            self.right() - amount,
145            self.bottom(),
146        )
147    }
148
149    /// Shrinks by some `amount` vertically and returns a new [`BoundingBox`].
150    #[inline(always)]
151    #[must_use]
152    pub fn shrink_vertical(&self, amount: f32) -> BoundingBox {
153        BoundingBox::from_min_max(
154            self.left(),
155            self.top() + amount,
156            self.right(),
157            self.bottom() - amount,
158        )
159    }
160
161    /// Shrinks each side by the given separate amounts and returns a new [`BoundingBox`].
162    pub fn shrink_sides(&self, left: f32, top: f32, right: f32, bottom: f32) -> BoundingBox {
163        BoundingBox::from_min_max(
164            self.left() + left,
165            self.top() + top,
166            self.right() - right,
167            self.bottom() - bottom,
168        )
169    }
170
171    /// Expands each side by the given separate amounts and returns a new [`BoundingBox`].
172    pub fn expand_sides(&self, left: f32, top: f32, right: f32, bottom: f32) -> BoundingBox {
173        BoundingBox::from_min_max(
174            self.left() - left,
175            self.top() - top,
176            self.right() + right,
177            self.bottom() + bottom,
178        )
179    }
180
181    /// Shifts the bounding box by the given X and Y offsets and returns a new [`BoundingBox`].
182    pub fn offset(&self, x: f32, y: f32) -> BoundingBox {
183        BoundingBox::from_min_max(
184            self.left() + x,
185            self.top() + y,
186            self.right() + x,
187            self.bottom() + y,
188        )
189    }
190
191    /// Expands by some `amount` in both directions and returns a new [`BoundingBox`].
192    #[inline(always)]
193    #[must_use]
194    pub fn expand(&self, amount: f32) -> BoundingBox {
195        BoundingBox::from_min_max(
196            self.left() - amount,
197            self.top() - amount,
198            self.right() + amount,
199            self.bottom() + amount,
200        )
201    }
202
203    /// Expands by some `amount` horizontally and returns a new [`BoundingBox`].
204    #[inline(always)]
205    #[must_use]
206    pub fn expand_horizontal(&self, amount: f32) -> BoundingBox {
207        BoundingBox::from_min_max(
208            self.left() - amount,
209            self.top(),
210            self.right() + amount,
211            self.bottom(),
212        )
213    }
214
215    /// Expands by some `amount` vertically and returns a new [`BoundingBox`].
216    #[inline(always)]
217    #[must_use]
218    pub fn expand_vertical(&self, amount: f32) -> BoundingBox {
219        BoundingBox::from_min_max(
220            self.left(),
221            self.top() - amount,
222            self.right(),
223            self.bottom() + amount,
224        )
225    }
226
227    /// Returns a new [BoundingBox] representing the intersection of the current bounding box and the given bounding box.
228    pub fn intersection(&self, other: &Self) -> Self {
229        let left = self.left().max(other.left());
230        let right = self.right().min(other.right());
231        let top = self.top().max(other.top());
232        let bottom = self.bottom().min(other.bottom());
233        if right <= left || bottom <= top {
234            BoundingBox::default()
235        } else {
236            BoundingBox::from_min_max(left, top, right, bottom)
237        }
238    }
239
240    /// Returns a new [BoundingBox] representing the union of the current bounding box and the given bounding box.
241    pub fn union(&self, other: &Self) -> Self {
242        let left = self.left().min(other.left());
243        let right = self.right().max(other.right());
244        let top = self.top().min(other.top());
245        let bottom = self.bottom().max(other.bottom());
246        BoundingBox::from_min_max(left, top, right, bottom)
247    }
248
249    /// Returns true if the current bounding box and the given bounding box intersect.
250    pub fn intersects(&self, other: &Self) -> bool {
251        let x_hit = (self.x >= other.x && self.x < other.x + other.w)
252            || (other.x >= self.x && other.x < self.x + self.w);
253        let y_hit = (self.y >= other.y && self.y < other.y + other.h)
254            || (other.y >= self.y && other.y < self.y + self.h);
255        x_hit && y_hit
256    }
257
258    /// Returns true if the given bounding box is contained within the current bounding box.
259    pub fn contains(&self, other: &Self) -> bool {
260        let x_hit = other.x >= self.x && other.x + other.w < self.x + self.w;
261        let y_hit = other.y >= self.y && other.y + other.h < self.y + self.h;
262        x_hit && y_hit
263    }
264
265    /// Returns true if the given point is contained within the current bounding box.
266    pub fn contains_point(&self, x: f32, y: f32) -> bool {
267        let x_hit = x >= self.x && x < self.x + self.w;
268        let y_hit = y >= self.y && y < self.y + self.h;
269        x_hit && y_hit
270    }
271
272    /// Because the diagonal distance of the current bounding box.
273    pub fn diagonal(&self) -> f32 {
274        (self.width() * self.width() + self.height() * self.height()).sqrt()
275    }
276
277    // pub fn transform(&self, transform: &Transform2D) -> Self {
278    //     let (tl, tt) = transform.transform_point(self.x, self.y);
279    //     let (tr, tb) = transform.transform_point(self.right(), self.bottom());
280    //     BoundingBox::from_min_max(tl, tt, tr, tb)
281    // }
282}
283
284impl From<BoundingBox> for skia_safe::Rect {
285    fn from(bb: BoundingBox) -> Self {
286        skia_safe::Rect { left: bb.left(), top: bb.top(), right: bb.right(), bottom: bb.bottom() }
287    }
288}
289
290impl From<skia_safe::Rect> for BoundingBox {
291    fn from(bb: Rect) -> Self {
292        BoundingBox { x: bb.left(), y: bb.top(), w: bb.width(), h: bb.height() }
293    }
294}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299    fn rect() -> BoundingBox {
300        BoundingBox { x: 100f32, y: 100f32, w: 100f32, h: 100f32 }
301    }
302
303    #[test]
304    fn get_center() {
305        let rect = rect();
306        assert_eq!(rect.center(), (150f32, 150f32));
307    }
308
309    #[test]
310    fn get_center_top() {
311        let rect = rect();
312        assert_eq!(rect.center_top(), (150f32, 100f32));
313    }
314
315    #[test]
316    fn get_center_bottom() {
317        let rect = rect();
318        assert_eq!(rect.center_bottom(), (150f32, 200f32));
319    }
320
321    #[test]
322    fn get_center_left() {
323        let rect = rect();
324        assert_eq!(rect.center_left(), (100f32, 150f32));
325    }
326
327    #[test]
328    fn get_center_right() {
329        let rect = rect();
330        assert_eq!(rect.center_right(), (200f32, 150f32));
331    }
332
333    #[test]
334    fn get_left() {
335        let rect = rect();
336        assert_eq!(rect.left(), 100f32);
337    }
338
339    #[test]
340    fn get_right() {
341        let rect = rect();
342        assert_eq!(rect.right(), 200f32);
343    }
344
345    #[test]
346    fn get_top() {
347        let rect = rect();
348        assert_eq!(rect.top(), 100f32);
349    }
350
351    #[test]
352    fn get_bottom() {
353        let rect = rect();
354        assert_eq!(rect.bottom(), 200f32);
355    }
356
357    #[test]
358    fn get_shrunken() {
359        let rect = rect();
360        let a = rect.shrink(25f32);
361        let b = BoundingBox { x: 125f32, y: 125f32, w: 50f32, h: 50f32 };
362        assert_eq!(a, b);
363    }
364
365    #[test]
366    fn get_shrunken_horizontal() {
367        let rect = rect();
368        let a = rect.shrink_horizontal(25f32);
369        let b = BoundingBox { x: 125f32, y: 100f32, w: 50f32, h: 100f32 };
370        assert_eq!(a, b);
371    }
372
373    #[test]
374    fn get_shrunken_vertical() {
375        let rect = rect();
376        let a = rect.shrink_vertical(25f32);
377        let b = BoundingBox { x: 100f32, y: 125f32, w: 100f32, h: 50f32 };
378        assert_eq!(a, b);
379    }
380
381    #[test]
382    fn get_expanded() {
383        let rect = rect();
384        let a = rect.expand(25f32);
385        let b = BoundingBox { x: 75f32, y: 75f32, w: 150f32, h: 150f32 };
386        assert_eq!(a, b);
387    }
388
389    #[test]
390    fn get_expanded_horizontal() {
391        let rect = rect();
392        let a = rect.expand_horizontal(25f32);
393        let b = BoundingBox { x: 75f32, y: 100f32, w: 150f32, h: 100f32 };
394        assert_eq!(a, b);
395    }
396
397    #[test]
398    fn get_expanded_vertical() {
399        let rect = rect();
400        let a = rect.expand_vertical(25f32);
401        let b = BoundingBox { x: 100f32, y: 75f32, w: 100f32, h: 150f32 };
402        assert_eq!(a, b);
403    }
404}