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        BoundingBox::from_min_max(left, top, right, bottom)
234    }
235
236    /// Returns a new [BoundingBox] representing the union of the current bounding box and the given bounding box.
237    pub fn union(&self, other: &Self) -> Self {
238        let left = self.left().min(other.left());
239        let right = self.right().max(other.right());
240        let top = self.top().min(other.top());
241        let bottom = self.bottom().max(other.bottom());
242        BoundingBox::from_min_max(left, top, right, bottom)
243    }
244
245    /// Returns true if the current bounding box and the given bounding box intersect.
246    pub fn intersects(&self, other: &Self) -> bool {
247        let x_hit = (self.x >= other.x && self.x < other.x + other.w)
248            || (other.x >= self.x && other.x < self.x + self.w);
249        let y_hit = (self.y >= other.y && self.y < other.y + other.h)
250            || (other.y >= self.y && other.y < self.y + self.h);
251        x_hit && y_hit
252    }
253
254    /// Returns true if the given bounding box is contained within the current bounding box.
255    pub fn contains(&self, other: &Self) -> bool {
256        let x_hit = other.x >= self.x && other.x + other.w < self.x + self.w;
257        let y_hit = other.y >= self.y && other.y + other.h < self.y + self.h;
258        x_hit && y_hit
259    }
260
261    /// Returns true if the given point is contained within the current bounding box.
262    pub fn contains_point(&self, x: f32, y: f32) -> bool {
263        let x_hit = x >= self.x && x < self.x + self.w;
264        let y_hit = y >= self.y && y < self.y + self.h;
265        x_hit && y_hit
266    }
267
268    /// Because the diagonal distance of the current bounding box.
269    pub fn diagonal(&self) -> f32 {
270        (self.width() * self.width() + self.height() * self.height()).sqrt()
271    }
272
273    // pub fn transform(&self, transform: &Transform2D) -> Self {
274    //     let (tl, tt) = transform.transform_point(self.x, self.y);
275    //     let (tr, tb) = transform.transform_point(self.right(), self.bottom());
276    //     BoundingBox::from_min_max(tl, tt, tr, tb)
277    // }
278}
279
280impl From<BoundingBox> for skia_safe::Rect {
281    fn from(bb: BoundingBox) -> Self {
282        skia_safe::Rect { left: bb.left(), top: bb.top(), right: bb.right(), bottom: bb.bottom() }
283    }
284}
285
286impl From<skia_safe::Rect> for BoundingBox {
287    fn from(bb: Rect) -> Self {
288        BoundingBox { x: bb.left(), y: bb.top(), w: bb.width(), h: bb.height() }
289    }
290}
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295    fn rect() -> BoundingBox {
296        BoundingBox { x: 100f32, y: 100f32, w: 100f32, h: 100f32 }
297    }
298
299    #[test]
300    fn get_center() {
301        let rect = rect();
302        assert_eq!(rect.center(), (150f32, 150f32));
303    }
304
305    #[test]
306    fn get_center_top() {
307        let rect = rect();
308        assert_eq!(rect.center_top(), (150f32, 100f32));
309    }
310
311    #[test]
312    fn get_center_bottom() {
313        let rect = rect();
314        assert_eq!(rect.center_bottom(), (150f32, 200f32));
315    }
316
317    #[test]
318    fn get_center_left() {
319        let rect = rect();
320        assert_eq!(rect.center_left(), (100f32, 150f32));
321    }
322
323    #[test]
324    fn get_center_right() {
325        let rect = rect();
326        assert_eq!(rect.center_right(), (200f32, 150f32));
327    }
328
329    #[test]
330    fn get_left() {
331        let rect = rect();
332        assert_eq!(rect.left(), 100f32);
333    }
334
335    #[test]
336    fn get_right() {
337        let rect = rect();
338        assert_eq!(rect.right(), 200f32);
339    }
340
341    #[test]
342    fn get_top() {
343        let rect = rect();
344        assert_eq!(rect.top(), 100f32);
345    }
346
347    #[test]
348    fn get_bottom() {
349        let rect = rect();
350        assert_eq!(rect.bottom(), 200f32);
351    }
352
353    #[test]
354    fn get_shrunken() {
355        let rect = rect();
356        let a = rect.shrink(25f32);
357        let b = BoundingBox { x: 125f32, y: 125f32, w: 50f32, h: 50f32 };
358        assert_eq!(a, b);
359    }
360
361    #[test]
362    fn get_shrunken_horizontal() {
363        let rect = rect();
364        let a = rect.shrink_horizontal(25f32);
365        let b = BoundingBox { x: 125f32, y: 100f32, w: 50f32, h: 100f32 };
366        assert_eq!(a, b);
367    }
368
369    #[test]
370    fn get_shrunken_vertical() {
371        let rect = rect();
372        let a = rect.shrink_vertical(25f32);
373        let b = BoundingBox { x: 100f32, y: 125f32, w: 100f32, h: 50f32 };
374        assert_eq!(a, b);
375    }
376
377    #[test]
378    fn get_expanded() {
379        let rect = rect();
380        let a = rect.expand(25f32);
381        let b = BoundingBox { x: 75f32, y: 75f32, w: 150f32, h: 150f32 };
382        assert_eq!(a, b);
383    }
384
385    #[test]
386    fn get_expanded_horizontal() {
387        let rect = rect();
388        let a = rect.expand_horizontal(25f32);
389        let b = BoundingBox { x: 75f32, y: 100f32, w: 150f32, h: 100f32 };
390        assert_eq!(a, b);
391    }
392
393    #[test]
394    fn get_expanded_vertical() {
395        let rect = rect();
396        let a = rect.expand_vertical(25f32);
397        let b = BoundingBox { x: 100f32, y: 75f32, w: 100f32, h: 150f32 };
398        assert_eq!(a, b);
399    }
400}