1 | use 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 | }; |
13 | use 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))] |
18 | pub struct StyledPixelsIterator<C> { |
19 | iter: Points, |
20 | |
21 | stroke_color: Option<C>, |
22 | |
23 | fill_area: Rectangle, |
24 | fill_color: Option<C>, |
25 | } |
26 | |
27 | impl<C: PixelColor> StyledPixelsIterator<C> { |
28 | pub(in crate::primitives) fn new(primitive: &Rectangle, style: &PrimitiveStyle<C>) -> Self { |
29 | let iter: Points = 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 | |
44 | impl<C: PixelColor> Iterator for StyledPixelsIterator<C> { |
45 | type Item = Pixel<C>; |
46 | |
47 | fn next(&mut self) -> Option<Self::Item> { |
48 | for point: Point in &mut self.iter { |
49 | let color: Option = if self.fill_area.contains(point) { |
50 | self.fill_color |
51 | } else { |
52 | self.stroke_color |
53 | }; |
54 | |
55 | if let Some(color: C) = color { |
56 | return Some(Pixel(point, color)); |
57 | } |
58 | } |
59 | |
60 | None |
61 | } |
62 | } |
63 | |
64 | impl<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 | |
72 | impl<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 | |
146 | impl<C: PixelColor> StyledDimensions<PrimitiveStyle<C>> for Rectangle { |
147 | fn styled_bounding_box(&self, style: &PrimitiveStyle<C>) -> Rectangle { |
148 | let offset: i32 = style.outside_stroke_width().saturating_as(); |
149 | |
150 | self.bounding_box().offset(offset) |
151 | } |
152 | } |
153 | |
154 | #[cfg (test)] |
155 | mod 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 | } |
405 | |