embedded_graphics/primitives/rounded_rectangle/
styled.rs

1use crate::{
2    draw_target::DrawTarget,
3    geometry::{Dimensions, Point},
4    pixelcolor::PixelColor,
5    primitives::{
6        common::{Scanline, StyledScanline},
7        rounded_rectangle::{points::Scanlines, RoundedRectangle},
8        styled::{StyledDimensions, StyledDrawable, StyledPixels},
9        PrimitiveStyle, Rectangle,
10    },
11    Pixel,
12};
13use az::SaturatingAs;
14
15use super::RoundedRectangleContains;
16
17/// Pixel iterator for each pixel in the rect border
18#[derive(Clone, Eq, PartialEq, Hash, Debug)]
19#[cfg_attr(feature = "defmt", derive(::defmt::Format))]
20pub struct StyledPixelsIterator<C> {
21    styled_scanlines: StyledScanlines,
22
23    stroke_left: Scanline,
24    fill: Scanline,
25    stroke_right: Scanline,
26
27    stroke_color: Option<C>,
28    fill_color: Option<C>,
29}
30
31impl<C: PixelColor> StyledPixelsIterator<C> {
32    pub(in crate::primitives) fn new(
33        primitive: &RoundedRectangle,
34        style: &PrimitiveStyle<C>,
35    ) -> Self {
36        let stroke_area = style.stroke_area(primitive);
37        let fill_area = style.fill_area(primitive);
38
39        Self {
40            styled_scanlines: StyledScanlines::new(&stroke_area, &fill_area),
41            stroke_left: Scanline::new_empty(0),
42            fill: Scanline::new_empty(0),
43            stroke_right: Scanline::new_empty(0),
44            stroke_color: style.stroke_color,
45            fill_color: style.fill_color,
46        }
47    }
48}
49
50impl<C: PixelColor> Iterator for StyledPixelsIterator<C> {
51    type Item = Pixel<C>;
52
53    fn next(&mut self) -> Option<Self::Item> {
54        match (self.stroke_color, self.fill_color) {
55            (Some(stroke_color), None) => loop {
56                if let Some(pixel) = self
57                    .stroke_left
58                    .next()
59                    .or_else(|| self.stroke_right.next())
60                    .map(|p| Pixel(p, stroke_color))
61                {
62                    return Some(pixel);
63                }
64
65                let scanline = self.styled_scanlines.next()?;
66                self.stroke_left = scanline.stroke_left();
67                self.stroke_right = scanline.stroke_right();
68            },
69            (Some(stroke_color), Some(fill_color)) => loop {
70                if let Some(pixel) = self
71                    .stroke_left
72                    .next()
73                    .map(|p| Pixel(p, stroke_color))
74                    .or_else(|| self.fill.next().map(|p| Pixel(p, fill_color)))
75                    .or_else(|| self.stroke_right.next().map(|p| Pixel(p, stroke_color)))
76                {
77                    return Some(pixel);
78                }
79
80                let scanline = self.styled_scanlines.next()?;
81                self.stroke_left = scanline.stroke_left();
82                self.fill = scanline.fill();
83                self.stroke_right = scanline.stroke_right();
84            },
85            (None, Some(fill_color)) => loop {
86                if let Some(pixel) = self.fill.next().map(|p| Pixel(p, fill_color)) {
87                    return Some(pixel);
88                }
89
90                let scanline = self.styled_scanlines.next()?;
91                self.fill = scanline.fill();
92            },
93            (None, None) => None,
94        }
95    }
96}
97
98impl<C: PixelColor> StyledPixels<PrimitiveStyle<C>> for RoundedRectangle {
99    type Iter = StyledPixelsIterator<C>;
100
101    fn pixels(&self, style: &PrimitiveStyle<C>) -> Self::Iter {
102        StyledPixelsIterator::new(self, style)
103    }
104}
105
106impl<C: PixelColor> StyledDrawable<PrimitiveStyle<C>> for RoundedRectangle {
107    type Color = C;
108    type Output = ();
109
110    fn draw_styled<D>(
111        &self,
112        style: &PrimitiveStyle<C>,
113        target: &mut D,
114    ) -> Result<Self::Output, D::Error>
115    where
116        D: DrawTarget<Color = C>,
117    {
118        match (style.effective_stroke_color(), style.fill_color) {
119            (Some(stroke_color), None) => {
120                for scanline in
121                    StyledScanlines::new(&style.stroke_area(self), &style.fill_area(self))
122                {
123                    scanline.draw_stroke(target, stroke_color)?;
124                }
125            }
126            (Some(stroke_color), Some(fill_color)) => {
127                for scanline in
128                    StyledScanlines::new(&style.stroke_area(self), &style.fill_area(self))
129                {
130                    scanline.draw_stroke_and_fill(target, stroke_color, fill_color)?;
131                }
132            }
133            (None, Some(fill_color)) => {
134                for scanline in Scanlines::new(&style.fill_area(self)) {
135                    scanline.draw(target, fill_color)?;
136                }
137            }
138            (None, None) => {}
139        }
140
141        Ok(())
142    }
143}
144
145impl<C: PixelColor> StyledDimensions<PrimitiveStyle<C>> for RoundedRectangle {
146    fn styled_bounding_box(&self, style: &PrimitiveStyle<C>) -> Rectangle {
147        let offset = style.outside_stroke_width().saturating_as();
148
149        self.bounding_box().offset(offset)
150    }
151}
152
153#[derive(Clone, Eq, PartialEq, Hash, Debug)]
154#[cfg_attr(feature = "defmt", derive(::defmt::Format))]
155struct StyledScanlines {
156    scanlines: Scanlines,
157    fill_area: RoundedRectangleContains,
158}
159
160impl StyledScanlines {
161    pub fn new(stroke_area: &RoundedRectangle, fill_area: &RoundedRectangle) -> Self {
162        Self {
163            scanlines: Scanlines::new(stroke_area),
164            fill_area: RoundedRectangleContains::new(fill_area),
165        }
166    }
167}
168
169impl Iterator for StyledScanlines {
170    type Item = StyledScanline;
171
172    fn next(&mut self) -> Option<Self::Item> {
173        self.scanlines.next().map(|scanline| {
174            if self.fill_area.rows.contains(&scanline.y) {
175                let fill_start = scanline
176                    .x
177                    .clone()
178                    .find(|x| self.fill_area.contains(Point::new(*x, scanline.y)))
179                    .unwrap_or(scanline.x.start);
180
181                let fill_end = scanline
182                    .x
183                    .clone()
184                    .rfind(|x| self.fill_area.contains(Point::new(*x, scanline.y)))
185                    .map(|x| x + 1)
186                    .unwrap_or(scanline.x.end);
187
188                StyledScanline::new(scanline.y, scanline.x, Some(fill_start..fill_end))
189            } else {
190                StyledScanline::new(scanline.y, scanline.x, None)
191            }
192        })
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199    use crate::{
200        geometry::{Dimensions, Point, Size},
201        iterator::PixelIteratorExt,
202        mock_display::MockDisplay,
203        pixelcolor::{BinaryColor, Rgb888, RgbColor},
204        primitives::{
205            rectangle::Rectangle, CornerRadii, Primitive, PrimitiveStyleBuilder, StrokeAlignment,
206        },
207        Drawable,
208    };
209
210    #[test]
211    fn transparent_style_no_render() {
212        let rounded_rect = RoundedRectangle::with_equal_corners(
213            Rectangle::new(Point::zero(), Size::new(10, 20)),
214            Size::new(4, 8),
215        )
216        .into_styled(PrimitiveStyleBuilder::<BinaryColor>::new().build());
217
218        assert!(rounded_rect.pixels().eq(core::iter::empty()));
219    }
220
221    #[test]
222    fn thin_line_zero_radius_equals_rectangle() {
223        let style = PrimitiveStyleBuilder::new()
224            .stroke_color(Rgb888::RED)
225            .stroke_width(1)
226            .fill_color(Rgb888::RED)
227            .build();
228
229        let mut expected = MockDisplay::new();
230        Rectangle::new(Point::zero(), Size::new(20, 30))
231            .into_styled(style)
232            .draw(&mut expected)
233            .unwrap();
234
235        let rounded_rect = RoundedRectangle::with_equal_corners(
236            Rectangle::new(Point::zero(), Size::new(20, 30)),
237            Size::zero(),
238        )
239        .into_styled(style);
240
241        let mut drawable = MockDisplay::new();
242        rounded_rect.draw(&mut drawable).unwrap();
243        drawable.assert_eq(&expected);
244
245        let mut pixels = MockDisplay::new();
246        rounded_rect.pixels().draw(&mut pixels).unwrap();
247        pixels.assert_eq(&expected);
248    }
249
250    #[test]
251    fn styled_unequal_corners() {
252        let expected_pattern = &[
253            "   GGGGGGGGGGGGGGGG     ",
254            "  GGGGGGGGGGGGGGGGGGG   ",
255            " GGGGGGGGGGGGGGGGGGGGG  ",
256            "GGGGGGGGGGGGGGGGGGGGGGG ",
257            "GGGGGGGGGGGGGGGGGGGGGGG ",
258            "GGGGGRRRRRRRRRRRRRGGGGGG",
259            "GGGGGRRRRRRRRRRRRRRGGGGG",
260            "GGGGGRRRRRRRRRRRRRRGGGGG",
261            "GGGGGRRRRRRRRRRRRRRGGGGG",
262            "GGGGGRRRRRRRRRRRRRRGGGGG",
263            "GGGGGRRRRRRRRRRRRRRGGGGG",
264            "GGGGGRRRRRRRRRRRRRRGGGGG",
265            "GGGGGRRRRRRRRRRRRRRGGGGG",
266            "GGGGGRRRRRRRRRRRRRRGGGGG",
267            "GGGGGRRRRRRRRRRRRRRGGGGG",
268            "GGGGGGRRRRRRRRRRRRRGGGGG",
269            " GGGGGRRRRRRRRRRRRGGGGGG",
270            " GGGGGGRRRRRRRRRRRGGGGG ",
271            "  GGGGGGGRRRRRRRRGGGGGG ",
272            "  GGGGGGGGGGGGGGGGGGGGG ",
273            "   GGGGGGGGGGGGGGGGGGG  ",
274            "    GGGGGGGGGGGGGGGGG   ",
275            "      GGGGGGGGGGGGGG    ",
276            "        GGGGGGGGGG      ",
277        ];
278
279        let rounded_rect = RoundedRectangle::new(
280            Rectangle::new(Point::new_equal(2), Size::new(20, 20)),
281            CornerRadii {
282                top_left: Size::new(3, 4),
283                top_right: Size::new(5, 6),
284                bottom_right: Size::new(7, 8),
285                bottom_left: Size::new(9, 10),
286            },
287        )
288        .into_styled(
289            PrimitiveStyleBuilder::new()
290                .stroke_width(5)
291                .fill_color(Rgb888::RED)
292                .stroke_color(Rgb888::GREEN)
293                .build(),
294        );
295
296        let mut drawable = MockDisplay::new();
297        rounded_rect.draw(&mut drawable).unwrap();
298        drawable.assert_pattern(expected_pattern);
299
300        let mut pixels = MockDisplay::new();
301        rounded_rect.pixels().draw(&mut pixels).unwrap();
302        pixels.assert_pattern(expected_pattern);
303    }
304
305    #[test]
306    fn styled_unfilled() {
307        let expected_pattern = &[
308            "  BBBBBBBBBBBBBBB   ",
309            " B               B  ",
310            "B                 B ",
311            "B                 BB",
312            "B                  B",
313            "B                  B",
314            "B                  B",
315            "B                  B",
316            "B                  B",
317            "B                  B",
318            "B                  B",
319            "B                  B",
320            "B                  B",
321            " B                 B",
322            " B                 B",
323            " BB               B ",
324            "  B               B ",
325            "   BB            B  ",
326            "    BB         BB   ",
327            "      BBBBBBBBB     ",
328        ];
329
330        let rounded_rect = RoundedRectangle::new(
331            Rectangle::new(Point::zero(), Size::new(20, 20)),
332            CornerRadii {
333                top_left: Size::new(3, 4),
334                top_right: Size::new(5, 6),
335                bottom_right: Size::new(7, 8),
336                bottom_left: Size::new(9, 10),
337            },
338        )
339        .into_styled(
340            PrimitiveStyleBuilder::new()
341                .stroke_width(1)
342                .stroke_color(Rgb888::BLUE)
343                .build(),
344        );
345
346        let mut drawable = MockDisplay::new();
347        rounded_rect.draw(&mut drawable).unwrap();
348        drawable.assert_pattern(expected_pattern);
349
350        let mut pixels = MockDisplay::new();
351        rounded_rect.pixels().draw(&mut pixels).unwrap();
352        pixels.assert_pattern(expected_pattern);
353    }
354
355    #[test]
356    fn full_height_corners() {
357        let expected_pattern = &[
358            "                RRRRRRRR                ",
359            "            RRRRRRRRRRRRRRRR            ",
360            "          RRRRRRRRRRRRRRRRRRRR          ",
361            "         RRRRRRRRRRRRRRRRRRRRRR         ",
362            "       RRRRRRRRRRRRRRRRRRRRRRRRRR       ",
363            "      RRRRRRRRRRRRRRRRRRRRRRRRRRRR      ",
364            "     RRRRRRRRRRRRRRRRRRRRRRRRRRRRRR     ",
365            "    RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR    ",
366            "    RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR    ",
367            "   RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR   ",
368            "  RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR  ",
369            "  RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR  ",
370            " RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR ",
371            " RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR ",
372            " RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR ",
373            " RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR ",
374            "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR",
375            "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR",
376            "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR",
377            "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR",
378        ];
379
380        let rounded_rect = RoundedRectangle::new(
381            Rectangle::new(Point::zero(), Size::new(40, 20)),
382            CornerRadii {
383                top_left: Size::new(20, 20),
384                top_right: Size::new(20, 20),
385                bottom_right: Size::new(0, 0),
386                bottom_left: Size::new(0, 0),
387            },
388        )
389        .into_styled(PrimitiveStyleBuilder::new().fill_color(Rgb888::RED).build());
390
391        let mut drawable = MockDisplay::new();
392        rounded_rect.draw(&mut drawable).unwrap();
393        drawable.assert_pattern(expected_pattern);
394
395        let mut pixels = MockDisplay::new();
396        rounded_rect.pixels().draw(&mut pixels).unwrap();
397        pixels.assert_pattern(expected_pattern);
398    }
399
400    #[test]
401    fn styled_dimensions() {
402        let base = PrimitiveStyleBuilder::new()
403            .stroke_width(10)
404            .stroke_color(Rgb888::RED);
405
406        let inside = base.stroke_alignment(StrokeAlignment::Inside).build();
407        let outside = base.stroke_alignment(StrokeAlignment::Outside).build();
408        let center = base.stroke_alignment(StrokeAlignment::Center).build();
409
410        let item = RoundedRectangle::new(
411            Rectangle::new(Point::new(10, 10), Size::new(40, 20)),
412            CornerRadii {
413                top_left: Size::new(20, 20),
414                top_right: Size::new(20, 20),
415                bottom_right: Size::new(0, 0),
416                bottom_left: Size::new(0, 0),
417            },
418        );
419
420        let center = item.into_styled(center);
421        let inside = item.into_styled(inside);
422        let outside = item.into_styled(outside);
423
424        assert_eq!(center.bounding_box(), item.bounding_box().offset(5));
425        assert_eq!(inside.bounding_box(), item.bounding_box());
426        assert_eq!(outside.bounding_box(), item.bounding_box().offset(10));
427
428        let mut display = MockDisplay::new();
429        center.draw(&mut display).unwrap();
430        assert_eq!(display.affected_area(), center.bounding_box());
431        let mut display = MockDisplay::new();
432        inside.draw(&mut display).unwrap();
433        assert_eq!(display.affected_area(), inside.bounding_box());
434        let mut display = MockDisplay::new();
435        outside.draw(&mut display).unwrap();
436        assert_eq!(display.affected_area(), outside.bounding_box());
437    }
438
439    #[test]
440    fn bounding_box_is_independent_of_colors() {
441        let rect = RoundedRectangle::new(
442            Rectangle::new(Point::new(5, 5), Size::new(11, 14)),
443            CornerRadii {
444                top_left: Size::new(20, 20),
445                top_right: Size::new(20, 20),
446                bottom_right: Size::new(0, 0),
447                bottom_left: Size::new(0, 0),
448            },
449        );
450
451        let transparent_rect = rect.into_styled(PrimitiveStyle::<Rgb888>::new());
452        let filled_rect = rect.into_styled(PrimitiveStyle::with_fill(Rgb888::RED));
453
454        assert_eq!(transparent_rect.bounding_box(), filled_rect.bounding_box(),);
455    }
456}