ferroui_base/
relative_point.rs1use std::fmt;
4use std::str::FromStr;
5
6use crate::utilities::span_helpers::{parse_double, InvariantF64};
7use crate::utilities::{FormatError, SpanStringTokenizer};
8use crate::{Point, Rect, Size};
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
12pub enum RelativeUnit {
13 #[default]
15 Relative,
16
17 Absolute,
19}
20
21#[derive(Clone, Copy, Debug, PartialEq, Default)]
23pub struct RelativePoint {
24 pub point: Point,
26 pub unit: RelativeUnit,
28}
29
30impl RelativePoint {
31 pub const TOP_LEFT: RelativePoint = RelativePoint::new(0.0, 0.0, RelativeUnit::Relative);
33
34 pub const CENTER: RelativePoint = RelativePoint::new(0.5, 0.5, RelativeUnit::Relative);
36
37 pub const BOTTOM_RIGHT: RelativePoint = RelativePoint::new(1.0, 1.0, RelativeUnit::Relative);
39
40 #[inline]
42 pub const fn new(x: f64, y: f64, unit: RelativeUnit) -> Self {
43 Self::from_point(Point::new(x, y), unit)
44 }
45
46 #[inline]
48 pub const fn from_point(point: Point, unit: RelativeUnit) -> Self {
49 Self { point, unit }
50 }
51
52 #[inline]
54 pub fn equals(&self, p: RelativePoint) -> bool {
55 self.unit == p.unit && self.point == p.point
56 }
57
58 #[inline]
61 pub fn to_pixels(&self, size: Size) -> Point {
62 if self.unit == RelativeUnit::Absolute {
63 self.point
64 } else {
65 Point::new(self.point.x * size.width, self.point.y * size.height)
66 }
67 }
68
69 #[inline]
72 pub fn to_pixels_rect(&self, rect: Rect) -> Point {
73 if self.unit == RelativeUnit::Absolute {
74 self.point
75 } else {
76 Point::new(
77 rect.x + self.point.x * rect.width,
78 rect.y + self.point.y * rect.height,
79 )
80 }
81 }
82
83 pub fn parse(s: &str) -> Result<Self, FormatError> {
85 SpanStringTokenizer::with_message(s, "Invalid RelativePoint.").scope(|t| {
86 let mut x = t.read_string()?;
87 let mut y = t.read_string()?;
88
89 let mut unit = RelativeUnit::Absolute;
90 let mut scale = 1.0;
91
92 if x.ends_with('%') {
93 if !y.ends_with('%') {
94 return Err(FormatError::new(
95 "If one coordinate is relative, both must be.",
96 ));
97 }
98
99 x = x.trim_end_matches('%');
100 y = y.trim_end_matches('%');
101 unit = RelativeUnit::Relative;
102 scale = 0.01;
103 }
104
105 let x = parse_double(x).ok_or_else(|| FormatError::invalid_input(x))?;
106 let y = parse_double(y).ok_or_else(|| FormatError::invalid_input(y))?;
107
108 Ok(RelativePoint::new(x * scale, y * scale, unit))
109 })
110 }
111}
112
113impl FromStr for RelativePoint {
114 type Err = FormatError;
115 #[inline]
116 fn from_str(s: &str) -> Result<Self, FormatError> {
117 RelativePoint::parse(s)
118 }
119}
120
121impl fmt::Display for RelativePoint {
122 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123 if self.unit == RelativeUnit::Absolute {
124 fmt::Display::fmt(&self.point, f)
125 } else {
126 write!(
127 f,
128 "{}%, {}%",
129 InvariantF64(self.point.x * 100.0),
130 InvariantF64(self.point.y * 100.0)
131 )
132 }
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn parse_should_accept_absolute_value() {
142 let result = RelativePoint::parse("4,5").unwrap();
143
144 assert_eq!(RelativePoint::new(4.0, 5.0, RelativeUnit::Absolute), result);
145 }
146
147 #[test]
148 fn parse_should_accept_relative_value() {
149 let result = RelativePoint::parse("25%, 50%").unwrap();
150
151 assert_eq!(
152 RelativePoint::new(0.25, 0.5, RelativeUnit::Relative),
153 result
154 );
155 }
156
157 #[test]
160 fn parse_errors_display_and_to_pixels() {
161 assert_eq!(
162 RelativePoint::parse("25%, 50").unwrap_err().message(),
163 "If one coordinate is relative, both must be."
164 );
165 assert!(RelativePoint::parse("25, 50%").is_err());
166 assert_eq!(
167 RelativePoint::parse("1").unwrap_err().message(),
168 "Invalid RelativePoint."
169 );
170 assert_eq!(
172 RelativePoint::parse("25%, 50, 3").unwrap_err().message(),
173 "Invalid RelativePoint."
174 );
175 assert_eq!(RelativePoint::CENTER.to_string(), "50%, 50%");
176 assert_eq!(
177 RelativePoint::new(4.0, 5.5, RelativeUnit::Absolute).to_string(),
178 "4, 5.5"
179 );
180 assert_eq!(
181 RelativePoint::CENTER.to_pixels(Size::new(100.0, 50.0)),
182 Point::new(50.0, 25.0)
183 );
184 assert_eq!(
185 RelativePoint::CENTER.to_pixels_rect(Rect::new(10.0, 10.0, 100.0, 50.0)),
186 Point::new(60.0, 35.0)
187 );
188 assert_eq!(
189 RelativePoint::new(4.0, 5.0, RelativeUnit::Absolute).to_pixels(Size::new(100.0, 50.0)),
190 Point::new(4.0, 5.0)
191 );
192 assert_eq!(RelativePoint::default(), RelativePoint::TOP_LEFT);
193 }
194}