| 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 | |