Skip to main content

ferroui_base/
rect.rs

1//! Defines a rectangle.
2
3use std::fmt;
4use std::ops::{Div, DivAssign, Mul, MulAssign};
5use std::str::FromStr;
6
7use crate::utilities::math_utilities;
8use crate::utilities::span_helpers::InvariantF64;
9use crate::utilities::{FormatError, SpanStringTokenizer};
10use crate::{Matrix, Point, Size, Thickness, Vector};
11
12/// Defines a rectangle.
13#[derive(Clone, Copy, Debug, PartialEq, Default)]
14pub struct Rect {
15    /// The X position.
16    pub x: f64,
17    /// The Y position.
18    pub y: f64,
19    /// The width.
20    pub width: f64,
21    /// The height.
22    pub height: f64,
23}
24
25impl Rect {
26    /// Initializes a new instance of the [`Rect`] structure.
27    #[inline]
28    pub const fn new(x: f64, y: f64, width: f64, height: f64) -> Self {
29        Self {
30            x,
31            y,
32            width,
33            height,
34        }
35    }
36
37    /// Initializes a [`Rect`] at the origin with the given size.
38    #[inline]
39    pub const fn from_size(size: Size) -> Self {
40        Self::new(0.0, 0.0, size.width, size.height)
41    }
42
43    /// Initializes a [`Rect`] from its position and size.
44    #[inline]
45    pub const fn from_position_size(position: Point, size: Size) -> Self {
46        Self::new(position.x, position.y, size.width, size.height)
47    }
48
49    /// Initializes a [`Rect`] from its top left and bottom right corners.
50    #[inline]
51    pub fn from_points(top_left: Point, bottom_right: Point) -> Self {
52        Self::new(
53            top_left.x,
54            top_left.y,
55            bottom_right.x - top_left.x,
56            bottom_right.y - top_left.y,
57        )
58    }
59
60    /// Gets the position of the rectangle.
61    #[inline]
62    pub const fn position(&self) -> Point {
63        Point::new(self.x, self.y)
64    }
65
66    /// Gets the size of the rectangle.
67    #[inline]
68    pub const fn size(&self) -> Size {
69        Size::new(self.width, self.height)
70    }
71
72    /// Gets the right position of the rectangle.
73    #[inline]
74    pub fn right(&self) -> f64 {
75        self.x + self.width
76    }
77
78    /// Gets the bottom position of the rectangle.
79    #[inline]
80    pub fn bottom(&self) -> f64 {
81        self.y + self.height
82    }
83
84    /// Gets the left position.
85    #[inline]
86    pub const fn left(&self) -> f64 {
87        self.x
88    }
89
90    /// Gets the top position.
91    #[inline]
92    pub const fn top(&self) -> f64 {
93        self.y
94    }
95
96    /// Gets the top left point of the rectangle.
97    #[inline]
98    pub const fn top_left(&self) -> Point {
99        Point::new(self.x, self.y)
100    }
101
102    /// Gets the top right point of the rectangle.
103    #[inline]
104    pub fn top_right(&self) -> Point {
105        Point::new(self.right(), self.y)
106    }
107
108    /// Gets the bottom left point of the rectangle.
109    #[inline]
110    pub fn bottom_left(&self) -> Point {
111        Point::new(self.x, self.bottom())
112    }
113
114    /// Gets the bottom right point of the rectangle.
115    #[inline]
116    pub fn bottom_right(&self) -> Point {
117        Point::new(self.right(), self.bottom())
118    }
119
120    /// Gets the center point of the rectangle.
121    #[inline]
122    pub fn center(&self) -> Point {
123        Point::new(self.x + (self.width / 2.0), self.y + (self.height / 2.0))
124    }
125
126    /// Determines whether a point is in the bounds of the rectangle (edges inclusive).
127    #[inline]
128    pub fn contains(&self, p: Point) -> bool {
129        p.x >= self.x && p.x <= self.x + self.width && p.y >= self.y && p.y <= self.y + self.height
130    }
131
132    /// Determines whether a point is in the bounds of the rectangle, exclusive of
133    /// the rectangle's bottom/right edge.
134    #[inline]
135    pub fn contains_exclusive(&self, p: Point) -> bool {
136        p.x >= self.x && p.x < self.x + self.width && p.y >= self.y && p.y < self.y + self.height
137    }
138
139    /// Determines whether the rectangle fully contains another rectangle.
140    #[inline]
141    pub fn contains_rect(&self, r: Rect) -> bool {
142        self.contains(r.top_left()) && self.contains(r.bottom_right())
143    }
144
145    /// Centers another rectangle in this rectangle.
146    #[inline]
147    pub fn center_rect(&self, rect: Rect) -> Rect {
148        Rect::new(
149            self.x + ((self.width - rect.width) / 2.0),
150            self.y + ((self.height - rect.height) / 2.0),
151            rect.width,
152            rect.height,
153        )
154    }
155
156    /// Inflates the rectangle by the same amount on every side.
157    #[inline]
158    pub fn inflate(&self, thickness: f64) -> Rect {
159        self.inflate_thickness(Thickness::uniform(thickness))
160    }
161
162    /// Inflates the rectangle.
163    #[inline]
164    pub fn inflate_thickness(&self, thickness: Thickness) -> Rect {
165        Rect::from_position_size(
166            Point::new(self.x - thickness.left, self.y - thickness.top),
167            self.size().inflate(thickness),
168        )
169    }
170
171    /// Deflates the rectangle by the same amount on every side.
172    #[inline]
173    pub fn deflate(&self, thickness: f64) -> Rect {
174        self.deflate_thickness(Thickness::uniform(thickness))
175    }
176
177    /// Deflates the rectangle by a [`Thickness`]. The deflated rectangle size
178    /// cannot be less than 0.
179    #[inline]
180    pub fn deflate_thickness(&self, thickness: Thickness) -> Rect {
181        Rect::from_position_size(
182            Point::new(self.x + thickness.left, self.y + thickness.top),
183            self.size().deflate(thickness),
184        )
185    }
186
187    /// Returns a boolean indicating whether the rect is equal to the other given
188    /// rect (exact comparison).
189    #[inline]
190    pub fn equals(&self, other: Rect) -> bool {
191        self.x == other.x
192            && self.y == other.y
193            && self.width == other.width
194            && self.height == other.height
195    }
196
197    /// Gets the intersection of two rectangles, or an empty (default) rectangle
198    /// if they do not overlap.
199    pub fn intersect(&self, rect: Rect) -> Rect {
200        let new_left = if rect.x > self.x { rect.x } else { self.x };
201        let new_top = if rect.y > self.y { rect.y } else { self.y };
202        let new_right = if rect.right() < self.right() {
203            rect.right()
204        } else {
205            self.right()
206        };
207        let new_bottom = if rect.bottom() < self.bottom() {
208            rect.bottom()
209        } else {
210            self.bottom()
211        };
212
213        if (new_right > new_left) && (new_bottom > new_top) {
214            Rect::new(
215                new_left,
216                new_top,
217                new_right - new_left,
218                new_bottom - new_top,
219            )
220        } else {
221            Rect::default()
222        }
223    }
224
225    /// Determines whether a rectangle intersects with this rectangle.
226    #[inline]
227    pub fn intersects(&self, rect: Rect) -> bool {
228        (rect.x < self.right())
229            && (self.x < rect.right())
230            && (rect.y < self.bottom())
231            && (self.y < rect.bottom())
232    }
233
234    /// Returns the axis-aligned bounding box of a transformed rectangle.
235    pub fn transform_to_aabb(&self, matrix: Matrix) -> Rect {
236        let points = [
237            self.top_left().transform(matrix),
238            self.top_right().transform(matrix),
239            self.bottom_right().transform(matrix),
240            self.bottom_left().transform(matrix),
241        ];
242
243        let mut left = f64::MAX;
244        let mut right = f64::MIN;
245        let mut top = f64::MAX;
246        let mut bottom = f64::MIN;
247
248        for p in points {
249            if p.x < left {
250                left = p.x;
251            }
252            if p.x > right {
253                right = p.x;
254            }
255            if p.y < top {
256                top = p.y;
257            }
258            if p.y > bottom {
259                bottom = p.y;
260            }
261        }
262
263        Rect::from_points(Point::new(left, top), Point::new(right, bottom))
264    }
265
266    /// Translates the rectangle by an offset.
267    #[inline]
268    pub fn translate(&self, offset: Vector) -> Rect {
269        Rect::from_position_size(self.position() + offset, self.size())
270    }
271
272    /// Normalizes the rectangle so both the width and height are positive, without
273    /// changing its location: positive-size rects are returned unchanged, negative
274    /// sizes are flipped. A rect with any NaN edge becomes the default rect.
275    pub fn normalize(&self) -> Rect {
276        let mut rect = *self;
277
278        if rect.right().is_nan()
279            || rect.bottom().is_nan()
280            || rect.x.is_nan()
281            || rect.y.is_nan()
282            || self.height.is_nan()
283            || self.width.is_nan()
284        {
285            return Rect::default();
286        }
287
288        if rect.width < 0.0 {
289            let x = self.x + self.width;
290            let width = self.x - x;
291
292            rect = rect.with_x(x).with_width(width);
293        }
294
295        if rect.height < 0.0 {
296            let y = self.y + self.height;
297            let height = self.y - y;
298
299            rect = rect.with_y(y).with_height(height);
300        }
301
302        rect
303    }
304
305    /// Gets the union of two rectangles. A rectangle whose width and height are
306    /// both zero is ignored.
307    pub fn union(&self, rect: Rect) -> Rect {
308        if self.width == 0.0 && self.height == 0.0 {
309            rect
310        } else if rect.width == 0.0 && rect.height == 0.0 {
311            *self
312        } else {
313            let x1 = math_utilities::min(self.x, rect.x);
314            let x2 = math_utilities::max(self.right(), rect.right());
315            let y1 = math_utilities::min(self.y, rect.y);
316            let y2 = math_utilities::max(self.bottom(), rect.bottom());
317
318            Rect::from_points(Point::new(x1, y1), Point::new(x2, y2))
319        }
320    }
321
322    /// Union of two optional rectangles; a missing side yields the other side.
323    #[inline]
324    pub fn union_optional(left: Option<Rect>, right: Option<Rect>) -> Option<Rect> {
325        match (left, right) {
326            (None, right) => right,
327            (left, None) => left,
328            (Some(left), Some(right)) => Some(left.union(right)),
329        }
330    }
331
332    /// Returns a new [`Rect`] with the specified X position.
333    #[inline]
334    pub const fn with_x(&self, x: f64) -> Rect {
335        Rect::new(x, self.y, self.width, self.height)
336    }
337
338    /// Returns a new [`Rect`] with the specified Y position.
339    #[inline]
340    pub const fn with_y(&self, y: f64) -> Rect {
341        Rect::new(self.x, y, self.width, self.height)
342    }
343
344    /// Returns a new [`Rect`] with the specified width.
345    #[inline]
346    pub const fn with_width(&self, width: f64) -> Rect {
347        Rect::new(self.x, self.y, width, self.height)
348    }
349
350    /// Returns a new [`Rect`] with the specified height.
351    #[inline]
352    pub const fn with_height(&self, height: f64) -> Rect {
353        Rect::new(self.x, self.y, self.width, height)
354    }
355
356    /// Parses a [`Rect`] string (`"x, y, width, height"`).
357    pub fn parse(s: &str) -> Result<Self, FormatError> {
358        SpanStringTokenizer::with_message(s, "Invalid Rect.").scope(|t| {
359            Ok(Rect::new(
360                t.read_double()?,
361                t.read_double()?,
362                t.read_double()?,
363                t.read_double()?,
364            ))
365        })
366    }
367
368    /// This method should be used internally to check for the rect emptiness.
369    /// Once a dedicated "empty" value exists this is the single place to update.
370    #[inline]
371    pub fn is_empty(&self) -> bool {
372        *self == Rect::default()
373    }
374}
375
376/// Multiplies a rectangle by a scaling vector.
377impl Mul<Vector> for Rect {
378    type Output = Rect;
379    #[inline]
380    fn mul(self, scale: Vector) -> Rect {
381        Rect::new(
382            self.x * scale.x,
383            self.y * scale.y,
384            self.width * scale.x,
385            self.height * scale.y,
386        )
387    }
388}
389
390/// Multiplies a rectangle by a scale.
391impl Mul<f64> for Rect {
392    type Output = Rect;
393    #[inline]
394    fn mul(self, scale: f64) -> Rect {
395        Rect::new(
396            self.x * scale,
397            self.y * scale,
398            self.width * scale,
399            self.height * scale,
400        )
401    }
402}
403
404/// Divides a rectangle by a vector.
405impl Div<Vector> for Rect {
406    type Output = Rect;
407    #[inline]
408    fn div(self, scale: Vector) -> Rect {
409        Rect::new(
410            self.x / scale.x,
411            self.y / scale.y,
412            self.width / scale.x,
413            self.height / scale.y,
414        )
415    }
416}
417
418impl MulAssign<Vector> for Rect {
419    #[inline]
420    fn mul_assign(&mut self, scale: Vector) {
421        *self = *self * scale;
422    }
423}
424
425impl MulAssign<f64> for Rect {
426    #[inline]
427    fn mul_assign(&mut self, scale: f64) {
428        *self = *self * scale;
429    }
430}
431
432impl DivAssign<Vector> for Rect {
433    #[inline]
434    fn div_assign(&mut self, scale: Vector) {
435        *self = *self / scale;
436    }
437}
438
439impl FromStr for Rect {
440    type Err = FormatError;
441    #[inline]
442    fn from_str(s: &str) -> Result<Self, FormatError> {
443        Rect::parse(s)
444    }
445}
446
447impl fmt::Display for Rect {
448    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
449        write!(
450            f,
451            "{}, {}, {}, {}",
452            InvariantF64(self.x),
453            InvariantF64(self.y),
454            InvariantF64(self.width),
455            InvariantF64(self.height)
456        )
457    }
458}
459
460#[cfg(test)]
461mod tests {
462    use super::*;
463
464    #[test]
465    fn union_should_return_correct_value_for_intersecting_rects() {
466        let result = Rect::new(0.0, 0.0, 100.0, 100.0).union(Rect::new(50.0, 50.0, 100.0, 100.0));
467
468        assert_eq!(Rect::new(0.0, 0.0, 150.0, 150.0), result);
469    }
470
471    #[test]
472    fn union_should_return_correct_value_for_non_intersecting_rects() {
473        let result = Rect::new(0.0, 0.0, 100.0, 100.0).union(Rect::new(150.0, 150.0, 100.0, 100.0));
474
475        assert_eq!(Rect::new(0.0, 0.0, 250.0, 250.0), result);
476    }
477
478    #[test]
479    fn union_should_ignore_empty_this_rect() {
480        let result = Rect::new(0.0, 0.0, 0.0, 0.0).union(Rect::new(150.0, 150.0, 100.0, 100.0));
481
482        assert_eq!(Rect::new(150.0, 150.0, 100.0, 100.0), result);
483    }
484
485    #[test]
486    fn union_should_ignore_empty_other_rect() {
487        let result = Rect::new(0.0, 0.0, 100.0, 100.0).union(Rect::new(150.0, 150.0, 0.0, 0.0));
488
489        assert_eq!(Rect::new(0.0, 0.0, 100.0, 100.0), result);
490    }
491
492    #[test]
493    fn normalize_should_reverse_negative_size() {
494        let result = Rect::from_points(Point::new(100.0, 100.0), Point::new(0.0, 0.0)).normalize();
495
496        assert_eq!(Rect::new(0.0, 0.0, 100.0, 100.0), result);
497    }
498
499    #[test]
500    fn normalize_should_make_invalid_rects_empty() {
501        let result = Rect::new(
502            f64::NEG_INFINITY,
503            f64::INFINITY,
504            f64::INFINITY,
505            f64::INFINITY,
506        )
507        .normalize();
508
509        assert_eq!(Rect::default(), result);
510    }
511
512    #[test]
513    fn parse_parses() {
514        let rect = Rect::parse("1,2 3,-4").unwrap();
515        let expected = Rect::new(1.0, 2.0, 3.0, -4.0);
516        assert_eq!(expected, rect);
517    }
518
519    // Additional coverage (not part of the reference suite).
520
521    #[test]
522    fn edges_and_corners() {
523        let r = Rect::new(10.0, 20.0, 30.0, 40.0);
524        assert_eq!(r.right(), 40.0);
525        assert_eq!(r.bottom(), 60.0);
526        assert_eq!(r.left(), 10.0);
527        assert_eq!(r.top(), 20.0);
528        assert_eq!(r.top_right(), Point::new(40.0, 20.0));
529        assert_eq!(r.bottom_left(), Point::new(10.0, 60.0));
530        assert_eq!(r.bottom_right(), Point::new(40.0, 60.0));
531        assert_eq!(r.center(), Point::new(25.0, 40.0));
532        assert_eq!(r.position(), Point::new(10.0, 20.0));
533        assert_eq!(r.size(), Size::new(30.0, 40.0));
534        assert_eq!(
535            Rect::from_size(Size::new(1.0, 2.0)),
536            Rect::new(0.0, 0.0, 1.0, 2.0)
537        );
538    }
539
540    #[test]
541    fn contains_intersects_and_intersect() {
542        let r = Rect::new(0.0, 0.0, 10.0, 10.0);
543        assert!(r.contains(Point::new(10.0, 10.0)));
544        assert!(!r.contains_exclusive(Point::new(10.0, 10.0)));
545        assert!(r.contains_exclusive(Point::new(0.0, 0.0)));
546        assert!(r.contains_rect(Rect::new(1.0, 1.0, 9.0, 9.0)));
547        assert!(!r.contains_rect(Rect::new(1.0, 1.0, 10.0, 9.0)));
548        assert!(r.intersects(Rect::new(5.0, 5.0, 10.0, 10.0)));
549        assert!(!r.intersects(Rect::new(10.0, 0.0, 10.0, 10.0)));
550        assert_eq!(
551            r.intersect(Rect::new(5.0, 5.0, 10.0, 10.0)),
552            Rect::new(5.0, 5.0, 5.0, 5.0)
553        );
554        assert_eq!(
555            r.intersect(Rect::new(10.0, 0.0, 10.0, 10.0)),
556            Rect::default()
557        );
558        assert!(Rect::default().is_empty());
559    }
560
561    #[test]
562    fn inflate_deflate_translate_center_rect() {
563        let r = Rect::new(10.0, 10.0, 20.0, 20.0);
564        assert_eq!(r.inflate(5.0), Rect::new(5.0, 5.0, 30.0, 30.0));
565        assert_eq!(r.deflate(5.0), Rect::new(15.0, 15.0, 10.0, 10.0));
566        assert_eq!(r.deflate(15.0), Rect::new(25.0, 25.0, 0.0, 0.0));
567        assert_eq!(
568            r.inflate_thickness(Thickness::new(1.0, 2.0, 3.0, 4.0)),
569            Rect::new(9.0, 8.0, 24.0, 26.0)
570        );
571        assert_eq!(
572            r.translate(Vector::new(1.0, -1.0)),
573            Rect::new(11.0, 9.0, 20.0, 20.0)
574        );
575        assert_eq!(
576            r.center_rect(Rect::new(0.0, 0.0, 10.0, 10.0)),
577            Rect::new(15.0, 15.0, 10.0, 10.0)
578        );
579    }
580
581    #[test]
582    fn transform_to_aabb() {
583        let r = Rect::new(0.0, 0.0, 10.0, 20.0);
584        assert_eq!(
585            r.transform_to_aabb(Matrix::create_translation(5.0, 5.0)),
586            Rect::new(5.0, 5.0, 10.0, 20.0)
587        );
588        let rotated = r.transform_to_aabb(Matrix::create_rotation(std::f64::consts::FRAC_PI_2));
589        assert!((rotated.x - -20.0).abs() < 1e-9);
590        assert!(rotated.y.abs() < 1e-9);
591        assert!((rotated.width - 20.0).abs() < 1e-9);
592        assert!((rotated.height - 10.0).abs() < 1e-9);
593    }
594
595    #[test]
596    fn operators_display_and_union_optional() {
597        let r = Rect::new(1.0, 2.0, 3.0, 4.0);
598        assert_eq!(r * 2.0, Rect::new(2.0, 4.0, 6.0, 8.0));
599        assert_eq!(r * Vector::new(2.0, 3.0), Rect::new(2.0, 6.0, 6.0, 12.0));
600        assert_eq!(r / Vector::new(2.0, 2.0), Rect::new(0.5, 1.0, 1.5, 2.0));
601        assert_eq!(r.to_string(), "1, 2, 3, 4");
602        assert_eq!(Rect::parse("1,2,3").unwrap_err().message(), "Invalid Rect.");
603        assert_eq!(Rect::union_optional(None, Some(r)), Some(r));
604        assert_eq!(Rect::union_optional(Some(r), None), Some(r));
605        assert_eq!(Rect::union_optional(None, None), None);
606    }
607}