1use std::str::FromStr;
4
5use crate::utilities::span_helpers::parse_double;
6use crate::utilities::{FormatError, SpanStringTokenizer};
7use crate::{Point, Rect, RelativeUnit, Size};
8
9#[derive(Clone, Copy, Debug, PartialEq, Default)]
11pub struct RelativeRect {
12 pub unit: RelativeUnit,
14 pub rect: Rect,
16}
17
18impl RelativeRect {
19 pub const FILL: RelativeRect = RelativeRect::new(0.0, 0.0, 1.0, 1.0, RelativeUnit::Relative);
21
22 #[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 #[inline]
33 pub const fn from_rect(rect: Rect, unit: RelativeUnit) -> Self {
34 Self { unit, rect }
35 }
36
37 #[inline]
39 pub const fn from_size(size: Size, unit: RelativeUnit) -> Self {
40 Self::from_rect(Rect::from_size(size), unit)
41 }
42
43 #[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 #[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 #[inline]
57 pub fn equals(&self, p: RelativeRect) -> bool {
58 self.unit == p.unit && self.rect == p.rect
59 }
60
61 #[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 #[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 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 fn round3(value: f64) -> f64 {
151 (value * 1000.0).round_ties_even() / 1000.0
152 }
153
154 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 #[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}