1use crate::decoration::BorderRadius;
23use crate::geometry::{Point, Rect};
24use crate::path::Path;
25use crate::Color;
26
27#[derive(Copy, Clone, Debug, PartialEq)]
30pub struct BorderSide {
31 pub color: Color,
33 pub width: f32,
35}
36
37impl BorderSide {
38 pub const NONE: Self = Self {
42 color: Color::TRANSPARENT,
43 width: 0.0,
44 };
45
46 pub const fn new(color: Color, width: f32) -> Self {
48 Self { color, width }
49 }
50
51 pub fn is_drawn(&self) -> bool {
53 self.width > 0.0 && self.color.a > 0.0
54 }
55}
56
57impl Default for BorderSide {
58 fn default() -> Self {
59 Self::NONE
60 }
61}
62
63#[derive(Copy, Clone, Debug, PartialEq)]
69pub enum ShapeBorder {
70 RoundedRectangle {
72 radius: BorderRadius,
74 side: BorderSide,
76 },
77 Stadium {
80 side: BorderSide,
82 },
83 Circle {
86 eccentricity: f32,
90 side: BorderSide,
92 },
93 Beveled {
97 radius: BorderRadius,
99 side: BorderSide,
101 },
102}
103
104impl Default for ShapeBorder {
105 fn default() -> Self {
106 Self::RoundedRectangle {
107 radius: BorderRadius::ZERO,
108 side: BorderSide::NONE,
109 }
110 }
111}
112
113impl ShapeBorder {
114 pub fn rounded(radius: impl Into<BorderRadius>) -> Self {
116 Self::RoundedRectangle {
117 radius: radius.into(),
118 side: BorderSide::NONE,
119 }
120 }
121
122 pub const fn stadium() -> Self {
124 Self::Stadium {
125 side: BorderSide::NONE,
126 }
127 }
128
129 pub const fn circle() -> Self {
131 Self::Circle {
132 eccentricity: 0.0,
133 side: BorderSide::NONE,
134 }
135 }
136
137 pub fn beveled(radius: impl Into<BorderRadius>) -> Self {
139 Self::Beveled {
140 radius: radius.into(),
141 side: BorderSide::NONE,
142 }
143 }
144
145 #[must_use]
147 pub fn with_side(self, side: BorderSide) -> Self {
148 match self {
149 Self::RoundedRectangle { radius, .. } => Self::RoundedRectangle { radius, side },
150 Self::Stadium { .. } => Self::Stadium { side },
151 Self::Circle { eccentricity, .. } => Self::Circle { eccentricity, side },
152 Self::Beveled { radius, .. } => Self::Beveled { radius, side },
153 }
154 }
155
156 pub fn side(&self) -> BorderSide {
158 match *self {
159 Self::RoundedRectangle { side, .. }
160 | Self::Stadium { side }
161 | Self::Circle { side, .. }
162 | Self::Beveled { side, .. } => side,
163 }
164 }
165
166 pub fn as_rounded(&self, rect: Rect) -> Option<(Rect, BorderRadius)> {
175 match *self {
176 Self::RoundedRectangle { radius, .. } => Some((rect, radius)),
177 Self::Stadium { .. } => Some((rect, BorderRadius::uniform(shortest(rect) * 0.5))),
178 Self::Circle { eccentricity, .. } => {
179 let box_ = circle_box(rect, eccentricity);
180 Some((box_, BorderRadius::uniform(shortest(box_) * 0.5)))
181 }
182 Self::Beveled { .. } => None,
183 }
184 }
185
186 pub fn outline(&self, rect: Rect) -> Path {
189 match *self {
190 Self::Beveled { radius, .. } => beveled_path(rect, radius),
191 _ => {
192 let (box_, radius) = self
193 .as_rounded(rect)
194 .expect("every shape but a bevel is a rounded rectangle");
195 rounded_path(box_, radius)
196 }
197 }
198 }
199}
200
201fn shortest(rect: Rect) -> f32 {
203 rect.width.min(rect.height).max(0.0)
204}
205
206fn circle_box(rect: Rect, eccentricity: f32) -> Rect {
210 let e = eccentricity.clamp(0.0, 1.0);
211 if rect.width == rect.height {
212 return rect;
213 }
214 if rect.width < rect.height {
215 let delta = (1.0 - e) * (rect.height - rect.width) * 0.5;
216 Rect::new(
217 rect.x,
218 rect.y + delta,
219 rect.width,
220 rect.height - delta * 2.0,
221 )
222 } else {
223 let delta = (1.0 - e) * (rect.width - rect.height) * 0.5;
224 Rect::new(
225 rect.x + delta,
226 rect.y,
227 rect.width - delta * 2.0,
228 rect.height,
229 )
230 }
231}
232
233fn fitted(rect: Rect, radius: BorderRadius) -> [f32; 4] {
235 let cap = shortest(rect) * 0.5;
236 [
237 radius.top_left.clamp(0.0, cap),
238 radius.top_right.clamp(0.0, cap),
239 radius.bottom_right.clamp(0.0, cap),
240 radius.bottom_left.clamp(0.0, cap),
241 ]
242}
243
244fn rounded_path(rect: Rect, radius: BorderRadius) -> Path {
246 let [tl, tr, br, bl] = fitted(rect, radius);
247 let (l, t) = (rect.x, rect.y);
248 let (r, b) = (rect.x + rect.width, rect.y + rect.height);
249 const HALF_PI: f32 = std::f32::consts::FRAC_PI_2;
250 Path::new()
251 .move_to(Point::new(l + tl, t))
252 .line_to(Point::new(r - tr, t))
253 .arc_to(Point::new(r - tr, t + tr), tr, -HALF_PI, 0.0)
254 .line_to(Point::new(r, b - br))
255 .arc_to(Point::new(r - br, b - br), br, 0.0, HALF_PI)
256 .line_to(Point::new(l + bl, b))
257 .arc_to(
258 Point::new(l + bl, b - bl),
259 bl,
260 HALF_PI,
261 std::f32::consts::PI,
262 )
263 .line_to(Point::new(l, t + tl))
264 .arc_to(
265 Point::new(l + tl, t + tl),
266 tl,
267 std::f32::consts::PI,
268 std::f32::consts::PI + HALF_PI,
269 )
270 .close()
271}
272
273fn beveled_path(rect: Rect, radius: BorderRadius) -> Path {
275 let [tl, tr, br, bl] = fitted(rect, radius);
276 let (l, t) = (rect.x, rect.y);
277 let (r, b) = (rect.x + rect.width, rect.y + rect.height);
278 Path::new()
279 .move_to(Point::new(l + tl, t))
280 .line_to(Point::new(r - tr, t))
281 .line_to(Point::new(r, t + tr))
282 .line_to(Point::new(r, b - br))
283 .line_to(Point::new(r - br, b))
284 .line_to(Point::new(l + bl, b))
285 .line_to(Point::new(l, b - bl))
286 .line_to(Point::new(l, t + tl))
287 .close()
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293
294 #[test]
298 fn a_stadium_is_a_pill_at_any_size() {
299 let wide = Rect::new(0.0, 0.0, 200.0, 32.0);
300 let (box_, radius) = ShapeBorder::stadium().as_rounded(wide).unwrap();
301 assert_eq!(box_, wide, "it fills the box it is given");
302 assert_eq!(radius, BorderRadius::uniform(16.0));
303
304 let tall = Rect::new(0.0, 0.0, 32.0, 200.0);
305 let (_, radius) = ShapeBorder::stadium().as_rounded(tall).unwrap();
306 assert_eq!(
307 radius,
308 BorderRadius::uniform(16.0),
309 "the *short* side, either way"
310 );
311 }
312
313 #[test]
317 fn a_circle_takes_a_square_out_of_the_middle() {
318 let wide = Rect::new(10.0, 0.0, 100.0, 40.0);
319 let (box_, radius) = ShapeBorder::circle().as_rounded(wide).unwrap();
320 assert_eq!(box_, Rect::new(40.0, 0.0, 40.0, 40.0), "centred and square");
321 assert_eq!(radius, BorderRadius::uniform(20.0));
322
323 let full = ShapeBorder::Circle {
324 eccentricity: 1.0,
325 side: BorderSide::NONE,
326 };
327 assert_eq!(
328 full.as_rounded(wide).unwrap().0,
329 wide,
330 "an eccentricity of one fills the box"
331 );
332
333 let half = ShapeBorder::Circle {
334 eccentricity: 0.5,
335 side: BorderSide::NONE,
336 };
337 assert_eq!(
338 half.as_rounded(wide).unwrap().0,
339 Rect::new(25.0, 0.0, 70.0, 40.0),
340 "and half way is half way"
341 );
342 }
343
344 #[test]
348 fn a_bevel_is_the_one_that_needs_a_path() {
349 let rect = Rect::new(0.0, 0.0, 100.0, 40.0);
350 assert!(ShapeBorder::beveled(8.0).as_rounded(rect).is_none());
351 assert!(ShapeBorder::rounded(8.0).as_rounded(rect).is_some());
352 assert!(ShapeBorder::stadium().as_rounded(rect).is_some());
353
354 for shape in [
356 ShapeBorder::beveled(8.0),
357 ShapeBorder::rounded(8.0),
358 ShapeBorder::stadium(),
359 ShapeBorder::circle(),
360 ] {
361 assert!(
362 !shape.outline(rect).verbs().is_empty(),
363 "{shape:?} has no outline"
364 );
365 }
366 }
367
368 #[test]
371 fn a_corner_is_capped_by_the_box() {
372 let rect = Rect::new(0.0, 0.0, 20.0, 10.0);
373 assert_eq!(fitted(rect, BorderRadius::uniform(100.0)), [5.0; 4]);
374 assert_eq!(fitted(rect, BorderRadius::uniform(-4.0)), [0.0; 4]);
375 }
376
377 #[test]
379 fn an_edge_is_drawn_only_when_there_is_one() {
380 assert!(!BorderSide::NONE.is_drawn());
381 assert!(!BorderSide::new(Color::rgb8(255, 0, 0), 0.0).is_drawn());
382 assert!(!BorderSide::new(Color::TRANSPARENT, 2.0).is_drawn());
383 assert!(BorderSide::new(Color::rgb8(255, 0, 0), 1.0).is_drawn());
384 assert_eq!(
385 ShapeBorder::stadium()
386 .with_side(BorderSide::new(Color::rgb8(255, 0, 0), 2.0))
387 .side()
388 .width,
389 2.0
390 );
391 }
392}