1 | use crate::{pixelcolor::PixelColor, primitives::OffsetOutline}; |
2 | use az::SaturatingAs; |
3 | |
4 | /// Style properties for primitives. |
5 | /// |
6 | /// `PrimitiveStyle` can be applied to a [primitive] to define how the primitive |
7 | /// is drawn. |
8 | /// |
9 | /// Because `PrimitiveStyle` has the [`non_exhaustive`] attribute, it cannot be created using a |
10 | /// struct literal. To create a `PrimitiveStyle`, the [`with_stroke`](PrimitiveStyle::with_stroke()) and |
11 | /// [`with_fill`](PrimitiveStyle::with_fill()) methods can be used for styles that only require a stroke or |
12 | /// fill respectively. For more complex styles, use the [`PrimitiveStyleBuilder`]. |
13 | /// |
14 | /// [primitive]: crate::primitives |
15 | /// [`non_exhaustive`]: https://blog.rust-lang.org/2019/12/19/Rust-1.40.0.html#[non_exhaustive]-structs,-enums,-and-variants |
16 | #[derive (Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] |
17 | #[cfg_attr (feature = "defmt" , derive(::defmt::Format))] |
18 | #[non_exhaustive ] |
19 | pub struct PrimitiveStyle<C> |
20 | where |
21 | C: PixelColor, |
22 | { |
23 | /// Fill color of the primitive. |
24 | /// |
25 | /// If `fill_color` is set to `None` no fill will be drawn. |
26 | pub fill_color: Option<C>, |
27 | |
28 | /// Stroke color of the primitive. |
29 | /// |
30 | /// If `stroke_color` is set to `None` or the `stroke_width` is set to `0` no stroke will be |
31 | /// drawn. |
32 | pub stroke_color: Option<C>, |
33 | |
34 | /// Stroke width in pixels. |
35 | pub stroke_width: u32, |
36 | |
37 | /// Stroke alignment. |
38 | /// |
39 | /// The stroke alignment sets if the stroke is drawn inside, outside or centered |
40 | /// on the outline of a shape. |
41 | /// |
42 | /// This property only applies to closed shapes (rectangle, circle, ...) and is |
43 | /// ignored for open shapes (line, ...). |
44 | pub stroke_alignment: StrokeAlignment, |
45 | } |
46 | |
47 | impl<C> PrimitiveStyle<C> |
48 | where |
49 | C: PixelColor, |
50 | { |
51 | /// Creates a primitive style without fill and stroke. |
52 | pub const fn new() -> Self { |
53 | Self::const_default() |
54 | } |
55 | |
56 | /// Creates a stroke primitive style. |
57 | /// |
58 | /// If the `stroke_width` is `0` the resulting style won't draw a stroke. |
59 | pub const fn with_stroke(stroke_color: C, stroke_width: u32) -> Self { |
60 | Self { |
61 | stroke_color: Some(stroke_color), |
62 | stroke_width, |
63 | ..PrimitiveStyle::const_default() |
64 | } |
65 | } |
66 | |
67 | /// Creates a fill primitive style. |
68 | pub const fn with_fill(fill_color: C) -> Self { |
69 | Self { |
70 | fill_color: Some(fill_color), |
71 | ..PrimitiveStyle::const_default() |
72 | } |
73 | } |
74 | |
75 | /// Returns the stroke width on the outside of the shape. |
76 | /// |
77 | /// The outside stroke width is determined by `stroke_width` and `stroke_alignment`. |
78 | pub(crate) const fn outside_stroke_width(&self) -> u32 { |
79 | match self.stroke_alignment { |
80 | StrokeAlignment::Inside => 0, |
81 | StrokeAlignment::Center => self.stroke_width / 2, |
82 | StrokeAlignment::Outside => self.stroke_width, |
83 | } |
84 | } |
85 | |
86 | /// Returns the stroke width on the inside of the shape. |
87 | /// |
88 | /// The inside stroke width is determined by `stroke_width` and `stroke_alignment`. |
89 | pub(crate) const fn inside_stroke_width(&self) -> u32 { |
90 | match self.stroke_alignment { |
91 | StrokeAlignment::Inside => self.stroke_width, |
92 | StrokeAlignment::Center => self.stroke_width.saturating_add(1) / 2, |
93 | StrokeAlignment::Outside => 0, |
94 | } |
95 | } |
96 | |
97 | /// Returns if a primitive drawn with this style is completely transparent. |
98 | pub const fn is_transparent(&self) -> bool { |
99 | (self.stroke_color.is_none() || self.stroke_width == 0) && self.fill_color.is_none() |
100 | } |
101 | |
102 | /// Returns the effective stroke color of the style. |
103 | /// |
104 | /// If the stroke width is 0, this method will return `None` regardless of the value in |
105 | /// `stroke_color`. |
106 | pub(crate) fn effective_stroke_color(&self) -> Option<C> { |
107 | self.stroke_color.filter(|_| self.stroke_width > 0) |
108 | } |
109 | |
110 | /// Returns the stroke area. |
111 | pub(in crate::primitives) fn stroke_area<P: OffsetOutline>(&self, primitive: &P) -> P { |
112 | // saturate offset at i32::max_value() if stroke width is to large |
113 | let offset = self.outside_stroke_width().saturating_as(); |
114 | |
115 | primitive.offset(offset) |
116 | } |
117 | |
118 | /// Returns the fill area. |
119 | pub(in crate::primitives) fn fill_area<P: OffsetOutline>(&self, primitive: &P) -> P { |
120 | // saturate offset at i32::min_value() if stroke width is to large |
121 | let offset = -self.inside_stroke_width().saturating_as::<i32>(); |
122 | |
123 | primitive.offset(offset) |
124 | } |
125 | |
126 | /// A helper function to allow `const` default. |
127 | // MSRV: Move into `Default` impl when we have consts in traits |
128 | const fn const_default() -> Self { |
129 | Self { |
130 | fill_color: None, |
131 | stroke_color: None, |
132 | stroke_width: 0, |
133 | stroke_alignment: StrokeAlignment::Center, |
134 | } |
135 | } |
136 | } |
137 | |
138 | impl<C> Default for PrimitiveStyle<C> |
139 | where |
140 | C: PixelColor, |
141 | { |
142 | fn default() -> Self { |
143 | Self::const_default() |
144 | } |
145 | } |
146 | |
147 | /// Primitive style builder. |
148 | /// |
149 | /// Use this builder to create [`PrimitiveStyle`]s. If any properties on the builder are omitted, |
150 | /// the value will remain at its default value. |
151 | /// |
152 | /// # Examples |
153 | /// |
154 | /// ## Build a style with configured stroke and fill |
155 | /// |
156 | /// This example builds a style for a circle with a 3px red stroke and a solid green fill. The |
157 | /// circle has its top-left at (10, 10) with a diameter of 20px. |
158 | /// |
159 | /// ```rust |
160 | /// use embedded_graphics::{ |
161 | /// pixelcolor::Rgb565, |
162 | /// prelude::*, |
163 | /// primitives::{Circle, PrimitiveStyle, PrimitiveStyleBuilder}, |
164 | /// }; |
165 | /// |
166 | /// let style: PrimitiveStyle<Rgb565> = PrimitiveStyleBuilder::new() |
167 | /// .stroke_color(Rgb565::RED) |
168 | /// .stroke_width(3) |
169 | /// .fill_color(Rgb565::GREEN) |
170 | /// .build(); |
171 | /// |
172 | /// let circle = Circle::new(Point::new(10, 10), 20).into_styled(style); |
173 | /// ``` |
174 | /// |
175 | /// ## Build a style with stroke and no fill |
176 | /// |
177 | /// This example builds a style for a rectangle with a 1px red stroke. Because `.fill_color()` is |
178 | /// not called, the fill color remains the default value of `None` (i.e. transparent). |
179 | /// |
180 | /// ```rust |
181 | /// use embedded_graphics::{ |
182 | /// pixelcolor::Rgb565, |
183 | /// prelude::*, |
184 | /// primitives::{Rectangle, PrimitiveStyle, PrimitiveStyleBuilder}, |
185 | /// }; |
186 | /// |
187 | /// let style: PrimitiveStyle<Rgb565> = PrimitiveStyleBuilder::new() |
188 | /// .stroke_color(Rgb565::RED) |
189 | /// .stroke_width(1) |
190 | /// .build(); |
191 | /// |
192 | /// let rectangle = Rectangle::new(Point::new(20, 20), Size::new(20, 10)).into_styled(style); |
193 | /// ``` |
194 | /// |
195 | #[derive (Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)] |
196 | #[cfg_attr (feature = "defmt" , derive(::defmt::Format))] |
197 | pub struct PrimitiveStyleBuilder<C> |
198 | where |
199 | C: PixelColor, |
200 | { |
201 | style: PrimitiveStyle<C>, |
202 | } |
203 | |
204 | impl<C> PrimitiveStyleBuilder<C> |
205 | where |
206 | C: PixelColor, |
207 | { |
208 | /// Creates a new primitive style builder. |
209 | pub const fn new() -> Self { |
210 | Self { |
211 | style: PrimitiveStyle::const_default(), |
212 | } |
213 | } |
214 | |
215 | /// Sets the fill color. |
216 | pub const fn fill_color(mut self, fill_color: C) -> Self { |
217 | self.style.fill_color = Some(fill_color); |
218 | |
219 | self |
220 | } |
221 | |
222 | /// Resets the fill color to transparent. |
223 | pub const fn reset_fill_color(mut self) -> Self { |
224 | self.style.fill_color = None; |
225 | |
226 | self |
227 | } |
228 | |
229 | /// Sets the stroke color. |
230 | pub const fn stroke_color(mut self, stroke_color: C) -> Self { |
231 | self.style.stroke_color = Some(stroke_color); |
232 | |
233 | self |
234 | } |
235 | |
236 | /// Resets the stroke color to transparent. |
237 | pub const fn reset_stroke_color(mut self) -> Self { |
238 | self.style.stroke_color = None; |
239 | |
240 | self |
241 | } |
242 | |
243 | /// Sets the stroke width. |
244 | pub const fn stroke_width(mut self, stroke_width: u32) -> Self { |
245 | self.style.stroke_width = stroke_width; |
246 | |
247 | self |
248 | } |
249 | |
250 | /// Sets the stroke alignment. |
251 | pub const fn stroke_alignment(mut self, stroke_alignment: StrokeAlignment) -> Self { |
252 | self.style.stroke_alignment = stroke_alignment; |
253 | |
254 | self |
255 | } |
256 | |
257 | /// Builds the primitive style. |
258 | pub const fn build(self) -> PrimitiveStyle<C> { |
259 | self.style |
260 | } |
261 | } |
262 | |
263 | impl<C> From<&PrimitiveStyle<C>> for PrimitiveStyleBuilder<C> |
264 | where |
265 | C: PixelColor, |
266 | { |
267 | fn from(style: &PrimitiveStyle<C>) -> Self { |
268 | Self { style: *style } |
269 | } |
270 | } |
271 | |
272 | /// Stroke alignment. |
273 | #[derive (Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] |
274 | #[cfg_attr (feature = "defmt" , derive(::defmt::Format))] |
275 | pub enum StrokeAlignment { |
276 | /// Inside. |
277 | Inside, |
278 | /// Center. |
279 | Center, |
280 | /// Outside. |
281 | Outside, |
282 | } |
283 | |
284 | impl Default for StrokeAlignment { |
285 | fn default() -> Self { |
286 | Self::Center |
287 | } |
288 | } |
289 | |
290 | #[cfg (test)] |
291 | mod tests { |
292 | use super::*; |
293 | use crate::pixelcolor::{BinaryColor, Rgb888, RgbColor}; |
294 | |
295 | #[test ] |
296 | fn default_style() { |
297 | assert_eq!( |
298 | PrimitiveStyle::<BinaryColor>::default(), |
299 | PrimitiveStyle { |
300 | fill_color: None, |
301 | stroke_color: None, |
302 | stroke_width: 0, |
303 | stroke_alignment: StrokeAlignment::Center, |
304 | } |
305 | ); |
306 | |
307 | assert_eq!( |
308 | PrimitiveStyle::<BinaryColor>::default(), |
309 | PrimitiveStyle::new() |
310 | ); |
311 | } |
312 | |
313 | #[test ] |
314 | fn constructors() { |
315 | let style = PrimitiveStyle::with_fill(Rgb888::RED); |
316 | assert_eq!(style.fill_color, Some(Rgb888::RED)); |
317 | assert_eq!(style.stroke_color, None); |
318 | |
319 | let style = PrimitiveStyle::with_stroke(Rgb888::GREEN, 123); |
320 | assert_eq!(style.fill_color, None); |
321 | assert_eq!(style.stroke_color, Some(Rgb888::GREEN)); |
322 | assert_eq!(style.stroke_width, 123); |
323 | } |
324 | |
325 | #[test ] |
326 | fn stroke_alignment_1px() { |
327 | let mut style = PrimitiveStyle::with_stroke(BinaryColor::On, 1); |
328 | |
329 | style.stroke_alignment = StrokeAlignment::Inside; |
330 | assert_eq!(style.inside_stroke_width(), 1); |
331 | assert_eq!(style.outside_stroke_width(), 0); |
332 | |
333 | style.stroke_alignment = StrokeAlignment::Center; |
334 | assert_eq!(style.inside_stroke_width(), 1); |
335 | assert_eq!(style.outside_stroke_width(), 0); |
336 | |
337 | style.stroke_alignment = StrokeAlignment::Outside; |
338 | assert_eq!(style.inside_stroke_width(), 0); |
339 | assert_eq!(style.outside_stroke_width(), 1); |
340 | } |
341 | |
342 | #[test ] |
343 | fn stroke_alignment_2px() { |
344 | let mut style = PrimitiveStyle::with_stroke(BinaryColor::On, 2); |
345 | |
346 | style.stroke_alignment = StrokeAlignment::Inside; |
347 | assert_eq!(style.inside_stroke_width(), 2); |
348 | assert_eq!(style.outside_stroke_width(), 0); |
349 | |
350 | style.stroke_alignment = StrokeAlignment::Center; |
351 | assert_eq!(style.inside_stroke_width(), 1); |
352 | assert_eq!(style.outside_stroke_width(), 1); |
353 | |
354 | style.stroke_alignment = StrokeAlignment::Outside; |
355 | assert_eq!(style.inside_stroke_width(), 0); |
356 | assert_eq!(style.outside_stroke_width(), 2); |
357 | } |
358 | |
359 | #[test ] |
360 | fn builder_default() { |
361 | assert_eq!( |
362 | PrimitiveStyleBuilder::<BinaryColor>::new().build(), |
363 | PrimitiveStyle::<BinaryColor>::default() |
364 | ); |
365 | } |
366 | |
367 | #[test ] |
368 | fn builder_stroke() { |
369 | assert_eq!( |
370 | PrimitiveStyleBuilder::new() |
371 | .stroke_color(BinaryColor::On) |
372 | .stroke_width(10) |
373 | .build(), |
374 | PrimitiveStyle::with_stroke(BinaryColor::On, 10) |
375 | ); |
376 | } |
377 | |
378 | #[test ] |
379 | fn builder_reset_stroke_color() { |
380 | assert_eq!( |
381 | PrimitiveStyleBuilder::new() |
382 | .stroke_color(BinaryColor::On) |
383 | .stroke_width(10) |
384 | .fill_color(BinaryColor::Off) |
385 | .reset_stroke_color() |
386 | .build(), |
387 | PrimitiveStyleBuilder::new() |
388 | .stroke_width(10) |
389 | .fill_color(BinaryColor::Off) |
390 | .build() |
391 | ); |
392 | } |
393 | |
394 | #[test ] |
395 | fn builder_fill() { |
396 | assert_eq!( |
397 | PrimitiveStyleBuilder::new() |
398 | .fill_color(BinaryColor::On) |
399 | .build(), |
400 | PrimitiveStyle::with_fill(BinaryColor::On) |
401 | ); |
402 | } |
403 | |
404 | #[test ] |
405 | fn builder_reset_fill_color() { |
406 | assert_eq!( |
407 | PrimitiveStyleBuilder::new() |
408 | .fill_color(BinaryColor::On) |
409 | .stroke_color(BinaryColor::Off) |
410 | .reset_fill_color() |
411 | .build(), |
412 | PrimitiveStyleBuilder::new() |
413 | .stroke_color(BinaryColor::Off) |
414 | .build(), |
415 | ); |
416 | } |
417 | |
418 | #[test ] |
419 | fn effective_stroke_color() { |
420 | assert_eq!( |
421 | PrimitiveStyle::with_stroke(BinaryColor::On, 1).effective_stroke_color(), |
422 | Some(BinaryColor::On) |
423 | ); |
424 | |
425 | assert_eq!( |
426 | PrimitiveStyle::with_stroke(BinaryColor::On, 0).effective_stroke_color(), |
427 | None |
428 | ); |
429 | } |
430 | |
431 | #[test ] |
432 | fn stroke_width_max_value() { |
433 | assert_eq!( |
434 | PrimitiveStyleBuilder::from(&PrimitiveStyle::with_stroke( |
435 | BinaryColor::On, |
436 | core::u32::MAX |
437 | )) |
438 | .stroke_alignment(StrokeAlignment::Center) |
439 | .build() |
440 | .inside_stroke_width(), |
441 | core::u32::MAX / 2 |
442 | ); |
443 | } |
444 | } |
445 | |