eg_seven_segment/
seven_segment_style.rs

1use core::convert::TryFrom;
2
3use embedded_graphics::{
4    prelude::*,
5    primitives::{Rectangle, StyledDrawable},
6    text::{
7        renderer::{CharacterStyle, TextMetrics, TextRenderer},
8        Baseline,
9    },
10};
11
12use crate::{Digit, Segments};
13
14/// Seven-segment character style.
15///
16/// Use [`SevenSegmentStyleBuilder`] to build styles.
17///
18/// [`SevenSegmentStyleBuilder`]: struct.SevenSegmentStyleBuilder.html
19#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
20#[non_exhaustive]
21pub struct SevenSegmentStyle<C> {
22    /// The size of each digit.
23    pub digit_size: Size,
24
25    /// The spacing between adjacent digits.
26    pub digit_spacing: u32,
27
28    /// The width of the segments.
29    pub segment_width: u32,
30
31    /// The color of active segments.
32    pub segment_color: Option<C>,
33
34    /// The color of inactive segments.
35    pub inactive_segment_color: Option<C>,
36}
37
38impl<C: PixelColor> SevenSegmentStyle<C> {
39    /// Returns the fill color for the given segment state.
40    pub(crate) fn state_color(&self, state: bool) -> Option<C> {
41        if state {
42            self.segment_color
43        } else {
44            self.inactive_segment_color
45        }
46    }
47
48    /// Returns the vertical offset between the line position and the top edge of the bounding box.
49    fn baseline_offset(&self, baseline: Baseline) -> u32 {
50        let bottom = self.digit_size.height.saturating_sub(1);
51
52        match baseline {
53            Baseline::Top => 0,
54            Baseline::Bottom | Baseline::Alphabetic => bottom,
55            Baseline::Middle => bottom / 2,
56        }
57    }
58}
59
60impl<C: PixelColor> CharacterStyle for SevenSegmentStyle<C> {
61    type Color = C;
62
63    fn set_text_color(&mut self, text_color: Option<Self::Color>) {
64        self.segment_color = text_color;
65    }
66}
67
68impl<C: PixelColor> TextRenderer for SevenSegmentStyle<C> {
69    type Color = C;
70
71    fn draw_string<D>(
72        &self,
73        text: &str,
74        mut position: Point,
75        baseline: Baseline,
76        target: &mut D,
77    ) -> Result<Point, D::Error>
78    where
79        D: DrawTarget<Color = C>,
80    {
81        position -= Size::new(0, self.baseline_offset(baseline));
82
83        for c in text.chars() {
84            if let Ok(segments) = Segments::try_from(c) {
85                position = Digit::new(segments, position).draw_styled(self, target)?;
86            } else if c == ':' {
87                if let Some(color) = self.segment_color {
88                    let dy = self.digit_size.height / 3;
89
90                    let mut rect = Rectangle::new(
91                        position + Size::new(0, dy - self.segment_width / 2),
92                        Size::new(self.segment_width, self.segment_width),
93                    );
94                    target.fill_solid(&rect, color)?;
95
96                    rect.top_left += Size::new(0, dy);
97                    target.fill_solid(&rect, color)?;
98                }
99
100                position += Size::new(self.segment_width + self.digit_spacing, 0);
101            } else if c == '.' {
102                if let Some(color) = self.segment_color {
103                    let rect = Rectangle::new(
104                        position + Size::new(0, self.digit_size.height - self.segment_width),
105                        Size::new(self.segment_width, self.segment_width),
106                    );
107                    target.fill_solid(&rect, color)?;
108                }
109
110                position += Size::new(self.segment_width + self.digit_spacing, 0);
111            } else {
112                position += self.digit_size.x_axis() + Size::new(self.digit_spacing, 0);
113            }
114        }
115
116        position += Size::new(0, self.baseline_offset(baseline));
117
118        Ok(position)
119    }
120
121    fn draw_whitespace<D>(
122        &self,
123        width: u32,
124        position: Point,
125        _baseline: Baseline,
126        _target: &mut D,
127    ) -> Result<Point, D::Error>
128    where
129        D: DrawTarget<Color = C>,
130    {
131        Ok(position + Size::new(width, 0))
132    }
133
134    fn measure_string(&self, text: &str, position: Point, baseline: Baseline) -> TextMetrics {
135        let width = text
136            .chars()
137            .map(|c| {
138                let width = if c == '.' || c == ':' {
139                    self.segment_width
140                } else {
141                    self.digit_size.width
142                };
143
144                width + self.digit_spacing
145            })
146            .sum::<u32>()
147            .saturating_sub(self.digit_spacing);
148
149        let bounding_box = Rectangle::new(
150            position - Size::new(0, self.baseline_offset(baseline)),
151            Size::new(width, self.digit_size.height),
152        );
153        let next_position = position + Size::new(width, 0);
154
155        TextMetrics {
156            bounding_box,
157            next_position,
158        }
159    }
160
161    fn line_height(&self) -> u32 {
162        self.digit_size.height + self.digit_spacing
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169    use crate::SevenSegmentStyleBuilder;
170    use embedded_graphics::{mock_display::MockDisplay, pixelcolor::BinaryColor, text::Text};
171
172    fn test_digits(
173        character_style: SevenSegmentStyle<BinaryColor>,
174        digits: &str,
175        expected_pattern: &[&str],
176    ) {
177        let mut display = MockDisplay::new();
178
179        Text::with_baseline(digits, Point::zero(), character_style, Baseline::Top)
180            .draw(&mut display)
181            .unwrap();
182
183        display.assert_pattern(expected_pattern);
184    }
185
186    #[test]
187    fn digits_1px_9px() {
188        let style = SevenSegmentStyleBuilder::new()
189            .digit_size(Size::new(5, 9))
190            .digit_spacing(1)
191            .segment_width(1)
192            .segment_color(BinaryColor::On)
193            .build();
194
195        test_digits(
196            style,
197            "0123456789",
198            &[
199                " ###         ###   ###         ###   ###   ###   ###   ### ",
200                "#   #     #     #     # #   # #     #         # #   # #   #",
201                "#   #     #     #     # #   # #     #         # #   # #   #",
202                "#   #     #     #     # #   # #     #         # #   # #   #",
203                "             ###   ###   ###   ###   ###         ###   ### ",
204                "#   #     # #         #     #     # #   #     # #   #     #",
205                "#   #     # #         #     #     # #   #     # #   #     #",
206                "#   #     # #         #     #     # #   #     # #   #     #",
207                " ###         ###   ###         ###   ###         ###   ### ",
208            ],
209        );
210    }
211
212    #[test]
213    fn digits_1px_10px() {
214        let style = SevenSegmentStyleBuilder::new()
215            .digit_size(Size::new(5, 10))
216            .digit_spacing(1)
217            .segment_width(1)
218            .segment_color(BinaryColor::On)
219            .build();
220
221        test_digits(
222            style,
223            "0123456789",
224            &[
225                " ###         ###   ###         ###   ###   ###   ###   ### ",
226                "#   #     #     #     # #   # #     #         # #   # #   #",
227                "#   #     #     #     # #   # #     #         # #   # #   #",
228                "#   #     #     #     # #   # #     #         # #   # #   #",
229                "             ###   ###   ###   ###   ###         ###   ### ",
230                "#   #     # #         #     #     # #   #     # #   #     #",
231                "#   #     # #         #     #     # #   #     # #   #     #",
232                "#   #     # #         #     #     # #   #     # #   #     #",
233                "#   #     # #         #     #     # #   #     # #   #     #",
234                " ###         ###   ###         ###   ###         ###   ### ",
235            ],
236        );
237    }
238
239    #[test]
240    fn digits_2px_12px() {
241        let style = SevenSegmentStyleBuilder::new()
242            .digit_size(Size::new(7, 12))
243            .digit_spacing(1)
244            .segment_width(2)
245            .segment_color(BinaryColor::On)
246            .build();
247
248        test_digits(
249            style,
250            "01234",
251            &[
252                "  ###             ###     ###          ",
253                "  ###             ###     ###          ",
254                "##   ##      ##      ##      ## ##   ##",
255                "##   ##      ##      ##      ## ##   ##",
256                "##   ##      ##      ##      ## ##   ##",
257                "                  ###     ###     ###  ",
258                "                  ###     ###     ###  ",
259                "##   ##      ## ##           ##      ##",
260                "##   ##      ## ##           ##      ##",
261                "##   ##      ## ##           ##      ##",
262                "  ###             ###     ###          ",
263                "  ###             ###     ###          ",
264            ],
265        );
266
267        test_digits(
268            style,
269            "56789",
270            &[
271                "  ###     ###     ###     ###     ###  ",
272                "  ###     ###     ###     ###     ###  ",
273                "##      ##           ## ##   ## ##   ##",
274                "##      ##           ## ##   ## ##   ##",
275                "##      ##           ## ##   ## ##   ##",
276                "  ###     ###             ###     ###  ",
277                "  ###     ###             ###     ###  ",
278                "     ## ##   ##      ## ##   ##      ##",
279                "     ## ##   ##      ## ##   ##      ##",
280                "     ## ##   ##      ## ##   ##      ##",
281                "  ###     ###             ###     ###  ",
282                "  ###     ###             ###     ###  ",
283            ],
284        );
285    }
286
287    #[test]
288    fn digits_2px_13px() {
289        let style = SevenSegmentStyleBuilder::new()
290            .digit_size(Size::new(7, 13))
291            .digit_spacing(1)
292            .segment_width(2)
293            .segment_color(BinaryColor::On)
294            .build();
295
296        test_digits(
297            style,
298            "01234",
299            &[
300                "  ###             ###     ###          ",
301                "  ###             ###     ###          ",
302                "##   ##      ##      ##      ## ##   ##",
303                "##   ##      ##      ##      ## ##   ##",
304                "##   ##      ##      ##      ## ##   ##",
305                "                  ###     ###     ###  ",
306                "                  ###     ###     ###  ",
307                "##   ##      ## ##           ##      ##",
308                "##   ##      ## ##           ##      ##",
309                "##   ##      ## ##           ##      ##",
310                "##   ##      ## ##           ##      ##",
311                "  ###             ###     ###          ",
312                "  ###             ###     ###          ",
313            ],
314        );
315
316        test_digits(
317            style,
318            "56789",
319            &[
320                "  ###     ###     ###     ###     ###  ",
321                "  ###     ###     ###     ###     ###  ",
322                "##      ##           ## ##   ## ##   ##",
323                "##      ##           ## ##   ## ##   ##",
324                "##      ##           ## ##   ## ##   ##",
325                "  ###     ###             ###     ###  ",
326                "  ###     ###             ###     ###  ",
327                "     ## ##   ##      ## ##   ##      ##",
328                "     ## ##   ##      ## ##   ##      ##",
329                "     ## ##   ##      ## ##   ##      ##",
330                "     ## ##   ##      ## ##   ##      ##",
331                "  ###     ###             ###     ###  ",
332                "  ###     ###             ###     ###  ",
333            ],
334        );
335    }
336
337    #[test]
338    fn digits_3px_15px() {
339        let style = SevenSegmentStyleBuilder::new()
340            .digit_size(Size::new(9, 15))
341            .digit_spacing(1)
342            .segment_width(3)
343            .segment_color(BinaryColor::On)
344            .build();
345
346        test_digits(
347            style,
348            "01234",
349            &[
350                "   ###                 ###       ###             ",
351                "  #####               #####     #####            ",
352                " # ### #         #     ### #     ### #   #     # ",
353                "###   ###       ###       ###       ### ###   ###",
354                "###   ###       ###       ###       ### ###   ###",
355                "###   ###       ###       ###       ### ###   ###",
356                " #     #         #     ### #     ### #   # ### # ",
357                "                      #####     #####     #####  ",
358                " #     #         #   # ###       ### #     ### # ",
359                "###   ###       ### ###             ###       ###",
360                "###   ###       ### ###             ###       ###",
361                "###   ###       ### ###             ###       ###",
362                " # ### #         #   # ###       ### #         # ",
363                "  #####               #####     #####            ",
364                "   ###                 ###       ###             ",
365            ],
366        );
367
368        test_digits(
369            style,
370            "56789",
371            &[
372                "   ###       ###       ###       ###       ###   ",
373                "  #####     #####     #####     #####     #####  ",
374                " # ###     # ###       ### #   # ### #   # ### # ",
375                "###       ###             ### ###   ### ###   ###",
376                "###       ###             ### ###   ### ###   ###",
377                "###       ###             ### ###   ### ###   ###",
378                " # ###     # ###           #   # ### #   # ### # ",
379                "  #####     #####               #####     #####  ",
380                "   ### #   # ### #         #   # ### #     ### # ",
381                "      ### ###   ###       ### ###   ###       ###",
382                "      ### ###   ###       ### ###   ###       ###",
383                "      ### ###   ###       ### ###   ###       ###",
384                "   ### #   # ### #         #   # ### #     ### # ",
385                "  #####     #####               #####     #####  ",
386                "   ###       ###                 ###       ###   ",
387            ],
388        );
389    }
390
391    #[test]
392    fn digits_3px_16px() {
393        let style = SevenSegmentStyleBuilder::new()
394            .digit_size(Size::new(9, 16))
395            .digit_spacing(1)
396            .segment_width(3)
397            .segment_color(BinaryColor::On)
398            .build();
399
400        test_digits(
401            style,
402            "01234",
403            &[
404                "   ###                 ###       ###             ",
405                "  #####               #####     #####            ",
406                " # ### #         #     ### #     ### #   #     # ",
407                "###   ###       ###       ###       ### ###   ###",
408                "###   ###       ###       ###       ### ###   ###",
409                "###   ###       ###       ###       ### ###   ###",
410                " #     #         #     ### #     ### #   # ### # ",
411                "                      #####     #####     #####  ",
412                " #     #         #   # ###       ### #     ### # ",
413                "###   ###       ### ###             ###       ###",
414                "###   ###       ### ###             ###       ###",
415                "###   ###       ### ###             ###       ###",
416                "###   ###       ### ###             ###       ###",
417                " # ### #         #   # ###       ### #         # ",
418                "  #####               #####     #####            ",
419                "   ###                 ###       ###             ",
420            ],
421        );
422
423        test_digits(
424            style,
425            "56789",
426            &[
427                "   ###       ###       ###       ###       ###   ",
428                "  #####     #####     #####     #####     #####  ",
429                " # ###     # ###       ### #   # ### #   # ### # ",
430                "###       ###             ### ###   ### ###   ###",
431                "###       ###             ### ###   ### ###   ###",
432                "###       ###             ### ###   ### ###   ###",
433                " # ###     # ###           #   # ### #   # ### # ",
434                "  #####     #####               #####     #####  ",
435                "   ### #   # ### #         #   # ### #     ### # ",
436                "      ### ###   ###       ### ###   ###       ###",
437                "      ### ###   ###       ### ###   ###       ###",
438                "      ### ###   ###       ### ###   ###       ###",
439                "      ### ###   ###       ### ###   ###       ###",
440                "   ### #   # ### #         #   # ### #     ### # ",
441                "  #####     #####               #####     #####  ",
442                "   ###       ###                 ###       ###   ",
443            ],
444        );
445    }
446
447    #[test]
448    fn chaining() {
449        let style1 = SevenSegmentStyleBuilder::new()
450            .digit_size(Size::new(5, 9))
451            .digit_spacing(1)
452            .segment_width(1)
453            .segment_color(BinaryColor::On)
454            .build();
455
456        let style2 = SevenSegmentStyleBuilder::from(&style1)
457            .digit_size(Size::new(7, 11))
458            .segment_color(BinaryColor::Off)
459            .build();
460
461        let mut display = MockDisplay::new();
462        let p = Point::new(0, 10);
463        let next = Text::new("12", p, style1).draw(&mut display).unwrap();
464        Text::new("3", next, style2).draw(&mut display).unwrap();
465
466        display.assert_pattern(&[
467            "             ..... ",
468            "                  .",
469            "       ###        .",
470            "    #     #       .",
471            "    #     #       .",
472            "    #     #  ..... ",
473            "       ###        .",
474            "    # #           .",
475            "    # #           .",
476            "    # #           .",
477            "       ###   ..... ",
478        ])
479    }
480
481    #[test]
482    fn chaining_with_colon() {
483        let style1 = SevenSegmentStyleBuilder::new()
484            .digit_size(Size::new(5, 9))
485            .digit_spacing(1)
486            .segment_width(1)
487            .segment_color(BinaryColor::On)
488            .build();
489
490        let style2 = SevenSegmentStyleBuilder::from(&style1)
491            .digit_size(Size::new(7, 11))
492            .segment_color(BinaryColor::Off)
493            .build();
494
495        let mut display = MockDisplay::new();
496        let p = Point::new(0, 10);
497        let next = Text::new("1:2", p, style1).draw(&mut display).unwrap();
498        Text::new("3", next, style2).draw(&mut display).unwrap();
499
500        display.assert_pattern(&[
501            "               ..... ",
502            "                    .",
503            "         ###        .",
504            "    #       #       .",
505            "    #       #       .",
506            "    # #     #  ..... ",
507            "         ###        .",
508            "    #   #           .",
509            "    # # #           .",
510            "    #   #           .",
511            "         ###   ..... ",
512        ])
513    }
514
515    #[test]
516    fn multiple_lines() {
517        let style = SevenSegmentStyleBuilder::new()
518            .digit_size(Size::new(5, 9))
519            .digit_spacing(2)
520            .segment_width(1)
521            .segment_color(BinaryColor::On)
522            .build();
523
524        test_digits(
525            style,
526            "12\n3",
527            &[
528                "        ### ",
529                "    #      #",
530                "    #      #",
531                "    #      #",
532                "        ### ",
533                "    #  #    ",
534                "    #  #    ",
535                "    #  #    ",
536                "        ### ",
537                "            ",
538                "            ",
539                " ###        ",
540                "    #       ",
541                "    #       ",
542                "    #       ",
543                " ###        ",
544                "    #       ",
545                "    #       ",
546                "    #       ",
547                " ###        ",
548            ],
549        );
550    }
551
552    fn test_baseline(baseline: Baseline, expected_pattern: &[&str]) {
553        let style = SevenSegmentStyleBuilder::new()
554            .digit_size(Size::new(5, 9))
555            .digit_spacing(2)
556            .segment_width(1)
557            .segment_color(BinaryColor::On)
558            .build();
559
560        let mut display = MockDisplay::new();
561        Text::with_baseline("8", Point::new(0, 8), style, baseline)
562            .draw(&mut display)
563            .unwrap();
564
565        display.assert_pattern(expected_pattern);
566    }
567
568    #[test]
569    fn baseline_top() {
570        test_baseline(
571            Baseline::Top,
572            &[
573                "     ", //
574                "     ", //
575                "     ", //
576                "     ", //
577                "     ", //
578                "     ", //
579                "     ", //
580                "     ", //
581                " ### ", // ###
582                "#   #", //
583                "#   #", //
584                "#   #", //
585                " ### ", //
586                "#   #", //
587                "#   #", //
588                "#   #", //
589                " ### ", //
590            ],
591        );
592    }
593
594    #[test]
595    fn baseline_middle() {
596        test_baseline(
597            Baseline::Middle,
598            &[
599                "     ", //
600                "     ", //
601                "     ", //
602                "     ", //
603                " ### ", //
604                "#   #", //
605                "#   #", //
606                "#   #", //
607                " ### ", // ###
608                "#   #", //
609                "#   #", //
610                "#   #", //
611                " ### ", //
612            ],
613        );
614    }
615
616    #[test]
617    fn baseline_bottom() {
618        test_baseline(
619            Baseline::Bottom,
620            &[
621                " ### ", //
622                "#   #", //
623                "#   #", //
624                "#   #", //
625                " ### ", //
626                "#   #", //
627                "#   #", //
628                "#   #", //
629                " ### ", // ###
630            ],
631        );
632    }
633
634    #[test]
635    fn baseline_alphabetic() {
636        test_baseline(
637            Baseline::Alphabetic,
638            &[
639                " ### ", //
640                "#   #", //
641                "#   #", //
642                "#   #", //
643                " ### ", //
644                "#   #", //
645                "#   #", //
646                "#   #", //
647                " ### ", // ###
648            ],
649        );
650    }
651
652    #[test]
653    fn measure_string() {
654        let style = SevenSegmentStyleBuilder::new()
655            .digit_size(Size::new(7, 12))
656            .digit_spacing(1)
657            .segment_width(2)
658            .segment_color(BinaryColor::On)
659            .build();
660
661        let position = Point::new(1, 2);
662
663        let metrics = style.measure_string("12", position, Baseline::Top);
664        assert_eq!(
665            metrics.bounding_box,
666            Rectangle::new(
667                position,
668                style.digit_size.component_mul(Size::new(2, 1)) + Size::new(style.digit_spacing, 0)
669            )
670        );
671        assert_eq!(
672            metrics.next_position,
673            position + metrics.bounding_box.size.x_axis()
674        );
675    }
676
677    #[test]
678    fn measure_string_with_colon() {
679        let style = SevenSegmentStyleBuilder::new()
680            .digit_size(Size::new(7, 12))
681            .digit_spacing(1)
682            .segment_width(2)
683            .segment_color(BinaryColor::On)
684            .build();
685
686        let position = Point::new(1, 2);
687
688        let metrics = style.measure_string("1:2", position, Baseline::Top);
689        assert_eq!(
690            metrics.bounding_box,
691            Rectangle::new(
692                position,
693                style.digit_size.component_mul(Size::new(2, 1))
694                    + Size::new(style.digit_spacing * 2 + style.segment_width, 0)
695            )
696        );
697        assert_eq!(
698            metrics.next_position,
699            position + metrics.bounding_box.size.x_axis()
700        );
701    }
702
703    #[test]
704    fn invalid_char() {
705        let style = SevenSegmentStyleBuilder::new()
706            .digit_size(Size::new(5, 9))
707            .digit_spacing(1)
708            .segment_width(1)
709            .segment_color(BinaryColor::On)
710            .build();
711
712        test_digits(
713            style,
714            "0W1",
715            &[
716                " ###             ",
717                "#   #           #",
718                "#   #           #",
719                "#   #           #",
720                "                 ",
721                "#   #           #",
722                "#   #           #",
723                "#   #           #",
724                " ###             ",
725            ],
726        );
727    }
728}