embedded_graphics/primitives/
primitive_style.rs

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}