embedded_graphics/primitives/rectangle/
styled.rs

1use crate::{
2    draw_target::DrawTarget,
3    geometry::{Dimensions, Point, Size},
4    pixelcolor::PixelColor,
5    primitives::{
6        rectangle::{Points, Rectangle},
7        styled::{StyledDimensions, StyledDrawable, StyledPixels},
8        PointsIter, PrimitiveStyle,
9    },
10    transform::Transform,
11    Pixel,
12};
13use az::SaturatingAs;
14
15/// Pixel iterator for each pixel in the rect border
16#[derive(Clone, Eq, PartialEq, Hash, Debug)]
17#[cfg_attr(feature = "defmt", derive(::defmt::Format))]
18pub struct StyledPixelsIterator<C> {
19    iter: Points,
20
21    stroke_color: Option<C>,
22
23    fill_area: Rectangle,
24    fill_color: Option<C>,
25}
26
27impl<C: PixelColor> StyledPixelsIterator<C> {
28    pub(in crate::primitives) fn new(primitive: &Rectangle, style: &PrimitiveStyle<C>) -> Self {
29        let iter = if !style.is_transparent() {
30            style.stroke_area(primitive).points()
31        } else {
32            Points::empty()
33        };
34
35        Self {
36            iter,
37            fill_area: style.fill_area(primitive),
38            stroke_color: style.stroke_color,
39            fill_color: style.fill_color,
40        }
41    }
42}
43
44impl<C: PixelColor> Iterator for StyledPixelsIterator<C> {
45    type Item = Pixel<C>;
46
47    fn next(&mut self) -> Option<Self::Item> {
48        for point in &mut self.iter {
49            let color = if self.fill_area.contains(point) {
50                self.fill_color
51            } else {
52                self.stroke_color
53            };
54
55            if let Some(color) = color {
56                return Some(Pixel(point, color));
57            }
58        }
59
60        None
61    }
62}
63
64impl<C: PixelColor> StyledPixels<PrimitiveStyle<C>> for Rectangle {
65    type Iter = StyledPixelsIterator<C>;
66
67    fn pixels(&self, style: &PrimitiveStyle<C>) -> Self::Iter {
68        Self::Iter::new(self, style)
69    }
70}
71
72impl<C: PixelColor> StyledDrawable<PrimitiveStyle<C>> for Rectangle {
73    type Color = C;
74    type Output = ();
75
76    fn draw_styled<D>(
77        &self,
78        style: &PrimitiveStyle<C>,
79        target: &mut D,
80    ) -> Result<Self::Output, D::Error>
81    where
82        D: DrawTarget<Color = C>,
83    {
84        let fill_area = style.fill_area(self);
85
86        // Fill rectangle
87        if let Some(fill_color) = style.fill_color {
88            target.fill_solid(&fill_area, fill_color)?;
89        }
90
91        // Draw stroke
92        if let Some(stroke_color) = style.effective_stroke_color() {
93            let stroke_width = style.stroke_width;
94
95            let stroke_area = style.stroke_area(self);
96
97            let top_border = Rectangle::new(
98                stroke_area.top_left,
99                Size::new(
100                    stroke_area.size.width,
101                    stroke_width.min(stroke_area.size.height / 2),
102                ),
103            );
104
105            let bottom_stroke_width =
106                stroke_width.min(stroke_area.size.height - top_border.size.height);
107
108            let bottom_border = Rectangle::new(
109                top_border.top_left
110                    + Size::new(
111                        0,
112                        stroke_area.size.height.saturating_sub(bottom_stroke_width),
113                    ),
114                Size::new(stroke_area.size.width, bottom_stroke_width),
115            );
116
117            target.fill_solid(&top_border, stroke_color)?;
118            target.fill_solid(&bottom_border, stroke_color)?;
119
120            if fill_area.size.height > 0 {
121                let left_border = Rectangle::new(
122                    stroke_area.top_left + top_border.size.y_axis(),
123                    Size::new(
124                        (stroke_width * 2).min(stroke_area.size.width + 1) / 2,
125                        fill_area.size.height,
126                    ),
127                );
128
129                let right_border = left_border.translate(Point::new(
130                    stroke_area
131                        .size
132                        .width
133                        .saturating_sub(left_border.size.width) as i32,
134                    0,
135                ));
136
137                target.fill_solid(&left_border, stroke_color)?;
138                target.fill_solid(&right_border, stroke_color)?;
139            }
140        }
141
142        Ok(())
143    }
144}
145
146impl<C: PixelColor> StyledDimensions<PrimitiveStyle<C>> for Rectangle {
147    fn styled_bounding_box(&self, style: &PrimitiveStyle<C>) -> Rectangle {
148        let offset = style.outside_stroke_width().saturating_as();
149
150        self.bounding_box().offset(offset)
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157    use crate::{
158        geometry::{Point, Size},
159        iterator::PixelIteratorExt,
160        mock_display::MockDisplay,
161        pixelcolor::{BinaryColor, Rgb565, RgbColor},
162        primitives::{Primitive, PrimitiveStyle, PrimitiveStyleBuilder, StrokeAlignment},
163        Drawable,
164    };
165
166    #[test]
167    fn it_draws_unfilled_rect() {
168        let mut rect = Rectangle::new(Point::new(2, 2), Size::new(3, 3))
169            .into_styled(PrimitiveStyle::with_stroke(Rgb565::RED, 1))
170            .pixels();
171
172        assert_eq!(rect.next(), Some(Pixel(Point::new(2, 2), Rgb565::RED)));
173        assert_eq!(rect.next(), Some(Pixel(Point::new(3, 2), Rgb565::RED)));
174        assert_eq!(rect.next(), Some(Pixel(Point::new(4, 2), Rgb565::RED)));
175
176        assert_eq!(rect.next(), Some(Pixel(Point::new(2, 3), Rgb565::RED)));
177        assert_eq!(rect.next(), Some(Pixel(Point::new(4, 3), Rgb565::RED)));
178
179        assert_eq!(rect.next(), Some(Pixel(Point::new(2, 4), Rgb565::RED)));
180        assert_eq!(rect.next(), Some(Pixel(Point::new(3, 4), Rgb565::RED)));
181        assert_eq!(rect.next(), Some(Pixel(Point::new(4, 4), Rgb565::RED)));
182    }
183
184    #[test]
185    fn points_iter_matches_filled_styled() {
186        let rectangle = Rectangle::new(Point::new(10, 10), Size::new(20, 30));
187
188        let styled_points = rectangle
189            .clone()
190            .into_styled(PrimitiveStyle::with_fill(Rgb565::WHITE))
191            .pixels()
192            .map(|Pixel(p, _)| p);
193
194        assert!(rectangle.points().eq(styled_points));
195    }
196
197    #[test]
198    fn stroke_alignment() {
199        const TOP_LEFT: Point = Point::new(5, 6);
200        const SIZE: Size = Size::new(10, 5);
201
202        let style = PrimitiveStyle::with_stroke(BinaryColor::On, 3);
203
204        let mut display_center = MockDisplay::new();
205        Rectangle::new(TOP_LEFT, SIZE)
206            .into_styled(style)
207            .draw(&mut display_center)
208            .unwrap();
209
210        let mut display_inside = MockDisplay::new();
211        Rectangle::new(TOP_LEFT - Point::new(1, 1), SIZE + Size::new(2, 2))
212            .into_styled(
213                PrimitiveStyleBuilder::from(&style)
214                    .stroke_alignment(StrokeAlignment::Inside)
215                    .build(),
216            )
217            .draw(&mut display_inside)
218            .unwrap();
219
220        let mut display_outside = MockDisplay::new();
221        Rectangle::new(TOP_LEFT + Point::new(2, 2), SIZE - Size::new(4, 4))
222            .into_styled(
223                PrimitiveStyleBuilder::from(&style)
224                    .stroke_alignment(StrokeAlignment::Outside)
225                    .build(),
226            )
227            .draw(&mut display_outside)
228            .unwrap();
229
230        display_inside.assert_eq(&display_center);
231        display_outside.assert_eq(&display_center);
232    }
233
234    #[test]
235    fn stroke_iter_vs_draw() {
236        const TOP_LEFT: Point = Point::new(5, 6);
237        const SIZE: Size = Size::new(10, 5);
238
239        let style = PrimitiveStyle::with_stroke(BinaryColor::On, 3);
240
241        let rectangle_center = Rectangle::new(TOP_LEFT, SIZE).into_styled(style);
242
243        let mut drawn_center = MockDisplay::new();
244        let mut iter_center = MockDisplay::new();
245        rectangle_center.draw(&mut drawn_center).unwrap();
246        rectangle_center.pixels().draw(&mut iter_center).unwrap();
247        drawn_center.assert_eq(&iter_center);
248
249        let rectangle_inside = Rectangle::new(TOP_LEFT - Point::new(1, 1), SIZE + Size::new(2, 2))
250            .into_styled(
251                PrimitiveStyleBuilder::from(&style)
252                    .stroke_alignment(StrokeAlignment::Inside)
253                    .build(),
254            );
255
256        let mut drawn_inside = MockDisplay::new();
257        let mut iter_inside = MockDisplay::new();
258        rectangle_inside.draw(&mut drawn_inside).unwrap();
259        rectangle_inside.pixels().draw(&mut iter_inside).unwrap();
260        drawn_inside.assert_eq(&iter_inside);
261
262        let rectangle_outside = Rectangle::new(TOP_LEFT + Point::new(2, 2), SIZE - Size::new(4, 4))
263            .into_styled(
264                PrimitiveStyleBuilder::from(&style)
265                    .stroke_alignment(StrokeAlignment::Outside)
266                    .build(),
267            );
268
269        let mut drawn_outside = MockDisplay::new();
270        let mut iter_outside = MockDisplay::new();
271        rectangle_outside.draw(&mut drawn_outside).unwrap();
272        rectangle_outside.pixels().draw(&mut iter_outside).unwrap();
273        drawn_outside.assert_eq(&iter_outside);
274    }
275
276    #[test]
277    fn fill_iter_vs_draw() {
278        const TOP_LEFT: Point = Point::new(5, 6);
279        const SIZE: Size = Size::new(10, 5);
280
281        let style = PrimitiveStyle::with_fill(BinaryColor::On);
282
283        let rectangle = Rectangle::new(TOP_LEFT, SIZE).into_styled(style);
284
285        let mut drawn = MockDisplay::new();
286        let mut iter = MockDisplay::new();
287        rectangle.draw(&mut drawn).unwrap();
288        rectangle.pixels().draw(&mut iter).unwrap();
289        drawn.assert_eq(&iter);
290    }
291
292    /// Compare the output of the draw() call vs iterators across multiple styles and stroke
293    /// alignments.
294    fn compare_drawable_iter(rect: Rectangle) {
295        let thin_stroke = PrimitiveStyle::with_stroke(Rgb565::RED, 1);
296        let stroke = PrimitiveStyle::with_stroke(Rgb565::RED, 5);
297        let stroke_fill = PrimitiveStyleBuilder::new()
298            .stroke_color(Rgb565::RED)
299            .stroke_width(5)
300            .fill_color(Rgb565::GREEN)
301            .build();
302        let fill = PrimitiveStyle::with_fill(Rgb565::BLUE);
303
304        for (name, style) in [
305            ("thin_stroke", thin_stroke),
306            ("stroke", stroke),
307            ("stroke_fill", stroke_fill),
308            ("fill", fill),
309        ]
310        .iter()
311        {
312            for alignment in [
313                StrokeAlignment::Center,
314                StrokeAlignment::Inside,
315                StrokeAlignment::Outside,
316            ]
317            .iter()
318            {
319                let style = PrimitiveStyleBuilder::from(style)
320                    .stroke_alignment(*alignment)
321                    .build();
322
323                let mut display_drawable = MockDisplay::new();
324                let mut display_iter = MockDisplay::new();
325
326                // Calls draw() impl above using fill_solid()
327                rect.into_styled(style).draw(&mut display_drawable).unwrap();
328
329                // Calls draw_iter()
330                rect.into_styled(style)
331                    .pixels()
332                    .draw(&mut display_iter)
333                    .unwrap();
334
335                display_drawable.assert_eq_with_message(
336                    &display_iter,
337                    |f| write!(f,
338                        "{} x {} rectangle with style '{}' and alignment {:?} does not match iterator",
339                        rect.size.width, rect.size.height, name, alignment
340                    )
341                );
342            }
343        }
344    }
345
346    #[test]
347    fn drawable_vs_iterator() {
348        compare_drawable_iter(Rectangle::new(Point::new(10, 20), Size::new(20, 30)))
349    }
350
351    #[test]
352    fn drawable_vs_iterator_squares() {
353        for i in 0..20 {
354            compare_drawable_iter(Rectangle::new(Point::new(7, 7), Size::new_equal(i)))
355        }
356    }
357
358    #[test]
359    fn reuse() {
360        let rectangle = Rectangle::new(Point::zero(), Size::new_equal(10));
361
362        let styled = rectangle.into_styled(PrimitiveStyle::with_fill(BinaryColor::On));
363
364        let _pixels = styled.pixels();
365
366        let moved = rectangle.translate(Point::new(1, 2));
367
368        assert_eq!(moved, Rectangle::new(Point::new(1, 2), Size::new_equal(10)));
369    }
370
371    #[test]
372    fn bounding_box() {
373        let rectangle = Rectangle::new(Point::new(10, 10), Size::new(15, 20));
374
375        let base = PrimitiveStyleBuilder::new()
376            .stroke_color(BinaryColor::On)
377            .stroke_width(5);
378
379        let center = rectangle.into_styled(base.stroke_alignment(StrokeAlignment::Center).build());
380        let inside = rectangle.into_styled(base.stroke_alignment(StrokeAlignment::Inside).build());
381        let outside =
382            rectangle.into_styled(base.stroke_alignment(StrokeAlignment::Outside).build());
383
384        let mut display = MockDisplay::new();
385        center.draw(&mut display).unwrap();
386        assert_eq!(display.affected_area(), center.bounding_box());
387        let mut display = MockDisplay::new();
388        inside.draw(&mut display).unwrap();
389        assert_eq!(display.affected_area(), inside.bounding_box());
390        let mut display = MockDisplay::new();
391        outside.draw(&mut display).unwrap();
392        assert_eq!(display.affected_area(), outside.bounding_box());
393    }
394
395    #[test]
396    fn bounding_box_is_independent_of_colors() {
397        let rect = Rectangle::new(Point::new(5, 5), Size::new(11, 14));
398
399        let transparent_rect = rect.into_styled(PrimitiveStyle::<BinaryColor>::new());
400        let filled_rect = rect.into_styled(PrimitiveStyle::with_fill(BinaryColor::On));
401
402        assert_eq!(transparent_rect.bounding_box(), filled_rect.bounding_box(),);
403    }
404}