embedded_graphics/primitives/rounded_rectangle/mod.rs
1//! The rounded rectangle primitive.
2
3use core::ops::Range;
4
5use crate::{
6 geometry::{Dimensions, Point, Size},
7 primitives::{rectangle::Rectangle, ContainsPoint, OffsetOutline, PointsIter, Primitive},
8 transform::Transform,
9};
10
11mod corner_radii;
12mod ellipse_quadrant;
13mod points;
14mod styled;
15
16pub use corner_radii::{CornerRadii, CornerRadiiBuilder};
17use ellipse_quadrant::{EllipseQuadrant, Quadrant};
18pub use points::Points;
19pub 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))]
141pub struct RoundedRectangle {
142 /// The base rectangle
143 pub rectangle: Rectangle,
144
145 /// The radius of each corner
146 pub corners: CornerRadii,
147}
148
149impl 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
243impl 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
271impl Primitive for RoundedRectangle {}
272
273impl PointsIter for RoundedRectangle {
274 type Iter = Points;
275
276 fn points(&self) -> Self::Iter {
277 Points::new(self)
278 }
279}
280
281impl ContainsPoint for RoundedRectangle {
282 fn contains(&self, point: Point) -> bool {
283 let rounded_rectangle_contains = RoundedRectangleContains::new(self);
284 rounded_rectangle_contains.contains(point)
285 }
286}
287
288impl Dimensions for RoundedRectangle {
289 fn bounding_box(&self) -> Rectangle {
290 self.rectangle
291 }
292}
293
294impl 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))]
342pub(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
363impl 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)]
426mod 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}