Skip to main content

ferroui_base/
pixel_rect.rs

1//! Represents a rectangle in device pixels.
2
3use std::fmt;
4use std::str::FromStr;
5
6use crate::utilities::{FormatError, SpanStringTokenizer};
7use crate::{PixelPoint, PixelSize, PixelVector, Point, Rect, Vector};
8
9/// Represents a rectangle in device pixels. Integer arithmetic wraps on overflow.
10#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
11pub struct PixelRect {
12    /// The X position.
13    pub x: i32,
14    /// The Y position.
15    pub y: i32,
16    /// The width.
17    pub width: i32,
18    /// The height.
19    pub height: i32,
20}
21
22impl PixelRect {
23    /// Initializes a new instance of the [`PixelRect`] structure.
24    #[inline]
25    pub const fn new(x: i32, y: i32, width: i32, height: i32) -> Self {
26        Self {
27            x,
28            y,
29            width,
30            height,
31        }
32    }
33
34    /// Initializes a [`PixelRect`] at the origin with the given size.
35    #[inline]
36    pub const fn from_size(size: PixelSize) -> Self {
37        Self::new(0, 0, size.width, size.height)
38    }
39
40    /// Initializes a [`PixelRect`] from its position and size.
41    #[inline]
42    pub const fn from_position_size(position: PixelPoint, size: PixelSize) -> Self {
43        Self::new(position.x, position.y, size.width, size.height)
44    }
45
46    /// Initializes a [`PixelRect`] from its top left and bottom right corners.
47    #[inline]
48    pub const fn from_points(top_left: PixelPoint, bottom_right: PixelPoint) -> Self {
49        Self::new(
50            top_left.x,
51            top_left.y,
52            bottom_right.x.wrapping_sub(top_left.x),
53            bottom_right.y.wrapping_sub(top_left.y),
54        )
55    }
56
57    /// Gets the position of the rectangle.
58    #[inline]
59    pub const fn position(&self) -> PixelPoint {
60        PixelPoint::new(self.x, self.y)
61    }
62
63    /// Gets the size of the rectangle.
64    #[inline]
65    pub const fn size(&self) -> PixelSize {
66        PixelSize::new(self.width, self.height)
67    }
68
69    /// Gets the right position of the rectangle.
70    #[inline]
71    pub const fn right(&self) -> i32 {
72        self.x.wrapping_add(self.width)
73    }
74
75    /// Gets the bottom position of the rectangle.
76    #[inline]
77    pub const fn bottom(&self) -> i32 {
78        self.y.wrapping_add(self.height)
79    }
80
81    /// Gets the top left point of the rectangle.
82    #[inline]
83    pub const fn top_left(&self) -> PixelPoint {
84        PixelPoint::new(self.x, self.y)
85    }
86
87    /// Gets the top right point of the rectangle.
88    #[inline]
89    pub const fn top_right(&self) -> PixelPoint {
90        PixelPoint::new(self.right(), self.y)
91    }
92
93    /// Gets the bottom left point of the rectangle.
94    #[inline]
95    pub const fn bottom_left(&self) -> PixelPoint {
96        PixelPoint::new(self.x, self.bottom())
97    }
98
99    /// Gets the bottom right point of the rectangle.
100    #[inline]
101    pub const fn bottom_right(&self) -> PixelPoint {
102        PixelPoint::new(self.right(), self.bottom())
103    }
104
105    /// Gets the center point of the rectangle.
106    #[inline]
107    pub const fn center(&self) -> PixelPoint {
108        PixelPoint::new(
109            self.x.wrapping_add(self.width / 2),
110            self.y.wrapping_add(self.height / 2),
111        )
112    }
113
114    /// Determines whether a point is in the bounds of the rectangle (edges inclusive).
115    #[inline]
116    pub const fn contains(&self, p: PixelPoint) -> bool {
117        p.x >= self.x && p.x <= self.right() && p.y >= self.y && p.y <= self.bottom()
118    }
119
120    /// Determines whether a point is in the bounds of the rectangle, exclusive of
121    /// the rectangle's bottom/right edge.
122    #[inline]
123    pub const fn contains_exclusive(&self, p: PixelPoint) -> bool {
124        p.x >= self.x
125            && p.x < self.x.wrapping_add(self.width)
126            && p.y >= self.y
127            && p.y < self.y.wrapping_add(self.height)
128    }
129
130    /// Determines whether the rectangle fully contains another rectangle.
131    #[inline]
132    pub const fn contains_rect(&self, r: PixelRect) -> bool {
133        self.contains(r.top_left()) && self.contains(r.bottom_right())
134    }
135
136    /// Centers another rectangle in this rectangle.
137    #[inline]
138    pub const fn center_rect(&self, rect: PixelRect) -> PixelRect {
139        PixelRect::new(
140            self.x.wrapping_add(self.width.wrapping_sub(rect.width) / 2),
141            self.y
142                .wrapping_add(self.height.wrapping_sub(rect.height) / 2),
143            rect.width,
144            rect.height,
145        )
146    }
147
148    /// Returns a boolean indicating whether the rect is equal to the other given rect.
149    #[inline]
150    pub const fn equals(&self, other: PixelRect) -> bool {
151        self.x == other.x
152            && self.y == other.y
153            && self.width == other.width
154            && self.height == other.height
155    }
156
157    /// Gets the intersection of two rectangles, or an empty (default) rectangle if
158    /// they do not overlap.
159    pub const fn intersect(&self, rect: PixelRect) -> PixelRect {
160        let new_left = if rect.x > self.x { rect.x } else { self.x };
161        let new_top = if rect.y > self.y { rect.y } else { self.y };
162        let new_right = if rect.right() < self.right() {
163            rect.right()
164        } else {
165            self.right()
166        };
167        let new_bottom = if rect.bottom() < self.bottom() {
168            rect.bottom()
169        } else {
170            self.bottom()
171        };
172
173        if (new_right > new_left) && (new_bottom > new_top) {
174            PixelRect::new(
175                new_left,
176                new_top,
177                new_right.wrapping_sub(new_left),
178                new_bottom.wrapping_sub(new_top),
179            )
180        } else {
181            PixelRect::new(0, 0, 0, 0)
182        }
183    }
184
185    /// Determines whether a rectangle intersects with this rectangle.
186    #[inline]
187    pub const fn intersects(&self, rect: PixelRect) -> bool {
188        (rect.x < self.right())
189            && (self.x < rect.right())
190            && (rect.y < self.bottom())
191            && (self.y < rect.bottom())
192    }
193
194    /// Translates the rectangle by an offset.
195    #[inline]
196    pub fn translate(&self, offset: PixelVector) -> PixelRect {
197        PixelRect::from_position_size(self.position() + offset, self.size())
198    }
199
200    /// Gets the union of two rectangles. A rectangle whose width and height are
201    /// both zero is ignored.
202    pub fn union(&self, rect: PixelRect) -> PixelRect {
203        if self.width == 0 && self.height == 0 {
204            rect
205        } else if rect.width == 0 && rect.height == 0 {
206            *self
207        } else {
208            let x1 = self.x.min(rect.x);
209            let x2 = self.right().max(rect.right());
210            let y1 = self.y.min(rect.y);
211            let y2 = self.bottom().max(rect.bottom());
212
213            PixelRect::from_points(PixelPoint::new(x1, y1), PixelPoint::new(x2, y2))
214        }
215    }
216
217    /// Returns a new [`PixelRect`] with the specified X position.
218    #[inline]
219    pub const fn with_x(&self, x: i32) -> PixelRect {
220        PixelRect::new(x, self.y, self.width, self.height)
221    }
222
223    /// Returns a new [`PixelRect`] with the specified Y position.
224    #[inline]
225    pub const fn with_y(&self, y: i32) -> PixelRect {
226        PixelRect::new(self.x, y, self.width, self.height)
227    }
228
229    /// Returns a new [`PixelRect`] with the specified width.
230    #[inline]
231    pub const fn with_width(&self, width: i32) -> PixelRect {
232        PixelRect::new(self.x, self.y, width, self.height)
233    }
234
235    /// Returns a new [`PixelRect`] with the specified height.
236    #[inline]
237    pub const fn with_height(&self, height: i32) -> PixelRect {
238        PixelRect::new(self.x, self.y, self.width, height)
239    }
240
241    /// Converts the [`PixelRect`] to a device-independent [`Rect`] using the
242    /// specified scaling factor.
243    #[inline]
244    pub fn to_rect(&self, scale: f64) -> Rect {
245        Rect::from_position_size(self.position().to_point(scale), self.size().to_size(scale))
246    }
247
248    /// Converts the [`PixelRect`] to a device-independent [`Rect`] using the
249    /// specified per-axis scaling factor.
250    #[inline]
251    pub fn to_rect_vector(&self, scale: Vector) -> Rect {
252        Rect::from_position_size(
253            self.position().to_point_vector(scale),
254            self.size().to_size_vector(scale),
255        )
256    }
257
258    /// Converts the [`PixelRect`] to a device-independent [`Rect`] using the
259    /// specified dots per inch (DPI).
260    #[inline]
261    pub fn to_rect_with_dpi(&self, dpi: f64) -> Rect {
262        Rect::from_position_size(
263            self.position().to_point_with_dpi(dpi),
264            self.size().to_size_with_dpi(dpi),
265        )
266    }
267
268    /// Converts the [`PixelRect`] to a device-independent [`Rect`] using the
269    /// specified per-axis dots per inch (DPI).
270    #[inline]
271    pub fn to_rect_with_dpi_vector(&self, dpi: Vector) -> Rect {
272        Rect::from_position_size(
273            self.position().to_point_with_dpi_vector(dpi),
274            self.size().to_size_with_dpi_vector(dpi),
275        )
276    }
277
278    /// Converts a [`Rect`] to device pixels using the specified scaling factor.
279    /// The top left is truncated and the bottom right is rounded up.
280    #[inline]
281    pub fn from_rect(rect: Rect, scale: f64) -> PixelRect {
282        PixelRect::from_points(
283            PixelPoint::from_point(rect.position(), scale),
284            Self::from_point_ceiling(rect.bottom_right(), Vector::new(scale, scale)),
285        )
286    }
287
288    /// Converts a [`Rect`] to device pixels using the specified per-axis scaling factor.
289    #[inline]
290    pub fn from_rect_vector(rect: Rect, scale: Vector) -> PixelRect {
291        PixelRect::from_points(
292            PixelPoint::from_point_vector(rect.position(), scale),
293            Self::from_point_ceiling(rect.bottom_right(), scale),
294        )
295    }
296
297    /// Converts a [`Rect`] to device pixels using the specified dots per inch (DPI).
298    #[inline]
299    pub fn from_rect_with_dpi(rect: Rect, dpi: f64) -> PixelRect {
300        PixelRect::from_points(
301            PixelPoint::from_point_with_dpi(rect.position(), dpi),
302            Self::from_point_ceiling(rect.bottom_right(), Vector::new(dpi / 96.0, dpi / 96.0)),
303        )
304    }
305
306    /// Converts a [`Rect`] to device pixels using the specified per-axis dots per inch (DPI).
307    #[inline]
308    pub fn from_rect_with_dpi_vector(rect: Rect, dpi: Vector) -> PixelRect {
309        PixelRect::from_points(
310            PixelPoint::from_point_with_dpi_vector(rect.position(), dpi),
311            Self::from_point_ceiling(rect.bottom_right(), dpi / 96.0),
312        )
313    }
314
315    /// Parses a [`PixelRect`] string (`"x, y, width, height"`).
316    pub fn parse(s: &str) -> Result<Self, FormatError> {
317        SpanStringTokenizer::with_message(s, "Invalid PixelRect.").scope(|t| {
318            Ok(PixelRect::new(
319                t.read_int32()?,
320                t.read_int32()?,
321                t.read_int32()?,
322                t.read_int32()?,
323            ))
324        })
325    }
326
327    #[inline]
328    fn from_point_ceiling(point: Point, scale: Vector) -> PixelPoint {
329        PixelPoint::new(
330            (point.x * scale.x).ceil() as i32,
331            (point.y * scale.y).ceil() as i32,
332        )
333    }
334}
335
336impl FromStr for PixelRect {
337    type Err = FormatError;
338    #[inline]
339    fn from_str(s: &str) -> Result<Self, FormatError> {
340        PixelRect::parse(s)
341    }
342}
343
344impl fmt::Display for PixelRect {
345    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
346        write!(f, "{}, {}, {}, {}", self.x, self.y, self.width, self.height)
347    }
348}
349
350#[cfg(test)]
351mod tests {
352    use super::*;
353
354    #[test]
355    fn from_rect_snaps_to_device_pixels() {
356        let rect = Rect::new(189.0, 189.0, 26.0, 164.0);
357        let result = PixelRect::from_rect(rect, 1.5);
358
359        assert_eq!(PixelRect::new(283, 283, 40, 247), result);
360    }
361
362    #[test]
363    fn from_rect_vector_snaps_to_device_pixels() {
364        let rect = Rect::new(189.0, 189.0, 26.0, 164.0);
365        let result = PixelRect::from_rect_vector(rect, Vector::new(1.5, 1.5));
366
367        assert_eq!(PixelRect::new(283, 283, 40, 247), result);
368    }
369
370    #[test]
371    fn from_rect_with_dpi_snaps_to_device_pixels() {
372        let rect = Rect::new(189.0, 189.0, 26.0, 164.0);
373        let result = PixelRect::from_rect_with_dpi(rect, 144.0);
374
375        assert_eq!(PixelRect::new(283, 283, 40, 247), result);
376    }
377
378    #[test]
379    fn from_rect_with_dpi_vector_snaps_to_device_pixels() {
380        let rect = Rect::new(189.0, 189.0, 26.0, 164.0);
381        let result = PixelRect::from_rect_with_dpi_vector(rect, Vector::new(144.0, 144.0));
382
383        assert_eq!(PixelRect::new(283, 283, 40, 247), result);
384    }
385
386    // Additional coverage (not part of the reference suite).
387
388    #[test]
389    fn geometry() {
390        let r = PixelRect::new(10, 20, 30, 41);
391        assert_eq!(r.right(), 40);
392        assert_eq!(r.bottom(), 61);
393        assert_eq!(r.center(), PixelPoint::new(25, 40));
394        assert_eq!(r.top_right(), PixelPoint::new(40, 20));
395        assert_eq!(r.bottom_left(), PixelPoint::new(10, 61));
396        assert!(r.contains(PixelPoint::new(40, 61)));
397        assert!(!r.contains_exclusive(PixelPoint::new(40, 61)));
398        assert!(r.contains_rect(PixelRect::new(11, 21, 5, 5)));
399        assert!(r.intersects(PixelRect::new(39, 60, 5, 5)));
400        assert!(!r.intersects(PixelRect::new(40, 61, 5, 5)));
401        assert_eq!(
402            r.intersect(PixelRect::new(20, 30, 100, 100)),
403            PixelRect::new(20, 30, 20, 31)
404        );
405        assert_eq!(
406            r.intersect(PixelRect::new(100, 100, 1, 1)),
407            PixelRect::default()
408        );
409        assert_eq!(
410            r.union(PixelRect::new(0, 0, 5, 5)),
411            PixelRect::new(0, 0, 40, 61)
412        );
413        assert_eq!(r.union(PixelRect::default()), r);
414        assert_eq!(
415            r.translate(PixelVector::new(1, 1)),
416            PixelRect::new(11, 21, 30, 41)
417        );
418        assert_eq!(
419            PixelRect::new(0, 0, 10, 10).center_rect(PixelRect::new(0, 0, 4, 4)),
420            PixelRect::new(3, 3, 4, 4)
421        );
422    }
423
424    #[test]
425    fn parse_display_and_to_rect() {
426        assert_eq!(
427            PixelRect::parse("1,2,3,4").unwrap(),
428            PixelRect::new(1, 2, 3, 4)
429        );
430        assert_eq!(
431            PixelRect::parse("1,2,3").unwrap_err().message(),
432            "Invalid PixelRect."
433        );
434        assert_eq!(PixelRect::new(1, 2, 3, 4).to_string(), "1, 2, 3, 4");
435        assert_eq!(
436            PixelRect::new(3, 6, 9, 12).to_rect(1.5),
437            Rect::new(2.0, 4.0, 6.0, 8.0)
438        );
439        assert_eq!(
440            PixelRect::new(3, 6, 9, 12).to_rect_with_dpi(144.0),
441            Rect::new(2.0, 4.0, 6.0, 8.0)
442        );
443    }
444}