Skip to main content

ferroui_base/platform/
ltrb_rect.rs

1use crate::utilities::span_helpers::InvariantF64;
2use crate::{Matrix, PixelRect, Point, Rect, Thickness};
3use std::fmt;
4
5/// A rectangle stored as left/top/right/bottom edges.
6#[derive(Clone, Copy, Debug, Default, PartialEq)]
7pub struct LtrbRect {
8    pub left: f64,
9    pub top: f64,
10    pub right: f64,
11    pub bottom: f64,
12}
13
14impl LtrbRect {
15    pub const INFINITE: LtrbRect =
16        LtrbRect { left: f64::NEG_INFINITY, top: f64::NEG_INFINITY, right: f64::INFINITY, bottom: f64::INFINITY };
17
18    pub const fn new(left: f64, top: f64, right: f64, bottom: f64) -> Self {
19        Self { left, top, right, bottom }
20    }
21
22    pub fn from_rect(rc: Rect) -> Self {
23        let rc = rc.normalize();
24        Self { left: rc.x, top: rc.y, right: rc.right(), bottom: rc.bottom() }
25    }
26
27    pub fn width(&self) -> f64 {
28        self.right - self.left
29    }
30
31    pub fn height(&self) -> f64 {
32        self.bottom - self.top
33    }
34
35    pub fn is_well_ordered(&self) -> bool {
36        self.left <= self.right && self.top <= self.bottom
37    }
38
39    pub fn is_zero_size(&self) -> bool {
40        self.left == self.right || self.top == self.bottom
41    }
42
43    pub fn is_empty(&self) -> bool {
44        self.is_zero_size()
45    }
46
47    pub fn null_if_zero_size(self) -> Option<LtrbRect> {
48        (!self.is_zero_size()).then_some(self)
49    }
50
51    pub fn intersect_or_null(&self, rect: LtrbRect) -> Option<LtrbRect> {
52        let left = if rect.left > self.left { rect.left } else { self.left };
53        let top = if rect.top > self.top { rect.top } else { self.top };
54        let right = if rect.right < self.right { rect.right } else { self.right };
55        let bottom = if rect.bottom < self.bottom { rect.bottom } else { self.bottom };
56        (right > left && bottom > top).then_some(LtrbRect::new(left, top, right, bottom))
57    }
58
59    pub fn intersect_or_empty(&self, rect: LtrbRect) -> LtrbRect {
60        self.intersect_or_null(rect).unwrap_or_default()
61    }
62
63    pub fn intersects(&self, rect: LtrbRect) -> bool {
64        rect.left < self.right && self.left < rect.right && rect.top < self.bottom && self.top < rect.bottom
65    }
66
67    pub fn contains(&self, x: f64, y: f64) -> bool {
68        x >= self.left && x <= self.right && y >= self.top && y <= self.bottom
69    }
70
71    pub fn to_rect(&self) -> Rect {
72        Rect::new(self.left, self.top, self.right - self.left, self.bottom - self.top)
73    }
74
75    pub fn inflate(&self, thickness: Thickness) -> LtrbRect {
76        LtrbRect::new(
77            self.left - thickness.left,
78            self.top - thickness.top,
79            self.right + thickness.right,
80            self.bottom + thickness.bottom,
81        )
82    }
83
84    /// The axis-aligned bounding box of the rectangle transformed by
85    /// `matrix`.
86    pub fn top_left(&self) -> Point {
87        Point::new(self.left, self.top)
88    }
89
90    pub fn top_right(&self) -> Point {
91        Point::new(self.right, self.top)
92    }
93
94    pub fn bottom_left(&self) -> Point {
95        Point::new(self.left, self.bottom)
96    }
97
98    pub fn bottom_right(&self) -> Point {
99        Point::new(self.right, self.bottom)
100    }
101
102    pub fn transform_to_aabb(&self, matrix: Matrix) -> LtrbRect {
103        let points = [
104            self.top_left().transform(matrix),
105            self.top_right().transform(matrix),
106            self.bottom_right().transform(matrix),
107            self.bottom_left().transform(matrix),
108        ];
109
110        let mut left = f64::MAX;
111        let mut right = f64::MIN;
112        let mut top = f64::MAX;
113        let mut bottom = f64::MIN;
114
115        for p in points {
116            if p.x < left {
117                left = p.x;
118            }
119            if p.x > right {
120                right = p.x;
121            }
122            if p.y < top {
123                top = p.y;
124            }
125            if p.y > bottom {
126                bottom = p.y;
127            }
128        }
129
130        LtrbRect::new(left, top, right, bottom)
131    }
132
133    /// The union of two optional rectangles, where `None` is the empty
134    /// set.
135    pub fn full_union(left: Option<LtrbRect>, right: Option<LtrbRect>) -> Option<LtrbRect> {
136        match (left, right) {
137            (None, right) => right,
138            (left, None) => left,
139            (Some(left), Some(right)) => Some(right.union(left)),
140        }
141    }
142
143    /// The union of an optional rectangle with an optional `Rect`, where
144    /// `None` is the empty set.
145    pub fn full_union_rect(left: Option<LtrbRect>, right: Option<Rect>) -> Option<LtrbRect> {
146        match (left, right) {
147            (left, None) => left,
148            (None, Some(right)) => Some(LtrbRect::from_rect(right)),
149            (Some(left), Some(right)) => Some(left.union(LtrbRect::from_rect(right))),
150        }
151    }
152
153    pub fn union(&self, rect: LtrbRect) -> LtrbRect {
154        LtrbRect::new(
155            self.left.min(rect.left),
156            self.top.min(rect.top),
157            self.right.max(rect.right),
158            self.bottom.max(rect.bottom),
159        )
160    }
161
162    /// Whether the rectangle contains another one, edges included.
163    pub fn contains_rect(&self, rect: LtrbRect) -> bool {
164        rect.left >= self.left && rect.right <= self.right && rect.top >= self.top && rect.bottom <= self.bottom
165    }
166}
167
168impl fmt::Display for LtrbRect {
169    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170        write!(
171            f,
172            "{}:{}-{}:{} ({}x{})",
173            InvariantF64(self.left),
174            InvariantF64(self.top),
175            InvariantF64(self.right),
176            InvariantF64(self.bottom),
177            InvariantF64(self.width()),
178            InvariantF64(self.height())
179        )
180    }
181}
182
183/// A pixel rectangle stored as left/top/right/bottom edges.
184#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
185pub struct LtrbPixelRect {
186    pub left: i32,
187    pub top: i32,
188    pub right: i32,
189    pub bottom: i32,
190}
191
192impl LtrbPixelRect {
193    pub const fn new(left: i32, top: i32, right: i32, bottom: i32) -> Self {
194        Self { left, top, right, bottom }
195    }
196
197    pub fn from_pixel_rect(rc: PixelRect) -> Self {
198        Self { left: rc.x, top: rc.y, right: rc.right(), bottom: rc.bottom() }
199    }
200
201    pub fn width(&self) -> i32 {
202        self.right - self.left
203    }
204
205    pub fn height(&self) -> i32 {
206        self.bottom - self.top
207    }
208
209    pub fn is_empty(&self) -> bool {
210        self.left == self.right && self.top == self.bottom
211    }
212
213    pub fn to_pixel_rect(&self) -> PixelRect {
214        PixelRect::new(self.left, self.top, self.right - self.left, self.bottom - self.top)
215    }
216
217    pub fn union(&self, rect: LtrbPixelRect) -> LtrbPixelRect {
218        LtrbPixelRect::new(
219            self.left.min(rect.left),
220            self.top.min(rect.top),
221            self.right.max(rect.right),
222            self.bottom.max(rect.bottom),
223        )
224    }
225
226    /// Converts a logical rectangle to the smallest pixel rectangle that
227    /// contains it at the given scaling.
228    pub fn from_rect_with_dpi(rect: LtrbRect, scaling: f64) -> Self {
229        Self {
230            left: (rect.left * scaling).floor() as i32,
231            top: (rect.top * scaling).floor() as i32,
232            right: (rect.right * scaling).ceil() as i32,
233            bottom: (rect.bottom * scaling).ceil() as i32,
234        }
235    }
236
237    pub fn contains(&self, x: i32, y: i32) -> bool {
238        x >= self.left && x <= self.right && y >= self.top && y <= self.bottom
239    }
240
241    pub fn to_rect_unscaled(&self) -> Rect {
242        Rect::new(
243            f64::from(self.left),
244            f64::from(self.top),
245            f64::from(self.right - self.left),
246            f64::from(self.bottom - self.top),
247        )
248    }
249
250    pub fn to_ltrb_rect_unscaled(&self) -> LtrbRect {
251        LtrbRect::new(f64::from(self.left), f64::from(self.top), f64::from(self.right), f64::from(self.bottom))
252    }
253
254    pub fn from_rect_unscaled(rect: LtrbRect) -> Self {
255        Self {
256            left: rect.left as i32,
257            top: rect.top as i32,
258            right: rect.right.ceil() as i32,
259            bottom: rect.bottom.ceil() as i32,
260        }
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    // Not from upstream: the original has no tests of these types.
267    use super::*;
268
269    #[test]
270    fn corners_and_containment() {
271        let rect = LtrbRect::new(1.0, 2.0, 5.0, 8.0);
272
273        assert_eq!(Point::new(1.0, 2.0), rect.top_left());
274        assert_eq!(Point::new(5.0, 2.0), rect.top_right());
275        assert_eq!(Point::new(1.0, 8.0), rect.bottom_left());
276        assert_eq!(Point::new(5.0, 8.0), rect.bottom_right());
277
278        assert!(rect.contains_rect(rect));
279        assert!(rect.contains_rect(LtrbRect::new(2.0, 3.0, 4.0, 4.0)));
280        assert!(!rect.contains_rect(LtrbRect::new(0.0, 3.0, 4.0, 4.0)));
281        assert_eq!("1:2-5:8 (4x6)", rect.to_string());
282    }
283
284    #[test]
285    fn full_union_with_a_rect() {
286        let left = LtrbRect::new(0.0, 0.0, 1.0, 1.0);
287        let right = Rect::new(2.0, 2.0, 2.0, 2.0);
288
289        assert_eq!(None, LtrbRect::full_union_rect(None, None));
290        assert_eq!(Some(left), LtrbRect::full_union_rect(Some(left), None));
291        assert_eq!(Some(LtrbRect::new(2.0, 2.0, 4.0, 4.0)), LtrbRect::full_union_rect(None, Some(right)));
292        assert_eq!(Some(LtrbRect::new(0.0, 0.0, 4.0, 4.0)), LtrbRect::full_union_rect(Some(left), Some(right)));
293    }
294
295    #[test]
296    fn pixel_rect_conversions_without_scaling() {
297        let rect = LtrbPixelRect::new(1, 2, 5, 8);
298
299        assert!(rect.contains(1, 2));
300        assert!(rect.contains(5, 8));
301        assert!(!rect.contains(6, 8));
302        assert_eq!(Rect::new(1.0, 2.0, 4.0, 6.0), rect.to_rect_unscaled());
303        assert_eq!(LtrbRect::new(1.0, 2.0, 5.0, 8.0), rect.to_ltrb_rect_unscaled());
304
305        let rounded = LtrbPixelRect::from_rect_unscaled(LtrbRect::new(1.7, 2.2, 4.1, 7.5));
306        assert_eq!(LtrbPixelRect::new(1, 2, 5, 8), rounded);
307    }
308}