Skip to main content

ferroui_base/
relative_rect.rs

1//! Defines a rectangle that may be defined relative to a containing element.
2
3use std::str::FromStr;
4
5use crate::utilities::span_helpers::parse_double;
6use crate::utilities::{FormatError, SpanStringTokenizer};
7use crate::{Point, Rect, RelativeUnit, Size};
8
9/// Defines a rectangle that may be defined relative to a containing element.
10#[derive(Clone, Copy, Debug, PartialEq, Default)]
11pub struct RelativeRect {
12    /// The unit of the rectangle.
13    pub unit: RelativeUnit,
14    /// The rectangle.
15    pub rect: Rect,
16}
17
18impl RelativeRect {
19    /// A rectangle that represents 100% of an area.
20    pub const FILL: RelativeRect = RelativeRect::new(0.0, 0.0, 1.0, 1.0, RelativeUnit::Relative);
21
22    /// Initializes a new instance of the [`RelativeRect`] structure.
23    #[inline]
24    pub const fn new(x: f64, y: f64, width: f64, height: f64, unit: RelativeUnit) -> Self {
25        Self {
26            unit,
27            rect: Rect::new(x, y, width, height),
28        }
29    }
30
31    /// Initializes a [`RelativeRect`] from a [`Rect`].
32    #[inline]
33    pub const fn from_rect(rect: Rect, unit: RelativeUnit) -> Self {
34        Self { unit, rect }
35    }
36
37    /// Initializes a [`RelativeRect`] at the origin with the given size.
38    #[inline]
39    pub const fn from_size(size: Size, unit: RelativeUnit) -> Self {
40        Self::from_rect(Rect::from_size(size), unit)
41    }
42
43    /// Initializes a [`RelativeRect`] from its position and size.
44    #[inline]
45    pub const fn from_position_size(position: Point, size: Size, unit: RelativeUnit) -> Self {
46        Self::from_rect(Rect::from_position_size(position, size), unit)
47    }
48
49    /// Initializes a [`RelativeRect`] from its top left and bottom right corners.
50    #[inline]
51    pub fn from_points(top_left: Point, bottom_right: Point, unit: RelativeUnit) -> Self {
52        Self::from_rect(Rect::from_points(top_left, bottom_right), unit)
53    }
54
55    /// Checks if the [`RelativeRect`] equals another rectangle.
56    #[inline]
57    pub fn equals(&self, p: RelativeRect) -> bool {
58        self.unit == p.unit && self.rect == p.rect
59    }
60
61    /// Converts a [`RelativeRect`] into pixels, using `size` as the size of the
62    /// visual the rectangle is relative to.
63    #[inline]
64    pub fn to_pixels(&self, size: Size) -> Rect {
65        if self.unit == RelativeUnit::Absolute {
66            self.rect
67        } else {
68            Rect::new(
69                self.rect.x * size.width,
70                self.rect.y * size.height,
71                self.rect.width * size.width,
72                self.rect.height * size.height,
73            )
74        }
75    }
76
77    /// Converts a [`RelativeRect`] into pixels, using `bounding_box` as the box the
78    /// rectangle is relative to.
79    #[inline]
80    pub fn to_pixels_rect(&self, bounding_box: Rect) -> Rect {
81        if self.unit == RelativeUnit::Absolute {
82            self.rect
83        } else {
84            Rect::new(
85                bounding_box.x + self.rect.x * bounding_box.width,
86                bounding_box.y + self.rect.y * bounding_box.height,
87                self.rect.width * bounding_box.width,
88                self.rect.height * bounding_box.height,
89            )
90        }
91    }
92
93    /// Parses a [`RelativeRect`] string: four numbers (absolute) or four
94    /// percentages (relative).
95    pub fn parse(s: &str) -> Result<Self, FormatError> {
96        SpanStringTokenizer::with_separator(s, ',', Some("Invalid RelativeRect.")).scope(|t| {
97            let mut x = t.read_span()?;
98            let mut y = t.read_span()?;
99            let mut width = t.read_span()?;
100            let mut height = t.read_span()?;
101
102            let mut unit = RelativeUnit::Absolute;
103            let mut scale = 1.0;
104
105            let x_relative = x.ends_with('%');
106            let y_relative = y.ends_with('%');
107            let width_relative = width.ends_with('%');
108            let height_relative = height.ends_with('%');
109
110            if x_relative && y_relative && width_relative && height_relative {
111                x = x.trim_end_matches('%');
112                y = y.trim_end_matches('%');
113                width = width.trim_end_matches('%');
114                height = height.trim_end_matches('%');
115
116                unit = RelativeUnit::Relative;
117                scale = 0.01;
118            } else if x_relative || y_relative || width_relative || height_relative {
119                return Err(FormatError::new(
120                    "If one coordinate is relative, all must be.",
121                ));
122            }
123
124            let parse = |v: &str| parse_double(v).ok_or_else(|| FormatError::invalid_input(v));
125
126            Ok(RelativeRect::new(
127                parse(x)? * scale,
128                parse(y)? * scale,
129                parse(width)? * scale,
130                parse(height)? * scale,
131                unit,
132            ))
133        })
134    }
135}
136
137impl FromStr for RelativeRect {
138    type Err = FormatError;
139    #[inline]
140    fn from_str(s: &str) -> Result<Self, FormatError> {
141        RelativeRect::parse(s)
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    /// Rounds to 3 decimals, ties to even.
150    fn round3(value: f64) -> f64 {
151        (value * 1000.0).round_ties_even() / 1000.0
152    }
153
154    /// Compares two rects after rounding each component to 3 decimals.
155    fn compare(a: RelativeRect, b: RelativeRect) -> bool {
156        a.unit == b.unit
157            && round3(a.rect.x) == round3(b.rect.x)
158            && round3(a.rect.y) == round3(b.rect.y)
159            && round3(a.rect.width) == round3(b.rect.width)
160            && round3(a.rect.height) == round3(b.rect.height)
161    }
162
163    #[test]
164    fn parse_should_accept_absolute_value() {
165        let result = RelativeRect::parse("4,5,50,60").unwrap();
166
167        assert!(compare(
168            RelativeRect::new(4.0, 5.0, 50.0, 60.0, RelativeUnit::Absolute),
169            result
170        ));
171    }
172
173    #[test]
174    fn parse_should_accept_relative_value() {
175        let result = RelativeRect::parse("10%, 20%, 40%, 70%").unwrap();
176
177        assert!(compare(
178            RelativeRect::new(0.1, 0.2, 0.4, 0.7, RelativeUnit::Relative),
179            result
180        ));
181    }
182
183    #[test]
184    fn parse_should_throw_mixed_values() {
185        assert!(RelativeRect::parse("10%, 20%, 40, 70%").is_err());
186    }
187
188    // Additional coverage (not part of the reference suite).
189
190    #[test]
191    fn to_pixels_and_errors() {
192        assert_eq!(
193            RelativeRect::parse("10%, 20%, 40, 70%")
194                .unwrap_err()
195                .message(),
196            "If one coordinate is relative, all must be."
197        );
198        assert_eq!(
199            RelativeRect::parse("1,2,3").unwrap_err().message(),
200            "Invalid RelativeRect."
201        );
202        assert_eq!(
203            RelativeRect::FILL.to_pixels(Size::new(100.0, 50.0)),
204            Rect::new(0.0, 0.0, 100.0, 50.0)
205        );
206        assert_eq!(
207            RelativeRect::new(0.5, 0.5, 0.5, 0.5, RelativeUnit::Relative)
208                .to_pixels_rect(Rect::new(10.0, 10.0, 100.0, 50.0)),
209            Rect::new(60.0, 35.0, 50.0, 25.0)
210        );
211        let abs = RelativeRect::new(1.0, 2.0, 3.0, 4.0, RelativeUnit::Absolute);
212        assert_eq!(abs.to_pixels(Size::new(100.0, 50.0)), abs.rect);
213    }
214}