1 | //! The rounded rectangle primitive. |
2 | |
3 | use core::ops::Range; |
4 | |
5 | use crate::{ |
6 | geometry::{Dimensions, Point, Size}, |
7 | primitives::{rectangle::Rectangle, ContainsPoint, OffsetOutline, PointsIter, Primitive}, |
8 | transform::Transform, |
9 | }; |
10 | |
11 | mod corner_radii; |
12 | mod ellipse_quadrant; |
13 | mod points; |
14 | mod styled; |
15 | |
16 | pub use corner_radii::{CornerRadii, CornerRadiiBuilder}; |
17 | use ellipse_quadrant::{EllipseQuadrant, Quadrant}; |
18 | pub use points::Points; |
19 | pub use styled::StyledPixelsIterator; |
20 | |
21 | /// Rounded rectangle primitive. |
22 | /// |
23 | /// Creates a rectangle with rounded corners. Corners can be circular or elliptical in shape, and |
24 | /// each corner may have a separate radius applied to it. To create a rounded rectangle with the same |
25 | /// radius for each corner, use the [`with_equal_corners`](RoundedRectangle::with_equal_corners()) method. |
26 | /// |
27 | /// Rounded rectangles with different radii for each corner can be created by passing a |
28 | /// [`CornerRadii`](super::CornerRadii) configuration struct to the [`new`](RoundedRectangle::new()) |
29 | /// method. |
30 | /// |
31 | /// # Overlapping corners |
32 | /// |
33 | /// It is possible to create a `RoundedRectangle` with corner radii too large to be contained within |
34 | /// its edges. When this happens, the corner radii will be confined to fit within the rounded |
35 | /// rectangle before use by other parts of embedded-graphics. |
36 | /// |
37 | /// This is similar but not identical to |
38 | /// [how the CSS specification works](https://www.w3.org/TR/css-backgrounds-3/#corner-overlap) as it |
39 | /// relies on floating point calculations. |
40 | /// |
41 | /// # Examples |
42 | /// |
43 | /// ## Create a uniform rounded rectangle |
44 | /// |
45 | /// This example creates a rounded rectangle 50px wide by 60px tall. Using |
46 | /// [`with_equal_corners`](RoundedRectangle::with_equal_corners()), all corners are given the same 10px circular |
47 | /// radius. The rectangle is drawn using a solid green fill with a 5px red stroke. |
48 | /// |
49 | /// ```rust |
50 | /// use embedded_graphics::{ |
51 | /// pixelcolor::Rgb565, |
52 | /// prelude::*, |
53 | /// primitives::{Rectangle, RoundedRectangle, PrimitiveStyle, PrimitiveStyleBuilder}, |
54 | /// }; |
55 | /// # use embedded_graphics::mock_display::MockDisplay; |
56 | /// # let mut display = MockDisplay::default(); |
57 | /// |
58 | /// let style = PrimitiveStyleBuilder::new() |
59 | /// .stroke_width(5) |
60 | /// .stroke_color(Rgb565::RED) |
61 | /// .fill_color(Rgb565::GREEN) |
62 | /// .build(); |
63 | /// |
64 | /// RoundedRectangle::with_equal_corners( |
65 | /// Rectangle::new(Point::new(5, 5), Size::new(40, 50)), |
66 | /// Size::new(10, 10), |
67 | /// ) |
68 | /// .into_styled(style) |
69 | /// .draw(&mut display)?; |
70 | /// # Ok::<(), core::convert::Infallible>(()) |
71 | /// ``` |
72 | /// |
73 | /// ## Different corner radii |
74 | /// |
75 | /// This example creates a rounded rectangle 50px wide by 60px tall. Each corner is given a distinct |
76 | /// radius in the x and y direction by creating a [`CornerRadii`](super::CornerRadii) |
77 | /// object and passing that to [`RoundedRectangle::new`](RoundedRectangle::new()). |
78 | /// |
79 | /// ```rust |
80 | /// use embedded_graphics::{ |
81 | /// pixelcolor::Rgb565, |
82 | /// prelude::*, |
83 | /// primitives::{CornerRadiiBuilder, Rectangle, RoundedRectangle, PrimitiveStyle, PrimitiveStyleBuilder}, |
84 | /// }; |
85 | /// # use embedded_graphics::mock_display::MockDisplay; |
86 | /// # let mut display = MockDisplay::default(); |
87 | /// |
88 | /// let style = PrimitiveStyleBuilder::new() |
89 | /// .stroke_width(5) |
90 | /// .stroke_color(Rgb565::RED) |
91 | /// .fill_color(Rgb565::GREEN) |
92 | /// .build(); |
93 | /// |
94 | /// let radii = CornerRadiiBuilder::new() |
95 | /// .top_left(Size::new(5, 6)) |
96 | /// .top_right(Size::new(7, 8)) |
97 | /// .bottom_right(Size::new(9, 10)) |
98 | /// .bottom_left(Size::new(11, 12)) |
99 | /// .build(); |
100 | /// |
101 | /// RoundedRectangle::new(Rectangle::new(Point::new(5, 5), Size::new(40, 50)), radii) |
102 | /// .into_styled(style) |
103 | /// .draw(&mut display)?; |
104 | /// # Ok::<(), core::convert::Infallible>(()) |
105 | /// ``` |
106 | /// |
107 | /// ## Using `CornerRadiiBuilder` |
108 | /// |
109 | /// This example creates a rounded rectangle 50px wide by 60px tall. Corner radii are set using the |
110 | /// [`CornerRadiiBuilder`](super::CornerRadiiBuilder) builder. |
111 | /// |
112 | /// ```rust |
113 | /// use embedded_graphics::{ |
114 | /// pixelcolor::Rgb565, |
115 | /// prelude::*, |
116 | /// primitives::{CornerRadii, CornerRadiiBuilder, Rectangle, RoundedRectangle, PrimitiveStyle, PrimitiveStyleBuilder}, |
117 | /// }; |
118 | /// # use embedded_graphics::mock_display::MockDisplay; |
119 | /// # let mut display = MockDisplay::default(); |
120 | /// |
121 | /// let style = PrimitiveStyleBuilder::new() |
122 | /// .stroke_width(5) |
123 | /// .stroke_color(Rgb565::RED) |
124 | /// .fill_color(Rgb565::GREEN) |
125 | /// .build(); |
126 | /// |
127 | /// let radii = CornerRadiiBuilder::new() |
128 | /// // Set the top left and top right corner radii to 10 x 20px |
129 | /// .top(Size::new(10, 20)) |
130 | /// // Set the bottom right corner radius to 5 x 8px |
131 | /// .bottom_right(Size::new(5, 8)) |
132 | /// .build(); |
133 | /// |
134 | /// RoundedRectangle::new(Rectangle::new(Point::new(5, 5), Size::new(40, 50)), radii) |
135 | /// .into_styled(style) |
136 | /// .draw(&mut display)?; |
137 | /// # Ok::<(), core::convert::Infallible>(()) |
138 | /// ``` |
139 | #[derive (Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] |
140 | #[cfg_attr (feature = "defmt" , derive(::defmt::Format))] |
141 | pub struct RoundedRectangle { |
142 | /// The base rectangle |
143 | pub rectangle: Rectangle, |
144 | |
145 | /// The radius of each corner |
146 | pub corners: CornerRadii, |
147 | } |
148 | |
149 | impl RoundedRectangle { |
150 | /// Creates a new rounded rectangle with the given corner radii. |
151 | /// |
152 | /// The size and position of the rounded rectangle is determined by the given base |
153 | /// rectangle. |
154 | pub const fn new(rectangle: Rectangle, corners: CornerRadii) -> Self { |
155 | Self { rectangle, corners } |
156 | } |
157 | |
158 | /// Creates a new rounded rectangle with equal corner radius for all corners. |
159 | /// |
160 | /// The size and position of the rounded rectangle is determined by the given base |
161 | /// rectangle. |
162 | pub const fn with_equal_corners(rectangle: Rectangle, corner_radius: Size) -> Self { |
163 | Self::new(rectangle, CornerRadii::new(corner_radius)) |
164 | } |
165 | |
166 | /// Return the rounded rectangle with confined corner radii. |
167 | /// |
168 | /// This method will return a rounded rectangle of the same width and height, but with all |
169 | /// corner radii confined to fit within its base rectangle. |
170 | /// |
171 | /// Calling this method is not necessary when using operations provided by embedded-graphics |
172 | /// (`.into_styled()`, `.contains()`, etc) as these confine the corner radii internally. |
173 | /// |
174 | /// # Examples |
175 | /// |
176 | /// ## Confine corner radii that are too large |
177 | /// |
178 | /// This example creates a rounded rectangle 50px x 60px in size. Each corner is set to an equal |
179 | /// radius of 40px x 40px. Each edge of the rectangle would thus need to be at least 80px long |
180 | /// to contain all corner radii completely. By using `confine_radii`, the corner radii are |
181 | /// reduced to 25px x 25px so that they fit within the 50px x 60px base rectangle. |
182 | /// |
183 | /// ```rust |
184 | /// use embedded_graphics::{ |
185 | /// geometry::{Point, Size}, |
186 | /// primitives::{CornerRadii, CornerRadiiBuilder, Rectangle, RoundedRectangle}, |
187 | /// }; |
188 | /// |
189 | /// let radii = CornerRadiiBuilder::new().all(Size::new(40, 40)).build(); |
190 | /// |
191 | /// let base_rectangle = Rectangle::new(Point::zero(), Size::new(50, 60)); |
192 | /// |
193 | /// let rounded_rectangle = RoundedRectangle::new(base_rectangle, radii); |
194 | /// |
195 | /// let confined = rounded_rectangle.confine_radii(); |
196 | /// |
197 | /// assert_eq!( |
198 | /// confined.corners, |
199 | /// CornerRadii { |
200 | /// top_left: Size::new(25, 25), |
201 | /// top_right: Size::new(25, 25), |
202 | /// bottom_right: Size::new(25, 25), |
203 | /// bottom_left: Size::new(25, 25), |
204 | /// } |
205 | /// ); |
206 | /// ``` |
207 | pub fn confine_radii(&self) -> Self { |
208 | Self::new(self.rectangle, self.corners.confine(self.rectangle.size)) |
209 | } |
210 | |
211 | fn get_confined_corner_quadrant(&self, quadrant: Quadrant) -> EllipseQuadrant { |
212 | let Self { |
213 | rectangle, corners, .. |
214 | } = self; |
215 | |
216 | let Rectangle { top_left, size, .. } = *rectangle; |
217 | |
218 | let corners = corners.confine(size); |
219 | |
220 | match quadrant { |
221 | Quadrant::TopLeft => { |
222 | EllipseQuadrant::new(top_left, corners.top_left, Quadrant::TopLeft) |
223 | } |
224 | Quadrant::TopRight => EllipseQuadrant::new( |
225 | top_left + size.x_axis() - corners.top_right.x_axis(), |
226 | corners.top_right, |
227 | Quadrant::TopRight, |
228 | ), |
229 | Quadrant::BottomRight => EllipseQuadrant::new( |
230 | top_left + size - corners.bottom_right, |
231 | corners.bottom_right, |
232 | Quadrant::BottomRight, |
233 | ), |
234 | Quadrant::BottomLeft => EllipseQuadrant::new( |
235 | top_left + size.y_axis() - corners.bottom_left.y_axis(), |
236 | corners.bottom_left, |
237 | Quadrant::BottomLeft, |
238 | ), |
239 | } |
240 | } |
241 | } |
242 | |
243 | impl OffsetOutline for RoundedRectangle { |
244 | fn offset(&self, offset: i32) -> Self { |
245 | let rectangle = self.rectangle.offset(offset); |
246 | |
247 | let corners = if offset >= 0 { |
248 | let corner_offset = Size::new_equal(offset as u32); |
249 | |
250 | CornerRadii { |
251 | top_left: self.corners.top_left.saturating_add(corner_offset), |
252 | top_right: self.corners.top_right.saturating_add(corner_offset), |
253 | bottom_right: self.corners.bottom_right.saturating_add(corner_offset), |
254 | bottom_left: self.corners.bottom_left.saturating_add(corner_offset), |
255 | } |
256 | } else { |
257 | let corner_offset = Size::new_equal((-offset) as u32); |
258 | |
259 | CornerRadii { |
260 | top_left: self.corners.top_left.saturating_sub(corner_offset), |
261 | top_right: self.corners.top_right.saturating_sub(corner_offset), |
262 | bottom_right: self.corners.bottom_right.saturating_sub(corner_offset), |
263 | bottom_left: self.corners.bottom_left.saturating_sub(corner_offset), |
264 | } |
265 | }; |
266 | |
267 | Self::new(rectangle, corners) |
268 | } |
269 | } |
270 | |
271 | impl Primitive for RoundedRectangle {} |
272 | |
273 | impl PointsIter for RoundedRectangle { |
274 | type Iter = Points; |
275 | |
276 | fn points(&self) -> Self::Iter { |
277 | Points::new(self) |
278 | } |
279 | } |
280 | |
281 | impl ContainsPoint for RoundedRectangle { |
282 | fn contains(&self, point: Point) -> bool { |
283 | let rounded_rectangle_contains: RoundedRectangleContains = RoundedRectangleContains::new(self); |
284 | rounded_rectangle_contains.contains(point) |
285 | } |
286 | } |
287 | |
288 | impl Dimensions for RoundedRectangle { |
289 | fn bounding_box(&self) -> Rectangle { |
290 | self.rectangle |
291 | } |
292 | } |
293 | |
294 | impl Transform for RoundedRectangle { |
295 | /// Translate the rounded rectangle from its current position to a new position by (x, y) |
296 | /// pixels, returning a new `RoundedRectangle`. For a mutating transform, see `translate_mut`. |
297 | /// |
298 | /// ``` |
299 | /// # use embedded_graphics::prelude::*; |
300 | /// use embedded_graphics::primitives::{Rectangle, RoundedRectangle}; |
301 | /// |
302 | /// let original = RoundedRectangle::with_equal_corners( |
303 | /// Rectangle::new(Point::new(5, 10), Size::new(20, 30)), |
304 | /// Size::new(10, 15), |
305 | /// ); |
306 | /// let moved = original.translate(Point::new(10, 12)); |
307 | /// |
308 | /// assert_eq!(original.bounding_box().top_left, Point::new(5, 10)); |
309 | /// assert_eq!(moved.bounding_box().top_left, Point::new(15, 22)); |
310 | /// ``` |
311 | fn translate(&self, by: Point) -> Self { |
312 | Self { |
313 | rectangle: self.rectangle.translate(by), |
314 | ..*self |
315 | } |
316 | } |
317 | |
318 | /// Translate the rounded rectangle from its current position to a new position by (x, y) pixels. |
319 | /// |
320 | /// ``` |
321 | /// # use embedded_graphics::prelude::*; |
322 | /// use embedded_graphics::primitives::{Rectangle, RoundedRectangle}; |
323 | /// |
324 | /// let mut shape = RoundedRectangle::with_equal_corners( |
325 | /// Rectangle::new(Point::new(5, 10), Size::new(20, 30)), |
326 | /// Size::new(10, 15), |
327 | /// ); |
328 | /// |
329 | /// shape.translate_mut(Point::new(10, 12)); |
330 | /// |
331 | /// assert_eq!(shape.bounding_box().top_left, Point::new(15, 22)); |
332 | /// ``` |
333 | fn translate_mut(&mut self, by: Point) -> &mut Self { |
334 | self.rectangle.translate_mut(by); |
335 | |
336 | self |
337 | } |
338 | } |
339 | |
340 | #[derive (Clone, PartialEq, Eq, Hash, Debug)] |
341 | #[cfg_attr (feature = "defmt" , derive(::defmt::Format))] |
342 | pub(in crate::primitives) struct RoundedRectangleContains { |
343 | /// Bounding box rows. |
344 | rows: Range<i32>, |
345 | /// Bounding box columns. |
346 | columns: Range<i32>, |
347 | |
348 | /// Rows that don't belong to a corner radius on the left side. |
349 | straight_rows_left: Range<i32>, |
350 | /// Rows that don't belong to a corner radius on the right side. |
351 | straight_rows_right: Range<i32>, |
352 | |
353 | /// Confined top left corner ellipse. |
354 | top_left: EllipseQuadrant, |
355 | /// Confined top right corner ellipse. |
356 | top_right: EllipseQuadrant, |
357 | /// Confined bottom left corner ellipse. |
358 | bottom_left: EllipseQuadrant, |
359 | /// Confined bottom right corner ellipse. |
360 | bottom_right: EllipseQuadrant, |
361 | } |
362 | |
363 | impl RoundedRectangleContains { |
364 | pub fn new(rounded_rectangle: &RoundedRectangle) -> Self { |
365 | let top_left = rounded_rectangle.get_confined_corner_quadrant(Quadrant::TopLeft); |
366 | let top_right = rounded_rectangle.get_confined_corner_quadrant(Quadrant::TopRight); |
367 | let bottom_left = rounded_rectangle.get_confined_corner_quadrant(Quadrant::BottomLeft); |
368 | let bottom_right = rounded_rectangle.get_confined_corner_quadrant(Quadrant::BottomRight); |
369 | |
370 | let rows = rounded_rectangle.rectangle.rows(); |
371 | let columns = rounded_rectangle.rectangle.columns(); |
372 | |
373 | let straight_rows_left = (rows.start + top_left.bounding_box().size.height as i32) |
374 | ..(rows.end - bottom_left.bounding_box().size.height as i32); |
375 | let straight_rows_right = (rows.start + top_right.bounding_box().size.height as i32) |
376 | ..(rows.end - bottom_right.bounding_box().size.height as i32); |
377 | |
378 | Self { |
379 | rows, |
380 | columns, |
381 | |
382 | straight_rows_left, |
383 | straight_rows_right, |
384 | |
385 | top_left, |
386 | top_right, |
387 | bottom_left, |
388 | bottom_right, |
389 | } |
390 | } |
391 | |
392 | pub fn contains(&self, point: Point) -> bool { |
393 | if !(self.rows.contains(&point.y) && self.columns.contains(&point.x)) { |
394 | return false; |
395 | } |
396 | |
397 | if point.y < self.straight_rows_left.start |
398 | && point.x < self.top_left.bounding_box().columns().end |
399 | { |
400 | return self.top_left.contains(point); |
401 | } |
402 | |
403 | if point.y < self.straight_rows_right.start |
404 | && point.x >= self.top_right.bounding_box().columns().start |
405 | { |
406 | return self.top_right.contains(point); |
407 | } |
408 | |
409 | if point.y >= self.straight_rows_left.end |
410 | && point.x < self.bottom_left.bounding_box().columns().end |
411 | { |
412 | return self.bottom_left.contains(point); |
413 | } |
414 | |
415 | if point.y >= self.straight_rows_right.end |
416 | && point.x >= self.bottom_right.bounding_box().columns().start |
417 | { |
418 | return self.bottom_right.contains(point); |
419 | } |
420 | |
421 | true |
422 | } |
423 | } |
424 | |
425 | #[cfg (test)] |
426 | mod tests { |
427 | use super::*; |
428 | use crate::{ |
429 | geometry::{Point, Size}, |
430 | mock_display::MockDisplay, |
431 | pixelcolor::BinaryColor, |
432 | primitives::CornerRadiiBuilder, |
433 | }; |
434 | |
435 | #[test ] |
436 | fn clamp_radius_at_rect_size() { |
437 | let clamped = RoundedRectangle::with_equal_corners( |
438 | Rectangle::new(Point::zero(), Size::new(20, 30)), |
439 | Size::new_equal(50), |
440 | ) |
441 | .points(); |
442 | |
443 | let expected = RoundedRectangle::with_equal_corners( |
444 | Rectangle::new(Point::zero(), Size::new(20, 30)), |
445 | Size::new_equal(10), |
446 | ) |
447 | .points(); |
448 | |
449 | assert!(clamped.eq(expected)); |
450 | } |
451 | |
452 | #[test ] |
453 | fn large_bottom_right_corner() { |
454 | let radii = CornerRadiiBuilder::new() |
455 | .all(Size::new_equal(20)) |
456 | .bottom_right(Size::new(200, 200)) |
457 | .build(); |
458 | |
459 | let base_rectangle = Rectangle::with_corners(Point::new_equal(20), Point::new_equal(100)); |
460 | |
461 | let rounded_rectangle = RoundedRectangle::new(base_rectangle, radii); |
462 | |
463 | let confined = rounded_rectangle.confine_radii(); |
464 | |
465 | assert_eq!( |
466 | confined, |
467 | RoundedRectangle { |
468 | rectangle: base_rectangle, |
469 | corners: CornerRadii { |
470 | top_left: Size::new_equal(7), |
471 | top_right: Size::new_equal(7), |
472 | bottom_right: Size::new_equal(73), |
473 | bottom_left: Size::new_equal(7), |
474 | } |
475 | } |
476 | ); |
477 | } |
478 | |
479 | #[test ] |
480 | fn offset() { |
481 | let center = Point::new(10, 20); |
482 | let rect = Rectangle::with_center(center, Size::new(3, 4)); |
483 | let rounded = RoundedRectangle::with_equal_corners(rect, Size::new(2, 3)); |
484 | |
485 | assert_eq!(rounded.offset(0), rounded); |
486 | |
487 | assert_eq!( |
488 | rounded.offset(1), |
489 | RoundedRectangle::with_equal_corners( |
490 | Rectangle::with_center(center, Size::new(5, 6)), |
491 | Size::new(3, 4) |
492 | ), |
493 | ); |
494 | assert_eq!( |
495 | rounded.offset(2), |
496 | RoundedRectangle::with_equal_corners( |
497 | Rectangle::with_center(center, Size::new(7, 8)), |
498 | Size::new(4, 5) |
499 | ), |
500 | ); |
501 | |
502 | assert_eq!( |
503 | rounded.offset(-1), |
504 | RoundedRectangle::with_equal_corners( |
505 | Rectangle::with_center(center, Size::new(1, 2)), |
506 | Size::new(1, 2) |
507 | ), |
508 | ); |
509 | assert_eq!( |
510 | rounded.offset(-2), |
511 | RoundedRectangle::with_equal_corners( |
512 | Rectangle::with_center(center, Size::new(0, 0)), |
513 | Size::new(0, 1) |
514 | ), |
515 | ); |
516 | assert_eq!( |
517 | rounded.offset(-3), |
518 | RoundedRectangle::with_equal_corners( |
519 | Rectangle::with_center(center, Size::new(0, 0)), |
520 | Size::new(0, 0) |
521 | ), |
522 | ); |
523 | } |
524 | |
525 | #[test ] |
526 | fn contains_equal_corners() { |
527 | let rounded_rectangle = RoundedRectangle::with_equal_corners( |
528 | Rectangle::new(Point::new(1, 2), Size::new(20, 10)), |
529 | Size::new(8, 4), |
530 | ); |
531 | |
532 | let expected = MockDisplay::from_points(rounded_rectangle.points(), BinaryColor::On); |
533 | |
534 | let display = MockDisplay::from_points( |
535 | rounded_rectangle |
536 | .rectangle |
537 | .offset(10) |
538 | .points() |
539 | .filter(|p| rounded_rectangle.contains(*p)), |
540 | BinaryColor::On, |
541 | ); |
542 | display.assert_eq(&expected); |
543 | } |
544 | |
545 | #[test ] |
546 | fn contains_different_corners() { |
547 | let rounded_rectangle = RoundedRectangle::new( |
548 | Rectangle::new(Point::new(1, 2), Size::new(25, 10)), |
549 | CornerRadiiBuilder::new() |
550 | .top_left(Size::new_equal(10)) |
551 | .bottom_right(Size::new_equal(10)) |
552 | .build(), |
553 | ); |
554 | |
555 | let expected = MockDisplay::from_points(rounded_rectangle.points(), BinaryColor::On); |
556 | |
557 | let display = MockDisplay::from_points( |
558 | rounded_rectangle |
559 | .rectangle |
560 | .offset(10) |
561 | .points() |
562 | .filter(|p| rounded_rectangle.contains(*p)), |
563 | BinaryColor::On, |
564 | ); |
565 | display.assert_eq(&expected); |
566 | } |
567 | } |
568 | |