Skip to main content

ferroui_base/
relative_point.rs

1//! Defines a point that may be defined relative to a containing element.
2
3use 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/// Defines the reference point units of a [`RelativePoint`] or `RelativeRect`.
11#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
12pub enum RelativeUnit {
13    /// The point is expressed as a fraction of the containing element's size.
14    #[default]
15    Relative,
16
17    /// The point is absolute (i.e. in pixels).
18    Absolute,
19}
20
21/// Defines a point that may be defined relative to a containing element.
22#[derive(Clone, Copy, Debug, PartialEq, Default)]
23pub struct RelativePoint {
24    /// The point.
25    pub point: Point,
26    /// The unit of the point.
27    pub unit: RelativeUnit,
28}
29
30impl RelativePoint {
31    /// A point at the top left of the containing element.
32    pub const TOP_LEFT: RelativePoint = RelativePoint::new(0.0, 0.0, RelativeUnit::Relative);
33
34    /// A point at the center of the containing element.
35    pub const CENTER: RelativePoint = RelativePoint::new(0.5, 0.5, RelativeUnit::Relative);
36
37    /// A point at the bottom right of the containing element.
38    pub const BOTTOM_RIGHT: RelativePoint = RelativePoint::new(1.0, 1.0, RelativeUnit::Relative);
39
40    /// Initializes a new instance of the [`RelativePoint`] structure.
41    #[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    /// Initializes a new instance of the [`RelativePoint`] structure from a [`Point`].
47    #[inline]
48    pub const fn from_point(point: Point, unit: RelativeUnit) -> Self {
49        Self { point, unit }
50    }
51
52    /// Checks if the [`RelativePoint`] equals another point.
53    #[inline]
54    pub fn equals(&self, p: RelativePoint) -> bool {
55        self.unit == p.unit && self.point == p.point
56    }
57
58    /// Converts a [`RelativePoint`] into pixels, using `size` as the size of the
59    /// visual the point is relative to.
60    #[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    /// Converts a [`RelativePoint`] into pixels, using `rect` as the bounding
70    /// rectangle the point is relative to.
71    #[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    /// Parses a [`RelativePoint`] string: `"x, y"` (absolute) or `"x%, y%"` (relative).
84    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    // Additional coverage (not part of the reference suite).
158
159    #[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        // Unconsumed input wins over the inner error.
171        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}