kozan_primitives/
rounded_rect.rs1use crate::geometry::{Corners, Point, Rect, Size};
2
3#[derive(Clone, Copy, Debug, PartialEq)]
12pub struct RoundedRect {
13 pub rect: Rect,
14 pub radii: Corners<Size>,
15}
16
17impl RoundedRect {
18 #[must_use]
19 pub fn new(rect: Rect, radii: Corners<Size>) -> Self {
20 Self { rect, radii }
21 }
22
23 #[must_use]
25 pub fn from_rect(rect: Rect) -> Self {
26 Self {
27 rect,
28 radii: Corners::all(Size::ZERO),
29 }
30 }
31
32 #[must_use]
34 pub fn uniform(rect: Rect, radius: f32) -> Self {
35 Self {
36 rect,
37 radii: Corners::all(Size::new(radius, radius)),
38 }
39 }
40
41 #[must_use]
43 pub fn is_sharp(&self) -> bool {
44 self.radii.top_left == Size::ZERO
45 && self.radii.top_right == Size::ZERO
46 && self.radii.bottom_right == Size::ZERO
47 && self.radii.bottom_left == Size::ZERO
48 }
49
50 #[must_use]
53 pub fn is_ellipse(&self) -> bool {
54 let hw = self.rect.width() * 0.5;
55 let hh = self.rect.height() * 0.5;
56 let expected = Size::new(hw, hh);
57 self.radii.top_left == expected
58 && self.radii.top_right == expected
59 && self.radii.bottom_right == expected
60 && self.radii.bottom_left == expected
61 }
62
63 #[must_use]
67 pub fn normalized(mut self) -> Self {
68 let w = self.rect.width();
69 let h = self.rect.height();
70
71 if w <= 0.0 || h <= 0.0 {
72 return Self::from_rect(self.rect);
73 }
74
75 let mut scale = 1.0f32;
79
80 let top_sum = self.radii.top_left.width + self.radii.top_right.width;
82 if top_sum > w {
83 scale = scale.min(w / top_sum);
84 }
85
86 let bottom_sum = self.radii.bottom_left.width + self.radii.bottom_right.width;
88 if bottom_sum > w {
89 scale = scale.min(w / bottom_sum);
90 }
91
92 let left_sum = self.radii.top_left.height + self.radii.bottom_left.height;
94 if left_sum > h {
95 scale = scale.min(h / left_sum);
96 }
97
98 let right_sum = self.radii.top_right.height + self.radii.bottom_right.height;
100 if right_sum > h {
101 scale = scale.min(h / right_sum);
102 }
103
104 if scale < 1.0 {
105 self.radii.top_left = scale_size(self.radii.top_left, scale);
106 self.radii.top_right = scale_size(self.radii.top_right, scale);
107 self.radii.bottom_right = scale_size(self.radii.bottom_right, scale);
108 self.radii.bottom_left = scale_size(self.radii.bottom_left, scale);
109 }
110
111 self
112 }
113
114 #[must_use]
119 pub fn contains(&self, point: Point) -> bool {
120 if !self.rect.contains_point(point) {
121 return false;
122 }
123
124 let norm = self.normalized();
125
126 let x = point.x;
128 let y = point.y;
129 let left = norm.rect.left();
130 let top = norm.rect.top();
131 let right = norm.rect.right();
132 let bottom = norm.rect.bottom();
133
134 let r = norm.radii.top_left;
136 if x < left + r.width
137 && y < top + r.height
138 && !point_in_ellipse(
139 x - (left + r.width),
140 y - (top + r.height),
141 r.width,
142 r.height,
143 )
144 {
145 return false;
146 }
147
148 let r = norm.radii.top_right;
150 if x > right - r.width
151 && y < top + r.height
152 && !point_in_ellipse(
153 x - (right - r.width),
154 y - (top + r.height),
155 r.width,
156 r.height,
157 )
158 {
159 return false;
160 }
161
162 let r = norm.radii.bottom_right;
164 if x > right - r.width
165 && y > bottom - r.height
166 && !point_in_ellipse(
167 x - (right - r.width),
168 y - (bottom - r.height),
169 r.width,
170 r.height,
171 )
172 {
173 return false;
174 }
175
176 let r = norm.radii.bottom_left;
178 if x < left + r.width
179 && y > bottom - r.height
180 && !point_in_ellipse(
181 x - (left + r.width),
182 y - (bottom - r.height),
183 r.width,
184 r.height,
185 )
186 {
187 return false;
188 }
189
190 true
191 }
192}
193
194fn scale_size(s: Size, factor: f32) -> Size {
195 Size::new(s.width * factor, s.height * factor)
196}
197
198fn point_in_ellipse(dx: f32, dy: f32, rx: f32, ry: f32) -> bool {
201 if rx <= 0.0 || ry <= 0.0 {
202 return true;
203 }
204 let nx = dx / rx;
205 let ny = dy / ry;
206 nx * nx + ny * ny <= 1.0
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 fn square() -> Rect {
214 Rect::new(0.0, 0.0, 100.0, 100.0)
215 }
216
217 #[test]
218 fn sharp_rect() {
219 let rr = RoundedRect::from_rect(square());
220 assert!(rr.is_sharp());
221 assert!(!rr.is_ellipse());
222 }
223
224 #[test]
225 fn uniform_radius() {
226 let rr = RoundedRect::uniform(square(), 10.0);
227 assert!(!rr.is_sharp());
228 assert!(!rr.is_ellipse());
229 }
230
231 #[test]
232 fn full_ellipse() {
233 let rr = RoundedRect::uniform(square(), 50.0);
234 assert!(rr.is_ellipse());
235 }
236
237 #[test]
238 fn contains_center() {
239 let rr = RoundedRect::uniform(square(), 20.0);
240 assert!(rr.contains(Point::new(50.0, 50.0)));
241 }
242
243 #[test]
244 fn excludes_outside() {
245 let rr = RoundedRect::uniform(square(), 20.0);
246 assert!(!rr.contains(Point::new(-1.0, 50.0)));
247 assert!(!rr.contains(Point::new(101.0, 50.0)));
248 }
249
250 #[test]
251 fn excludes_rounded_corner() {
252 let rr = RoundedRect::uniform(square(), 20.0);
255 assert!(!rr.contains(Point::new(1.0, 1.0)));
256 }
257
258 #[test]
259 fn includes_just_inside_corner() {
260 let rr = RoundedRect::uniform(square(), 20.0);
261 assert!(rr.contains(Point::new(10.0, 10.0)));
264 }
265
266 #[test]
267 fn normalize_scales_overlapping_radii() {
268 let rr = RoundedRect::uniform(square(), 100.0);
270 let norm = rr.normalized();
271 assert!((norm.radii.top_left.width - 50.0).abs() < 0.01);
273 }
274
275 #[test]
276 fn sharp_rect_contains_like_rect() {
277 let rr = RoundedRect::from_rect(square());
278 assert!(rr.contains(Point::new(0.0, 0.0)));
279 assert!(rr.contains(Point::new(99.0, 99.0)));
280 assert!(!rr.contains(Point::new(100.0, 100.0)));
281 }
282}