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#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
21#[cfg_attr(feature = "defmt", derive(::defmt::Format))]
22pub struct Text<'a, S> {
23 pub text: &'a str,
25
26 pub position: Point,
28
29 pub character_style: S,
31
32 pub text_style: TextStyle,
34}
35
36impl<'a, S> Text<'a, S> {
37 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 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 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 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 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 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 " # ", " # # ", "# #", "#####", "# #", "# #", " ", "#### ", "# #", "#### ", "# #", "# #", "#### ", ]);
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}