1use crate::{pixelcolor::PixelColor, primitives::OffsetOutline};
2use 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]
19pub struct PrimitiveStyle<C>
20where
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
47impl<C> PrimitiveStyle<C>
48where
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
138impl<C> Default for PrimitiveStyle<C>
139where
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))]
197pub struct PrimitiveStyleBuilder<C>
198where
199 C: PixelColor,
200{
201 style: PrimitiveStyle<C>,
202}
203
204impl<C> PrimitiveStyleBuilder<C>
205where
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
263impl<C> From<&PrimitiveStyle<C>> for PrimitiveStyleBuilder<C>
264where
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))]
275pub enum StrokeAlignment {
276 /// Inside.
277 Inside,
278 /// Center.
279 Center,
280 /// Outside.
281 Outside,
282}
283
284impl Default for StrokeAlignment {
285 fn default() -> Self {
286 Self::Center
287 }
288}
289
290#[cfg(test)]
291mod 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