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}