1 | use 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 | }; |
12 | use az::SaturatingAs; |
13 | |
14 | use 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))] |
22 | pub 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 | |
36 | impl<'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 | |
93 | impl<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 | |
108 | impl<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 | |
153 | impl<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: Point = self.position; |
162 | |
163 | for (line: &str, position: Point) in self.lines() { |
164 | next_position = self.character_style.draw_string( |
165 | text:line, |
166 | position, |
167 | self.text_style.baseline, |
168 | target, |
169 | )?; |
170 | } |
171 | |
172 | Ok(next_position) |
173 | } |
174 | } |
175 | |
176 | fn update_min_max(min_max: &mut Option<(Point, Point)>, metrics: &TextMetrics) { |
177 | if let Some(bottom_right: Point) = metrics.bounding_box.bottom_right() { |
178 | if let Some((min: &mut Point, max: &mut Point)) = 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 | |
189 | impl<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: &str, position: Point) in self.lines() { |
194 | let metrics: TextMetrics = |
195 | self.character_style |
196 | .measure_string(text:line, position, self.text_style.baseline); |
197 | update_min_max(&mut min_max, &metrics); |
198 | } |
199 | |
200 | if let Some((min: Point, max: Point)) = min_max { |
201 | Rectangle::with_corners(corner_1:min, corner_2:max) |
202 | } else { |
203 | Rectangle::new(self.position, Size::zero()) |
204 | } |
205 | } |
206 | } |
207 | |
208 | #[cfg (test)] |
209 | mod 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 | } |
783 | |