embedded_graphics/text/
text.rs

1use crate::{
2    draw_target::DrawTarget,
3    geometry::{Dimensions, Point, Size},
4    primitives::Rectangle,
5    text::{
6        renderer::{TextMetrics, TextRenderer},
7        Alignment, Baseline, TextStyle,
8    },
9    transform::Transform,
10    Drawable,
11};
12use az::SaturatingAs;
13
14use super::TextStyleBuilder;
15/// Text drawable.
16///
17/// A text drawable can be used to draw text to a draw target.
18///
19/// See the [module-level documentation](super) for more information about text drawables and examples.
20#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
21#[cfg_attr(feature = "defmt", derive(::defmt::Format))]
22pub struct Text<'a, S> {
23    /// The string.
24    pub text: &'a str,
25
26    /// The position.
27    pub position: Point,
28
29    /// The character style.
30    pub character_style: S,
31
32    /// The text style.
33    pub text_style: TextStyle,
34}
35
36impl<'a, S> Text<'a, S> {
37    /// Creates a text drawable with the default text style.
38    pub const fn new(text: &'a str, position: Point, character_style: S) -> Self {
39        Self {
40            text,
41            position,
42            character_style,
43            text_style: TextStyleBuilder::new().build(),
44        }
45    }
46
47    /// Creates a text drawable with the given text style.
48    pub const fn with_text_style(
49        text: &'a str,
50        position: Point,
51        character_style: S,
52        text_style: TextStyle,
53    ) -> Self {
54        Self {
55            text,
56            position,
57            character_style,
58            text_style,
59        }
60    }
61
62    /// Creates a text drawable with the given baseline.
63    pub const fn with_baseline(
64        text: &'a str,
65        position: Point,
66        character_style: S,
67        baseline: Baseline,
68    ) -> Self {
69        Self {
70            text,
71            position,
72            character_style,
73            text_style: TextStyle::with_baseline(baseline),
74        }
75    }
76
77    /// Creates a text drawable with the given alignment.
78    pub const fn with_alignment(
79        text: &'a str,
80        position: Point,
81        character_style: S,
82        alignment: Alignment,
83    ) -> Self {
84        Self {
85            text,
86            position,
87            character_style,
88            text_style: TextStyle::with_alignment(alignment),
89        }
90    }
91}
92
93impl<S: Clone> Transform for Text<'_, S> {
94    fn translate(&self, by: Point) -> Self {
95        Self {
96            position: self.position + by,
97            ..self.clone()
98        }
99    }
100
101    fn translate_mut(&mut self, by: Point) -> &mut Self {
102        self.position += by;
103
104        self
105    }
106}
107
108impl<S: TextRenderer> Text<'_, S> {
109    fn line_height(&self) -> i32 {
110        self.text_style
111            .line_height
112            .to_absolute(self.character_style.line_height())
113            .saturating_as::<i32>()
114    }
115
116    fn lines(&self) -> impl Iterator<Item = (&str, Point)> {
117        let mut position = self.position;
118
119        self.text.split('\n').map(move |line| {
120            let p = match self.text_style.alignment {
121                Alignment::Left => position,
122                Alignment::Right => {
123                    let metrics = self.character_style.measure_string(
124                        line,
125                        Point::zero(),
126                        self.text_style.baseline,
127                    );
128                    position - (metrics.next_position - Point::new(1, 0))
129                }
130                Alignment::Center => {
131                    let metrics = self.character_style.measure_string(
132                        line,
133                        Point::zero(),
134                        self.text_style.baseline,
135                    );
136                    position - (metrics.next_position - Point::new(1, 0)) / 2
137                }
138            };
139
140            position.y += self.line_height();
141
142            // remove trailing '\r' for '\r\n' line endings
143            let len = line.len();
144            if len > 0 && line.as_bytes()[len - 1] == b'\r' {
145                (&line[0..len - 1], p)
146            } else {
147                (line, p)
148            }
149        })
150    }
151}
152
153impl<S: TextRenderer> Drawable for Text<'_, S> {
154    type Color = S::Color;
155    type Output = Point;
156
157    fn draw<D>(&self, target: &mut D) -> Result<Point, D::Error>
158    where
159        D: DrawTarget<Color = Self::Color>,
160    {
161        let mut next_position = self.position;
162
163        for (line, position) in self.lines() {
164            next_position = self.character_style.draw_string(
165                line,
166                position,
167                self.text_style.baseline,
168                target,
169            )?;
170        }
171
172        Ok(next_position)
173    }
174}
175
176fn update_min_max(min_max: &mut Option<(Point, Point)>, metrics: &TextMetrics) {
177    if let Some(bottom_right) = metrics.bounding_box.bottom_right() {
178        if let Some((min, max)) = min_max {
179            min.x = min.x.min(metrics.bounding_box.top_left.x);
180            min.y = min.y.min(metrics.bounding_box.top_left.y);
181            max.x = max.x.max(bottom_right.x);
182            max.y = max.y.max(bottom_right.y);
183        } else {
184            *min_max = Some((metrics.bounding_box.top_left, bottom_right));
185        }
186    }
187}
188
189impl<S: TextRenderer> Dimensions for Text<'_, S> {
190    fn bounding_box(&self) -> Rectangle {
191        let mut min_max: Option<(Point, Point)> = None;
192
193        for (line, position) in self.lines() {
194            let metrics =
195                self.character_style
196                    .measure_string(line, position, self.text_style.baseline);
197            update_min_max(&mut min_max, &metrics);
198        }
199
200        if let Some((min, max)) = min_max {
201            Rectangle::with_corners(min, max)
202        } else {
203            Rectangle::new(self.position, Size::zero())
204        }
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211    use crate::{
212        geometry::Size,
213        mock_display::MockDisplay,
214        mono_font::{
215            ascii::{FONT_6X13, FONT_6X9},
216            tests::assert_text_from_pattern,
217            MonoTextStyle, MonoTextStyleBuilder,
218        },
219        pixelcolor::BinaryColor,
220        primitives::{Primitive, PrimitiveStyle},
221        text::{Alignment, Baseline, LineHeight, TextStyleBuilder},
222    };
223
224    const HELLO_WORLD: &'static str = "Hello World!";
225
226    #[test]
227    fn constructor() {
228        let character_style = MonoTextStyle::new(&FONT_6X9, BinaryColor::On);
229
230        let text = Text::new("Hello e-g", Point::new(10, 11), character_style);
231
232        assert_eq!(
233            text,
234            Text {
235                text: "Hello e-g",
236                position: Point::new(10, 11),
237                character_style,
238                text_style: TextStyle::default(),
239            }
240        );
241    }
242
243    #[test]
244    fn multiline() {
245        assert_text_from_pattern(
246            "AB\nC",
247            &FONT_6X9,
248            &[
249                "            ",
250                "  #   ####  ",
251                " # #  #   # ",
252                "#   # ####  ",
253                "##### #   # ",
254                "#   # #   # ",
255                "#   # ####  ",
256                "            ",
257                "            ",
258                "            ",
259                "  ##        ",
260                " #  #       ",
261                " #          ",
262                " #          ",
263                " #  #       ",
264                "  ##        ",
265            ],
266        );
267    }
268
269    #[test]
270    fn multiline_empty_line() {
271        assert_text_from_pattern(
272            "A\n\nBC",
273            &FONT_6X9,
274            &[
275                "            ",
276                "  #         ",
277                " # #        ",
278                "#   #       ",
279                "#####       ",
280                "#   #       ",
281                "#   #       ",
282                "            ",
283                "            ",
284                "            ",
285                "            ",
286                "            ",
287                "            ",
288                "            ",
289                "            ",
290                "            ",
291                "            ",
292                "            ",
293                "            ",
294                "####    ##  ",
295                "#   #  #  # ",
296                "####   #    ",
297                "#   #  #    ",
298                "#   #  #  # ",
299                "####    ##  ",
300                "            ",
301            ],
302        );
303    }
304
305    #[test]
306    fn multiline_dimensions() {
307        let character_style = MonoTextStyleBuilder::new()
308            .font(&FONT_6X9)
309            .text_color(BinaryColor::On)
310            .build();
311
312        let text = Text::with_baseline("AB\nC", Point::zero(), character_style, Baseline::Top);
313
314        assert_eq!(
315            text.bounding_box(),
316            Rectangle::new(Point::zero(), Size::new(2 * 6, 2 * 9))
317        );
318    }
319
320    #[test]
321    fn multiline_trailing_newline() {
322        let character_style = MonoTextStyleBuilder::new()
323            .font(&FONT_6X9)
324            .text_color(BinaryColor::On)
325            .build();
326
327        let mut single_text_display = MockDisplay::new();
328        Text::with_baseline("AB\nC", Point::zero(), character_style, Baseline::Top)
329            .draw(&mut single_text_display)
330            .unwrap();
331
332        let mut multiple_text_display = MockDisplay::new();
333        let pos = Text::with_baseline("AB\n", Point::zero(), character_style, Baseline::Top)
334            .draw(&mut multiple_text_display)
335            .unwrap();
336        Text::with_baseline("C", pos, character_style, Baseline::Top)
337            .draw(&mut multiple_text_display)
338            .unwrap();
339
340        assert_eq!(single_text_display, multiple_text_display);
341    }
342
343    #[test]
344    fn line_endings() {
345        let character_style = MonoTextStyleBuilder::new()
346            .font(&FONT_6X9)
347            .text_color(BinaryColor::On)
348            .build();
349
350        let mut cr_lf_display = MockDisplay::new();
351        Text::with_baseline("AB\r\nC", Point::zero(), character_style, Baseline::Top)
352            .draw(&mut cr_lf_display)
353            .unwrap();
354
355        let mut lf_display = MockDisplay::new();
356        Text::with_baseline("AB\nC", Point::zero(), character_style, Baseline::Top)
357            .draw(&mut lf_display)
358            .unwrap();
359
360        assert_eq!(cr_lf_display, lf_display);
361    }
362
363    #[test]
364    fn position_and_translate() {
365        let style = MonoTextStyle::new(&FONT_6X9, BinaryColor::On);
366
367        let hello = Text::new(HELLO_WORLD, Point::zero(), style);
368
369        let hello_translated = hello.translate(Point::new(5, -20));
370        assert_eq!(
371            hello.bounding_box().size,
372            hello_translated.bounding_box().size
373        );
374
375        let mut hello_with_point = Text::new(HELLO_WORLD, Point::new(5, -20), style);
376        assert_eq!(hello_translated, hello_with_point);
377
378        hello_with_point.translate_mut(Point::new(-5, 20));
379        assert_eq!(hello, hello_with_point);
380    }
381
382    #[test]
383    fn inverted_text() {
384        let mut display_inverse = MockDisplay::new();
385        let style_inverse = MonoTextStyleBuilder::new()
386            .font(&FONT_6X9)
387            .text_color(BinaryColor::Off)
388            .background_color(BinaryColor::On)
389            .build();
390        Text::new("Mm", Point::new(0, 7), style_inverse)
391            .draw(&mut display_inverse)
392            .unwrap();
393
394        let mut display_normal = MockDisplay::new();
395        let style_normal = MonoTextStyleBuilder::new()
396            .font(&FONT_6X9)
397            .text_color(BinaryColor::On)
398            .background_color(BinaryColor::Off)
399            .build();
400        Text::new("Mm", Point::new(0, 7), style_normal)
401            .draw(&mut display_normal)
402            .unwrap();
403
404        display_inverse.assert_eq(&display_normal.map(|c| c.invert()));
405    }
406
407    #[test]
408    fn no_fill_does_not_hang() {
409        let mut display = MockDisplay::new();
410        Text::new(
411            " ",
412            Point::zero(),
413            MonoTextStyle::new(&FONT_6X9, BinaryColor::On),
414        )
415        .draw(&mut display)
416        .unwrap();
417
418        display.assert_eq(&MockDisplay::new());
419    }
420
421    #[test]
422    fn transparent_text_color_does_not_overwrite_background() {
423        let character_style = MonoTextStyleBuilder::new()
424            .font(&FONT_6X9)
425            .background_color(BinaryColor::On)
426            .build();
427
428        let mut display = MockDisplay::new();
429        display.set_allow_overdraw(true);
430
431        // Draw a background for the first character
432        Rectangle::new(Point::zero(), Size::new(6, 8))
433            .into_styled(PrimitiveStyle::with_fill(BinaryColor::Off))
434            .draw(&mut display)
435            .unwrap();
436
437        Text::with_baseline("AA", Point::zero(), character_style, Baseline::Top)
438            .draw(&mut display)
439            .unwrap();
440
441        display.assert_pattern(&[
442            "############",
443            "##.##### ###",
444            "#.#.### # ##",
445            ".###.# ### #",
446            ".....#     #",
447            ".###.# ### #",
448            ".###.# ### #",
449            "############",
450            "############",
451        ]);
452    }
453
454    #[test]
455    #[ignore]
456    fn transparent_text_has_zero_size_but_retains_position() {
457        let style = MonoTextStyleBuilder::<BinaryColor>::new()
458            .font(&FONT_6X9)
459            .build();
460
461        let styled = Text::new(" A", Point::new(7, 11), style);
462
463        assert_eq!(
464            styled.bounding_box(),
465            Rectangle::new(Point::new(7, 11), Size::zero()),
466            "Transparent text is expected to have a zero sized bounding box with the top left corner at the text position",
467        );
468    }
469
470    #[test]
471    fn alignment_left() {
472        let character_style = MonoTextStyleBuilder::new()
473            .font(&FONT_6X9)
474            .text_color(BinaryColor::On)
475            .build();
476
477        let text_style = TextStyleBuilder::new()
478            .alignment(Alignment::Left)
479            .baseline(Baseline::Top)
480            .build();
481
482        let mut display = MockDisplay::new();
483        Text::with_text_style("A\nBC", Point::new(0, 0), character_style, text_style)
484            .draw(&mut display)
485            .unwrap();
486
487        display.assert_pattern(&[
488            "            ",
489            "  #         ",
490            " # #        ",
491            "#   #       ",
492            "#####       ",
493            "#   #       ",
494            "#   #       ",
495            "            ",
496            "            ",
497            "            ",
498            "####    ##  ",
499            "#   #  #  # ",
500            "####   #    ",
501            "#   #  #    ",
502            "#   #  #  # ",
503            "####    ##  ",
504            "            ",
505        ]);
506    }
507
508    #[test]
509    fn alignment_center() {
510        let character_style = MonoTextStyleBuilder::new()
511            .font(&FONT_6X9)
512            .text_color(BinaryColor::On)
513            .build();
514
515        let text_style = TextStyleBuilder::new()
516            .alignment(Alignment::Center)
517            .baseline(Baseline::Top)
518            .build();
519
520        let mut display = MockDisplay::new();
521        Text::with_text_style("A\nBC", Point::new(5, 0), character_style, text_style)
522            .draw(&mut display)
523            .unwrap();
524
525        display.assert_pattern(&[
526            "            ",
527            "     #      ",
528            "    # #     ",
529            "   #   #    ",
530            "   #####    ",
531            "   #   #    ",
532            "   #   #    ",
533            "            ",
534            "            ",
535            "            ",
536            "####    ##  ",
537            "#   #  #  # ",
538            "####   #    ",
539            "#   #  #    ",
540            "#   #  #  # ",
541            "####    ##  ",
542            "            ",
543        ]);
544    }
545
546    #[test]
547    fn horizontal_alignment_right() {
548        let character_style = MonoTextStyleBuilder::new()
549            .font(&FONT_6X9)
550            .text_color(BinaryColor::On)
551            .build();
552
553        let text_style = TextStyleBuilder::new()
554            .alignment(Alignment::Right)
555            .baseline(Baseline::Top)
556            .build();
557
558        let mut display = MockDisplay::new();
559        Text::with_text_style("A\nBC", Point::new(11, 0), character_style, text_style)
560            .draw(&mut display)
561            .unwrap();
562
563        display.assert_pattern(&[
564            "            ",
565            "        #   ",
566            "       # #  ",
567            "      #   # ",
568            "      ##### ",
569            "      #   # ",
570            "      #   # ",
571            "            ",
572            "            ",
573            "            ",
574            "####    ##  ",
575            "#   #  #  # ",
576            "####   #    ",
577            "#   #  #    ",
578            "#   #  #  # ",
579            "####    ##  ",
580            "            ",
581        ]);
582    }
583
584    #[test]
585    fn baseline() {
586        let mut display = MockDisplay::new();
587
588        let character_style = MonoTextStyleBuilder::new()
589            .font(&FONT_6X9)
590            .text_color(BinaryColor::On)
591            .build();
592
593        Text::with_baseline("t", Point::new(0, 8), character_style, Baseline::Top)
594            .draw(&mut display)
595            .unwrap();
596        Text::with_baseline("m", Point::new(6, 8), character_style, Baseline::Middle)
597            .draw(&mut display)
598            .unwrap();
599        Text::with_baseline("b", Point::new(12, 8), character_style, Baseline::Bottom)
600            .draw(&mut display)
601            .unwrap();
602        Text::with_baseline(
603            "B",
604            Point::new(18, 8),
605            character_style,
606            Baseline::Alphabetic,
607        )
608        .draw(&mut display)
609        .unwrap();
610
611        display.assert_pattern(&[
612            "                       ",
613            "             #         ",
614            "             #         ",
615            "             ###  #### ",
616            "             #  # #   #",
617            "             #  # #### ",
618            "             ###  #   #",
619            "      ## #        #   #",
620            "      # # #       #### ",
621            "  #   # # #            ",
622            "  #   #   #            ",
623            " ###                   ",
624            "  #                    ",
625            "  # #                  ",
626            "   #                   ",
627        ]);
628    }
629
630    #[test]
631    fn bounding_box() {
632        for &baseline in &[
633            Baseline::Top,
634            Baseline::Middle,
635            Baseline::Bottom,
636            Baseline::Alphabetic,
637        ] {
638            for &alignment in &[Alignment::Left, Alignment::Center, Alignment::Right] {
639                let character_style = MonoTextStyleBuilder::new()
640                    .font(&FONT_6X9)
641                    .text_color(BinaryColor::On)
642                    .background_color(BinaryColor::Off)
643                    .build();
644
645                let text_style = TextStyleBuilder::new()
646                    .alignment(alignment)
647                    .baseline(baseline)
648                    .build();
649
650                let text = Text::with_text_style(
651                    "1\n23",
652                    Point::new_equal(20),
653                    character_style,
654                    text_style,
655                );
656
657                let mut display = MockDisplay::new();
658                text.draw(&mut display).unwrap();
659
660                assert_eq!(
661                    display.affected_area(),
662                    text.bounding_box(),
663                    "alignment: {:?}, baseline: {:?}",
664                    alignment,
665                    baseline
666                );
667            }
668        }
669    }
670
671    #[test]
672    fn chained_text_drawing() {
673        let character_style1 = MonoTextStyleBuilder::new()
674            .font(&FONT_6X9)
675            .text_color(BinaryColor::On)
676            .build();
677
678        let character_style2 = MonoTextStyleBuilder::new()
679            .font(&FONT_6X13)
680            .text_color(BinaryColor::Off)
681            .build();
682
683        let mut display = MockDisplay::new();
684        let next = Text::new("AB", Point::new(0, 8), character_style1)
685            .draw(&mut display)
686            .unwrap();
687        Text::new("C", next, character_style2)
688            .draw(&mut display)
689            .unwrap();
690
691        display.assert_pattern(&[
692            "             ...  ",
693            "            .   . ",
694            "            .     ",
695            "  #   ####  .     ",
696            " # #  #   # .     ",
697            "#   # ####  .     ",
698            "##### #   # .     ",
699            "#   # #   # .   . ",
700            "#   # ####   ...  ",
701        ]);
702    }
703
704    #[test]
705    fn line_height_pixels() {
706        let character_style = MonoTextStyleBuilder::new()
707            .font(&FONT_6X9)
708            .text_color(BinaryColor::On)
709            .build();
710
711        let text_style = TextStyleBuilder::new()
712            .line_height(LineHeight::Pixels(7))
713            .build();
714
715        let mut display = MockDisplay::new();
716        Text::with_text_style("A\nB", Point::new(0, 5), character_style, text_style)
717            .draw(&mut display)
718            .unwrap();
719
720        display.assert_pattern(&[
721            "  #  ", //
722            " # # ", //
723            "#   #", //
724            "#####", //
725            "#   #", //
726            "#   #", //
727            "     ", //
728            "#### ", //
729            "#   #", //
730            "#### ", //
731            "#   #", //
732            "#   #", //
733            "#### ", //
734        ]);
735    }
736
737    #[test]
738    fn line_height_percent() {
739        let character_style = MonoTextStyleBuilder::new()
740            .font(&FONT_6X9)
741            .text_color(BinaryColor::On)
742            .build();
743
744        let text_style = TextStyleBuilder::new()
745            .baseline(Baseline::Top)
746            .line_height(LineHeight::Percent(200))
747            .build();
748
749        let mut display = MockDisplay::new();
750        Text::with_text_style("A\nBC", Point::zero(), character_style, text_style)
751            .draw(&mut display)
752            .unwrap();
753
754        display.assert_pattern(&[
755            "            ",
756            "  #         ",
757            " # #        ",
758            "#   #       ",
759            "#####       ",
760            "#   #       ",
761            "#   #       ",
762            "            ",
763            "            ",
764            "            ",
765            "            ",
766            "            ",
767            "            ",
768            "            ",
769            "            ",
770            "            ",
771            "            ",
772            "            ",
773            "            ",
774            "####    ##  ",
775            "#   #  #  # ",
776            "####   #    ",
777            "#   #  #    ",
778            "#   #  #  # ",
779            "####    ##  ",
780        ]);
781    }
782}