1 | use crate::{ |
2 | draw_target::DrawTarget, |
3 | geometry::{Point, Size}, |
4 | image::Image, |
5 | mono_font::{ |
6 | draw_target::{Background, Both, Foreground, MonoFontDrawTarget}, |
7 | MonoFont, |
8 | }, |
9 | pixelcolor::{BinaryColor, PixelColor}, |
10 | primitives::Rectangle, |
11 | text::{ |
12 | renderer::{CharacterStyle, TextMetrics, TextRenderer}, |
13 | Baseline, DecorationColor, |
14 | }, |
15 | Drawable, |
16 | }; |
17 | use az::SaturatingAs; |
18 | |
19 | /// Style properties for text using a monospaced font. |
20 | /// |
21 | /// A `MonoTextStyle` can be applied to a [`Text`] object to define how the text is drawn. |
22 | /// |
23 | /// Because `MonoTextStyle` has the [`non_exhaustive`] attribute, it cannot be created using a |
24 | /// struct literal. To create a `MonoTextStyle` with a given text color and transparent |
25 | /// background, use the [`new`] method. For more complex text styles, use the |
26 | /// [`MonoTextStyleBuilder`]. |
27 | /// |
28 | /// [`Text`]: crate::text::Text |
29 | /// [`non_exhaustive`]: https://blog.rust-lang.org/2019/12/19/Rust-1.40.0.html#[non_exhaustive]-structs,-enums,-and-variants |
30 | /// [`new`]: MonoTextStyle::new() |
31 | #[derive (Copy, Clone, Debug, PartialEq)] |
32 | #[cfg_attr (feature = "defmt" , derive(::defmt::Format))] |
33 | #[non_exhaustive ] |
34 | pub struct MonoTextStyle<'a, C> { |
35 | /// Text color. |
36 | pub text_color: Option<C>, |
37 | |
38 | /// Background color. |
39 | pub background_color: Option<C>, |
40 | |
41 | /// Underline color. |
42 | pub underline_color: DecorationColor<C>, |
43 | |
44 | /// Strikethrough color. |
45 | pub strikethrough_color: DecorationColor<C>, |
46 | |
47 | /// Font. |
48 | pub font: &'a MonoFont<'a>, |
49 | } |
50 | |
51 | impl<'a, C> MonoTextStyle<'a, C> |
52 | where |
53 | C: PixelColor, |
54 | { |
55 | /// Creates a text style with transparent background. |
56 | pub const fn new(font: &'a MonoFont<'a>, text_color: C) -> Self { |
57 | MonoTextStyleBuilder::new() |
58 | .font(font) |
59 | .text_color(text_color) |
60 | .build() |
61 | } |
62 | |
63 | /// Returns `true` if the style is transparent. |
64 | /// |
65 | /// Drawing a `Text` with a transparent `MonoTextStyle` will not draw any pixels. |
66 | /// |
67 | /// [`Text`]: super::text::Text |
68 | pub fn is_transparent(&self) -> bool { |
69 | self.text_color.is_none() |
70 | && self.background_color.is_none() |
71 | && self.underline_color.is_none() |
72 | && self.strikethrough_color.is_none() |
73 | } |
74 | |
75 | fn line_elements<'t>( |
76 | &self, |
77 | mut position: Point, |
78 | text: &'t str, |
79 | ) -> impl Iterator<Item = (Point, LineElement)> + 't { |
80 | let char_width = self.font.character_size.width as i32; |
81 | let spacing_width = self.font.character_spacing as i32; |
82 | |
83 | let mut chars = text.chars(); |
84 | let mut next_char = chars.next(); |
85 | let mut add_spacing = false; |
86 | |
87 | core::iter::from_fn(move || { |
88 | if add_spacing { |
89 | let p = position; |
90 | position.x += spacing_width; |
91 | |
92 | add_spacing = false; |
93 | |
94 | Some((p, LineElement::Spacing)) |
95 | } else if let Some(c) = next_char { |
96 | let p = position; |
97 | position.x += char_width; |
98 | |
99 | next_char = chars.next(); |
100 | add_spacing = next_char.is_some(); |
101 | |
102 | Some((p, LineElement::Char(c))) |
103 | } else { |
104 | Some((position, LineElement::Done)) |
105 | } |
106 | }) |
107 | } |
108 | |
109 | fn draw_decorations<D>( |
110 | &self, |
111 | width: u32, |
112 | position: Point, |
113 | target: &mut D, |
114 | ) -> Result<(), D::Error> |
115 | where |
116 | D: DrawTarget<Color = C>, |
117 | { |
118 | if let Some(color) = self.strikethrough_color.to_color(self.text_color) { |
119 | let rect = self.font.strikethrough.to_rectangle(position, width); |
120 | target.fill_solid(&rect, color)?; |
121 | } |
122 | |
123 | if let Some(color) = self.underline_color.to_color(self.text_color) { |
124 | let rect = self.font.underline.to_rectangle(position, width); |
125 | target.fill_solid(&rect, color)?; |
126 | } |
127 | |
128 | Ok(()) |
129 | } |
130 | |
131 | fn draw_string_binary<D>( |
132 | &self, |
133 | text: &str, |
134 | position: Point, |
135 | mut target: D, |
136 | ) -> Result<Point, D::Error> |
137 | where |
138 | D: DrawTarget<Color = BinaryColor>, |
139 | { |
140 | for (p, element) in self.line_elements(position, text) { |
141 | match element { |
142 | LineElement::Char(c) => { |
143 | let glyph = self.font.glyph(c); |
144 | Image::new(&glyph, p).draw(&mut target)?; |
145 | } |
146 | // Fill space between characters if background color is set. |
147 | LineElement::Spacing if self.font.character_spacing > 0 => { |
148 | if self.background_color.is_some() { |
149 | target.fill_solid( |
150 | &Rectangle::new( |
151 | p, |
152 | Size::new( |
153 | self.font.character_spacing, |
154 | self.font.character_size.height, |
155 | ), |
156 | ), |
157 | BinaryColor::Off, |
158 | )?; |
159 | } |
160 | } |
161 | LineElement::Spacing => {} |
162 | LineElement::Done => return Ok(p), |
163 | } |
164 | } |
165 | |
166 | Ok(position) |
167 | } |
168 | |
169 | /// Returns the vertical offset between the line position and the top edge of the bounding box. |
170 | fn baseline_offset(&self, baseline: Baseline) -> i32 { |
171 | match baseline { |
172 | Baseline::Top => 0, |
173 | Baseline::Bottom => self |
174 | .font |
175 | .character_size |
176 | .height |
177 | .saturating_sub(1) |
178 | .saturating_as(), |
179 | Baseline::Middle => { |
180 | (self.font.character_size.height.saturating_sub(1) / 2).saturating_as() |
181 | } |
182 | Baseline::Alphabetic => self.font.baseline.saturating_as(), |
183 | } |
184 | } |
185 | } |
186 | |
187 | impl<C> TextRenderer for MonoTextStyle<'_, C> |
188 | where |
189 | C: PixelColor, |
190 | { |
191 | type Color = C; |
192 | |
193 | fn draw_string<D>( |
194 | &self, |
195 | text: &str, |
196 | position: Point, |
197 | baseline: Baseline, |
198 | target: &mut D, |
199 | ) -> Result<Point, D::Error> |
200 | where |
201 | D: DrawTarget<Color = Self::Color>, |
202 | { |
203 | let position = position - Point::new(0, self.baseline_offset(baseline)); |
204 | |
205 | let next = match (self.text_color, self.background_color) { |
206 | (Some(text_color), Some(background_color)) => self.draw_string_binary( |
207 | text, |
208 | position, |
209 | MonoFontDrawTarget::new(target, Both(text_color, background_color)), |
210 | )?, |
211 | (Some(text_color), None) => self.draw_string_binary( |
212 | text, |
213 | position, |
214 | MonoFontDrawTarget::new(target, Foreground(text_color)), |
215 | )?, |
216 | (None, Some(background_color)) => self.draw_string_binary( |
217 | text, |
218 | position, |
219 | MonoFontDrawTarget::new(target, Background(background_color)), |
220 | )?, |
221 | (None, None) => { |
222 | let dx = (self.font.character_size.width + self.font.character_spacing) |
223 | * text.chars().count() as u32; |
224 | |
225 | position + Size::new(dx, 0) |
226 | } |
227 | }; |
228 | |
229 | if next.x > position.x { |
230 | let width = (next.x - position.x) as u32; |
231 | self.draw_decorations(width, position, target)?; |
232 | } |
233 | |
234 | Ok(next + Point::new(0, self.baseline_offset(baseline))) |
235 | } |
236 | |
237 | fn draw_whitespace<D>( |
238 | &self, |
239 | width: u32, |
240 | position: Point, |
241 | baseline: Baseline, |
242 | target: &mut D, |
243 | ) -> Result<Point, D::Error> |
244 | where |
245 | D: DrawTarget<Color = Self::Color>, |
246 | { |
247 | let position = position - Point::new(0, self.baseline_offset(baseline)); |
248 | |
249 | if width != 0 { |
250 | if let Some(background_color) = self.background_color { |
251 | target.fill_solid( |
252 | &Rectangle::new(position, Size::new(width, self.font.character_size.height)), |
253 | background_color, |
254 | )?; |
255 | } |
256 | |
257 | self.draw_decorations(width, position, target)?; |
258 | } |
259 | |
260 | Ok(position + Point::new(width.saturating_as(), self.baseline_offset(baseline))) |
261 | } |
262 | |
263 | fn measure_string(&self, text: &str, position: Point, baseline: Baseline) -> TextMetrics { |
264 | let bb_position = position - Point::new(0, self.baseline_offset(baseline)); |
265 | |
266 | let bb_width = (text.chars().count() as u32 |
267 | * (self.font.character_size.width + self.font.character_spacing)) |
268 | .saturating_sub(self.font.character_spacing); |
269 | |
270 | let bb_height = if self.underline_color != DecorationColor::None { |
271 | self.font.underline.height + self.font.underline.offset |
272 | } else { |
273 | self.font.character_size.height |
274 | }; |
275 | |
276 | let bb_size = Size::new(bb_width, bb_height); |
277 | |
278 | TextMetrics { |
279 | bounding_box: Rectangle::new(bb_position, bb_size), |
280 | next_position: position + bb_size.x_axis(), |
281 | } |
282 | } |
283 | |
284 | fn line_height(&self) -> u32 { |
285 | self.font.character_size.height |
286 | } |
287 | } |
288 | |
289 | impl<C> CharacterStyle for MonoTextStyle<'_, C> |
290 | where |
291 | C: PixelColor, |
292 | { |
293 | type Color = C; |
294 | |
295 | fn set_text_color(&mut self, text_color: Option<Self::Color>) { |
296 | self.text_color = text_color; |
297 | } |
298 | |
299 | fn set_background_color(&mut self, background_color: Option<Self::Color>) { |
300 | self.background_color = background_color; |
301 | } |
302 | |
303 | fn set_underline_color(&mut self, underline_color: DecorationColor<Self::Color>) { |
304 | self.underline_color = underline_color; |
305 | } |
306 | |
307 | fn set_strikethrough_color(&mut self, strikethrough_color: DecorationColor<Self::Color>) { |
308 | self.strikethrough_color = strikethrough_color; |
309 | } |
310 | } |
311 | |
312 | #[derive (Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] |
313 | #[cfg_attr (feature = "defmt" , derive(::defmt::Format))] |
314 | enum LineElement { |
315 | Char(char), |
316 | Spacing, |
317 | Done, |
318 | } |
319 | |
320 | /// Text style builder for monospaced fonts. |
321 | /// |
322 | /// Use this builder to create [`MonoTextStyle`]s for [`Text`]. |
323 | /// |
324 | /// # Examples |
325 | /// |
326 | /// ## Render yellow text on a blue background |
327 | /// |
328 | /// This uses the [`FONT_6X9`] font, but [other fonts] can also be used. |
329 | /// |
330 | /// ```rust |
331 | /// use embedded_graphics::{ |
332 | /// mono_font::{ascii::FONT_6X9, MonoTextStyle, MonoTextStyleBuilder}, |
333 | /// pixelcolor::Rgb565, |
334 | /// prelude::*, |
335 | /// text::Text, |
336 | /// }; |
337 | /// |
338 | /// let style = MonoTextStyleBuilder::new() |
339 | /// .font(&FONT_6X9) |
340 | /// .text_color(Rgb565::YELLOW) |
341 | /// .background_color(Rgb565::BLUE) |
342 | /// .build(); |
343 | /// |
344 | /// let text = Text::new("Hello Rust!" , Point::new(0, 0), style); |
345 | /// ``` |
346 | /// |
347 | /// ## Transparent background |
348 | /// |
349 | /// If a property is omitted, it will remain at its default value in the resulting |
350 | /// `MonoTextStyle` returned by `.build()`. This example draws white text with no background at |
351 | /// all. |
352 | /// |
353 | /// ```rust |
354 | /// use embedded_graphics::{ |
355 | /// mono_font::{ascii::FONT_6X9, MonoTextStyle, MonoTextStyleBuilder}, |
356 | /// pixelcolor::Rgb565, |
357 | /// prelude::*, |
358 | /// text::Text, |
359 | /// }; |
360 | /// |
361 | /// let style = MonoTextStyleBuilder::new() |
362 | /// .font(&FONT_6X9) |
363 | /// .text_color(Rgb565::WHITE) |
364 | /// .build(); |
365 | /// |
366 | /// let text = Text::new("Hello Rust!" , Point::new(0, 0), style); |
367 | /// ``` |
368 | /// |
369 | /// ## Modifying an existing style |
370 | /// |
371 | /// The builder can also be used to modify an existing style. |
372 | /// |
373 | /// ``` |
374 | /// use embedded_graphics::{ |
375 | /// mono_font::{ascii::{FONT_6X9, FONT_10X20}, MonoTextStyle, MonoTextStyleBuilder}, |
376 | /// pixelcolor::Rgb565, |
377 | /// prelude::*, |
378 | /// text::Text, |
379 | /// }; |
380 | /// |
381 | /// let style = MonoTextStyle::new(&FONT_6X9, Rgb565::YELLOW); |
382 | /// |
383 | /// let style_larger = MonoTextStyleBuilder::from(&style) |
384 | /// .font(&FONT_10X20) |
385 | /// .build(); |
386 | /// ``` |
387 | /// |
388 | /// [`FONT_6X9`]: crate::mono_font::ascii::FONT_6X9 |
389 | /// [other fonts]: super |
390 | /// [`Text`]: crate::text::Text |
391 | #[derive (Copy, Clone, Debug)] |
392 | #[cfg_attr (feature = "defmt" , derive(::defmt::Format))] |
393 | pub struct MonoTextStyleBuilder<'a, C> { |
394 | style: MonoTextStyle<'a, C>, |
395 | } |
396 | |
397 | impl<'a, C> MonoTextStyleBuilder<'a, C> |
398 | where |
399 | C: PixelColor, |
400 | { |
401 | /// Creates a new text style builder. |
402 | pub const fn new() -> Self { |
403 | Self { |
404 | style: MonoTextStyle { |
405 | font: &super::NULL_FONT, |
406 | background_color: None, |
407 | text_color: None, |
408 | underline_color: DecorationColor::None, |
409 | strikethrough_color: DecorationColor::None, |
410 | }, |
411 | } |
412 | } |
413 | |
414 | /// Sets the font. |
415 | pub const fn font<'b>(self, font: &'b MonoFont<'b>) -> MonoTextStyleBuilder<'b, C> { |
416 | let style = MonoTextStyle { |
417 | font, |
418 | background_color: self.style.background_color, |
419 | text_color: self.style.text_color, |
420 | underline_color: self.style.underline_color, |
421 | strikethrough_color: self.style.strikethrough_color, |
422 | }; |
423 | |
424 | MonoTextStyleBuilder { style } |
425 | } |
426 | |
427 | /// Enables underline using the text color. |
428 | pub const fn underline(mut self) -> Self { |
429 | self.style.underline_color = DecorationColor::TextColor; |
430 | |
431 | self |
432 | } |
433 | |
434 | /// Enables strikethrough using the text color. |
435 | pub const fn strikethrough(mut self) -> Self { |
436 | self.style.strikethrough_color = DecorationColor::TextColor; |
437 | |
438 | self |
439 | } |
440 | |
441 | /// Resets the text color to transparent. |
442 | pub const fn reset_text_color(mut self) -> Self { |
443 | self.style.text_color = None; |
444 | |
445 | self |
446 | } |
447 | |
448 | /// Resets the background color to transparent. |
449 | pub const fn reset_background_color(mut self) -> Self { |
450 | self.style.background_color = None; |
451 | |
452 | self |
453 | } |
454 | |
455 | /// Removes the underline decoration. |
456 | pub const fn reset_underline(mut self) -> Self { |
457 | self.style.underline_color = DecorationColor::None; |
458 | |
459 | self |
460 | } |
461 | |
462 | /// Removes the strikethrough decoration. |
463 | pub const fn reset_strikethrough(mut self) -> Self { |
464 | self.style.strikethrough_color = DecorationColor::None; |
465 | |
466 | self |
467 | } |
468 | |
469 | /// Sets the text color. |
470 | pub const fn text_color(mut self, text_color: C) -> Self { |
471 | self.style.text_color = Some(text_color); |
472 | |
473 | self |
474 | } |
475 | |
476 | /// Sets the background color. |
477 | pub const fn background_color(mut self, background_color: C) -> Self { |
478 | self.style.background_color = Some(background_color); |
479 | |
480 | self |
481 | } |
482 | |
483 | /// Enables underline with a custom color. |
484 | pub const fn underline_with_color(mut self, underline_color: C) -> Self { |
485 | self.style.underline_color = DecorationColor::Custom(underline_color); |
486 | |
487 | self |
488 | } |
489 | |
490 | /// Enables strikethrough with a custom color. |
491 | pub const fn strikethrough_with_color(mut self, strikethrough_color: C) -> Self { |
492 | self.style.strikethrough_color = DecorationColor::Custom(strikethrough_color); |
493 | |
494 | self |
495 | } |
496 | |
497 | /// Builds the text style. |
498 | /// |
499 | /// This method can only be called after a font was set by using the [`font`] method. All other |
500 | /// settings are optional and they will be set to their default value if they are missing. |
501 | /// |
502 | /// [`font`]: MonoTextStyleBuilder::font() |
503 | pub const fn build(self) -> MonoTextStyle<'a, C> { |
504 | self.style |
505 | } |
506 | } |
507 | |
508 | impl<'a, C> From<&MonoTextStyle<'a, C>> for MonoTextStyleBuilder<'a, C> |
509 | where |
510 | C: PixelColor, |
511 | { |
512 | fn from(style: &MonoTextStyle<'a, C>) -> Self { |
513 | Self { style: *style } |
514 | } |
515 | } |
516 | |
517 | #[cfg (test)] |
518 | mod tests { |
519 | use super::*; |
520 | use crate::{ |
521 | geometry::Dimensions, |
522 | image::ImageRaw, |
523 | mock_display::MockDisplay, |
524 | mono_font::{ |
525 | ascii::{FONT_10X20, FONT_6X9}, |
526 | iso_8859_1::FONT_6X9 as FONT_6X9_LATIN1, |
527 | mapping, |
528 | tests::*, |
529 | DecorationDimensions, |
530 | }, |
531 | pixelcolor::{BinaryColor, Rgb888, RgbColor}, |
532 | text::Text, |
533 | Drawable, |
534 | }; |
535 | |
536 | const SPACED_FONT: MonoFont = MonoFont { |
537 | character_spacing: 5, |
538 | underline: DecorationDimensions::new(9, 1), |
539 | ..FONT_6X9 |
540 | }; |
541 | |
542 | const UNDERLINED_WHITE_STYLE: MonoTextStyle<Rgb888> = MonoTextStyleBuilder::new() |
543 | .font(&FONT_6X9) |
544 | .text_color(Rgb888::WHITE) |
545 | .underline() |
546 | .build(); |
547 | |
548 | #[test ] |
549 | fn builder_default() { |
550 | assert_eq!( |
551 | MonoTextStyleBuilder::<BinaryColor>::new() |
552 | .font(&FONT_10X20) |
553 | .build(), |
554 | MonoTextStyle { |
555 | font: &FONT_10X20, |
556 | text_color: None, |
557 | background_color: None, |
558 | underline_color: DecorationColor::None, |
559 | strikethrough_color: DecorationColor::None, |
560 | } |
561 | ); |
562 | } |
563 | |
564 | #[test ] |
565 | fn builder_text_color() { |
566 | assert_eq!( |
567 | MonoTextStyleBuilder::new() |
568 | .font(&FONT_10X20) |
569 | .text_color(BinaryColor::On) |
570 | .build(), |
571 | MonoTextStyle::new(&FONT_10X20, BinaryColor::On) |
572 | ); |
573 | } |
574 | |
575 | #[test ] |
576 | fn builder_background_color() { |
577 | assert_eq!( |
578 | MonoTextStyleBuilder::new() |
579 | .font(&FONT_10X20) |
580 | .background_color(BinaryColor::On) |
581 | .build(), |
582 | { |
583 | let mut style = MonoTextStyleBuilder::new().font(&FONT_10X20).build(); |
584 | |
585 | style.text_color = None; |
586 | style.background_color = Some(BinaryColor::On); |
587 | |
588 | style |
589 | } |
590 | ); |
591 | } |
592 | |
593 | #[test ] |
594 | fn builder_resets() { |
595 | let base = MonoTextStyleBuilder::new() |
596 | .font(&FONT_10X20) |
597 | .text_color(BinaryColor::On) |
598 | .background_color(BinaryColor::On) |
599 | .underline() |
600 | .strikethrough(); |
601 | |
602 | assert_eq!( |
603 | base.reset_text_color().build(), |
604 | MonoTextStyleBuilder::new() |
605 | .font(&FONT_10X20) |
606 | .background_color(BinaryColor::On) |
607 | .underline() |
608 | .strikethrough() |
609 | .build() |
610 | ); |
611 | |
612 | assert_eq!( |
613 | base.reset_background_color().build(), |
614 | MonoTextStyleBuilder::new() |
615 | .font(&FONT_10X20) |
616 | .text_color(BinaryColor::On) |
617 | .underline() |
618 | .strikethrough() |
619 | .build() |
620 | ); |
621 | |
622 | assert_eq!( |
623 | base.reset_underline().build(), |
624 | MonoTextStyleBuilder::new() |
625 | .font(&FONT_10X20) |
626 | .text_color(BinaryColor::On) |
627 | .background_color(BinaryColor::On) |
628 | .strikethrough() |
629 | .build() |
630 | ); |
631 | |
632 | assert_eq!( |
633 | base.reset_strikethrough().build(), |
634 | MonoTextStyleBuilder::new() |
635 | .font(&FONT_10X20) |
636 | .text_color(BinaryColor::On) |
637 | .background_color(BinaryColor::On) |
638 | .underline() |
639 | .build() |
640 | ); |
641 | } |
642 | |
643 | #[test ] |
644 | fn underline_text_color() { |
645 | let mut display = MockDisplay::new(); |
646 | Text::new("ABC" , Point::new(0, 6), UNDERLINED_WHITE_STYLE) |
647 | .draw(&mut display) |
648 | .unwrap(); |
649 | |
650 | display.assert_pattern(&[ |
651 | " " , |
652 | " W WWWW WW " , |
653 | " W W W W W W " , |
654 | "W W WWWW W " , |
655 | "WWWWW W W W " , |
656 | "W W W W W W " , |
657 | "W W WWWW WW " , |
658 | " " , |
659 | "WWWWWWWWWWWWWWWWWW" , |
660 | ]); |
661 | } |
662 | |
663 | #[test ] |
664 | fn underline_text_color_with_alignment() { |
665 | let mut display = MockDisplay::new(); |
666 | Text::with_baseline( |
667 | "ABC" , |
668 | Point::new(0, 6), |
669 | UNDERLINED_WHITE_STYLE, |
670 | Baseline::Middle, |
671 | ) |
672 | .draw(&mut display) |
673 | .unwrap(); |
674 | |
675 | display.assert_pattern(&[ |
676 | " " , |
677 | " " , |
678 | " " , |
679 | " W WWWW WW " , |
680 | " W W W W W W " , |
681 | "W W WWWW W " , |
682 | "WWWWW W W W " , |
683 | "W W W W W W " , |
684 | "W W WWWW WW " , |
685 | " " , |
686 | "WWWWWWWWWWWWWWWWWW" , |
687 | ]); |
688 | } |
689 | |
690 | #[test ] |
691 | fn underline_custom_color() { |
692 | let style = MonoTextStyleBuilder::new() |
693 | .font(&FONT_6X9) |
694 | .text_color(Rgb888::WHITE) |
695 | .underline_with_color(Rgb888::RED) |
696 | .build(); |
697 | |
698 | let mut display = MockDisplay::new(); |
699 | Text::new("ABC" , Point::new(0, 6), style) |
700 | .draw(&mut display) |
701 | .unwrap(); |
702 | |
703 | display.assert_pattern(&[ |
704 | " " , |
705 | " W WWWW WW " , |
706 | " W W W W W W " , |
707 | "W W WWWW W " , |
708 | "WWWWW W W W " , |
709 | "W W W W W W " , |
710 | "W W WWWW WW " , |
711 | " " , |
712 | "RRRRRRRRRRRRRRRRRR" , |
713 | ]); |
714 | } |
715 | |
716 | #[test ] |
717 | fn strikethrough_text_color() { |
718 | let style = MonoTextStyleBuilder::new() |
719 | .font(&FONT_6X9) |
720 | .text_color(Rgb888::WHITE) |
721 | .strikethrough() |
722 | .build(); |
723 | |
724 | let mut display = MockDisplay::new(); |
725 | display.set_allow_overdraw(true); |
726 | |
727 | Text::new("ABC" , Point::new(0, 6), style) |
728 | .draw(&mut display) |
729 | .unwrap(); |
730 | |
731 | display.assert_pattern(&[ |
732 | " " , |
733 | " W WWWW WW " , |
734 | " W W W W W W " , |
735 | "W W WWWW W " , |
736 | "WWWWWWWWWWWWWWWWWW" , |
737 | "W W W W W W " , |
738 | "W W WWWW WW " , |
739 | ]); |
740 | } |
741 | |
742 | #[test ] |
743 | fn strikethrough_custom_color() { |
744 | let style = MonoTextStyleBuilder::new() |
745 | .font(&FONT_6X9) |
746 | .text_color(Rgb888::WHITE) |
747 | .strikethrough_with_color(Rgb888::RED) |
748 | .build(); |
749 | |
750 | let mut display = MockDisplay::new(); |
751 | display.set_allow_overdraw(true); |
752 | |
753 | Text::new("ABC" , Point::new(0, 6), style) |
754 | .draw(&mut display) |
755 | .unwrap(); |
756 | |
757 | display.assert_pattern(&[ |
758 | " " , |
759 | " W WWWW WW " , |
760 | " W W W W W W " , |
761 | "W W WWWW W " , |
762 | "RRRRRRRRRRRRRRRRRR" , |
763 | "W W W W W W " , |
764 | "W W WWWW WW " , |
765 | ]); |
766 | } |
767 | |
768 | #[test ] |
769 | fn whitespace_background() { |
770 | let style = MonoTextStyleBuilder::new() |
771 | .font(&FONT_6X9) |
772 | .text_color(Rgb888::YELLOW) |
773 | .background_color(Rgb888::WHITE) |
774 | .build(); |
775 | |
776 | let mut display = MockDisplay::new(); |
777 | style |
778 | .draw_whitespace(4, Point::zero(), Baseline::Top, &mut display) |
779 | .unwrap(); |
780 | |
781 | display.assert_pattern(&[ |
782 | "WWWW" , // |
783 | "WWWW" , // |
784 | "WWWW" , // |
785 | "WWWW" , // |
786 | "WWWW" , // |
787 | "WWWW" , // |
788 | "WWWW" , // |
789 | "WWWW" , // |
790 | "WWWW" , // |
791 | ]); |
792 | } |
793 | |
794 | #[test ] |
795 | fn whitespace_decorations() { |
796 | let style = MonoTextStyleBuilder::new() |
797 | .font(&FONT_6X9) |
798 | .text_color(Rgb888::YELLOW) |
799 | .underline_with_color(Rgb888::GREEN) |
800 | .strikethrough_with_color(Rgb888::RED) |
801 | .build(); |
802 | |
803 | let mut display = MockDisplay::new(); |
804 | style |
805 | .draw_whitespace(3, Point::zero(), Baseline::Top, &mut display) |
806 | .unwrap(); |
807 | |
808 | display.assert_pattern(&[ |
809 | " " , // |
810 | " " , // |
811 | " " , // |
812 | " " , // |
813 | "RRR" , // |
814 | " " , // |
815 | " " , // |
816 | " " , // |
817 | "GGG" , // |
818 | ]); |
819 | } |
820 | |
821 | #[test ] |
822 | fn whitespace_background_and_decorations() { |
823 | let style = MonoTextStyleBuilder::new() |
824 | .font(&FONT_6X9) |
825 | .text_color(Rgb888::YELLOW) |
826 | .background_color(Rgb888::WHITE) |
827 | .underline_with_color(Rgb888::GREEN) |
828 | .strikethrough_with_color(Rgb888::RED) |
829 | .build(); |
830 | |
831 | let mut display = MockDisplay::new(); |
832 | display.set_allow_overdraw(true); |
833 | |
834 | style |
835 | .draw_whitespace(8, Point::zero(), Baseline::Top, &mut display) |
836 | .unwrap(); |
837 | |
838 | display.assert_pattern(&[ |
839 | "WWWWWWWW" , // |
840 | "WWWWWWWW" , // |
841 | "WWWWWWWW" , // |
842 | "WWWWWWWW" , // |
843 | "RRRRRRRR" , // |
844 | "WWWWWWWW" , // |
845 | "WWWWWWWW" , // |
846 | "WWWWWWWW" , // |
847 | "GGGGGGGG" , // |
848 | ]); |
849 | } |
850 | |
851 | #[test ] |
852 | fn character_spacing() { |
853 | assert_text_from_pattern( |
854 | "##" , |
855 | &SPACED_FONT, |
856 | &[ |
857 | " " , |
858 | " # # # # " , |
859 | " # # # # " , |
860 | "##### ##### " , |
861 | " # # # # " , |
862 | "##### ##### " , |
863 | " # # # # " , |
864 | " # # # # " , |
865 | ], |
866 | ); |
867 | } |
868 | |
869 | #[test ] |
870 | fn character_spacing_with_background() { |
871 | let character_style = MonoTextStyleBuilder::new() |
872 | .font(&SPACED_FONT) |
873 | .text_color(BinaryColor::On) |
874 | .background_color(BinaryColor::Off) |
875 | .build(); |
876 | |
877 | let mut display = MockDisplay::new(); |
878 | Text::with_baseline("##" , Point::zero(), character_style, Baseline::Top) |
879 | .draw(&mut display) |
880 | .unwrap(); |
881 | |
882 | display.assert_pattern(&[ |
883 | "................." , |
884 | ".#.#........#.#.." , |
885 | ".#.#........#.#.." , |
886 | "#####......#####." , |
887 | ".#.#........#.#.." , |
888 | "#####......#####." , |
889 | ".#.#........#.#.." , |
890 | ".#.#........#.#.." , |
891 | "................." , |
892 | ]); |
893 | } |
894 | |
895 | #[test ] |
896 | fn character_spacing_decorations() { |
897 | let character_style = MonoTextStyleBuilder::new() |
898 | .font(&SPACED_FONT) |
899 | .text_color(Rgb888::WHITE) |
900 | .underline_with_color(Rgb888::GREEN) |
901 | .strikethrough_with_color(Rgb888::RED) |
902 | .build(); |
903 | |
904 | let mut display = MockDisplay::new(); |
905 | display.set_allow_overdraw(true); |
906 | |
907 | Text::with_baseline("##" , Point::zero(), character_style, Baseline::Top) |
908 | .draw(&mut display) |
909 | .unwrap(); |
910 | |
911 | display.assert_pattern(&[ |
912 | " " , |
913 | " W W W W " , |
914 | " W W W W " , |
915 | "WWWWW WWWWW " , |
916 | "RRRRRRRRRRRRRRRRR" , |
917 | "WWWWW WWWWW " , |
918 | " W W W W " , |
919 | " W W W W " , |
920 | " " , |
921 | "GGGGGGGGGGGGGGGGG" , |
922 | ]); |
923 | } |
924 | |
925 | #[test ] |
926 | fn character_spacing_dimensions() { |
927 | let style = MonoTextStyleBuilder::new() |
928 | .font(&SPACED_FONT) |
929 | .text_color(BinaryColor::On) |
930 | .build(); |
931 | |
932 | assert_eq!( |
933 | Text::with_baseline("#" , Point::zero(), style, Baseline::Top).bounding_box(), |
934 | Rectangle::new(Point::zero(), Size::new(6, 9)), |
935 | ); |
936 | |
937 | assert_eq!( |
938 | Text::with_baseline("##" , Point::zero(), style, Baseline::Top).bounding_box(), |
939 | Rectangle::new(Point::zero(), Size::new(6 * 2 + 5, 9)), |
940 | ); |
941 | assert_eq!( |
942 | Text::with_baseline("###" , Point::zero(), style, Baseline::Top).bounding_box(), |
943 | Rectangle::new(Point::zero(), Size::new(6 * 3 + 5 * 2, 9)), |
944 | ); |
945 | } |
946 | |
947 | #[test ] |
948 | fn underlined_character_dimensions() { |
949 | let style = MonoTextStyleBuilder::new() |
950 | .font(&SPACED_FONT) |
951 | .text_color(BinaryColor::On) |
952 | .underline() |
953 | .build(); |
954 | |
955 | assert_eq!( |
956 | Text::with_baseline("#" , Point::zero(), style, Baseline::Top).bounding_box(), |
957 | Rectangle::new(Point::zero(), Size::new(6, 10)), |
958 | ); |
959 | } |
960 | |
961 | #[test ] |
962 | fn control_characters() { |
963 | let style = MonoTextStyle::new(&FONT_6X9, BinaryColor::On); |
964 | |
965 | let mut display = MockDisplay::new(); |
966 | style |
967 | .draw_string("A \t\n\rB" , Point::zero(), Baseline::Top, &mut display) |
968 | .unwrap(); |
969 | |
970 | let mut expected = MockDisplay::new(); |
971 | style |
972 | .draw_string("A???B" , Point::zero(), Baseline::Top, &mut expected) |
973 | .unwrap(); |
974 | |
975 | display.assert_eq(&expected); |
976 | } |
977 | |
978 | #[test ] |
979 | fn character_style() { |
980 | let mut style = MonoTextStyle::new(&FONT_6X9, BinaryColor::On); |
981 | CharacterStyle::set_text_color(&mut style, None); |
982 | CharacterStyle::set_background_color(&mut style, Some(BinaryColor::On)); |
983 | CharacterStyle::set_underline_color(&mut style, DecorationColor::TextColor); |
984 | CharacterStyle::set_strikethrough_color( |
985 | &mut style, |
986 | DecorationColor::Custom(BinaryColor::On), |
987 | ); |
988 | |
989 | assert_eq!( |
990 | style, |
991 | MonoTextStyle { |
992 | text_color: None, |
993 | background_color: Some(BinaryColor::On), |
994 | underline_color: DecorationColor::TextColor, |
995 | strikethrough_color: DecorationColor::Custom(BinaryColor::On), |
996 | font: &FONT_6X9, |
997 | } |
998 | ); |
999 | } |
1000 | |
1001 | #[test ] |
1002 | fn draw_string_return_value() { |
1003 | let style = MonoTextStyle::new(&FONT_6X9, BinaryColor::On); |
1004 | let start = Point::new(10, 20); |
1005 | let expected_next = start + Point::new(2 * 6, 0); |
1006 | |
1007 | for baseline in [ |
1008 | Baseline::Top, |
1009 | Baseline::Middle, |
1010 | Baseline::Alphabetic, |
1011 | Baseline::Bottom, |
1012 | ] |
1013 | .iter() |
1014 | { |
1015 | let mut display = MockDisplay::new(); |
1016 | let next = style |
1017 | .draw_string("AB" , start, *baseline, &mut display) |
1018 | .unwrap(); |
1019 | |
1020 | assert_eq!( |
1021 | next, expected_next, |
1022 | "Unexpected next point for {:?}: {:?} (expected {:?})" , |
1023 | baseline, next, expected_next |
1024 | ); |
1025 | } |
1026 | } |
1027 | |
1028 | #[test ] |
1029 | fn draw_whitespace_return_value() { |
1030 | let style = MonoTextStyle::new(&FONT_6X9, BinaryColor::On); |
1031 | let start = Point::new(10, 20); |
1032 | let expected_next = start + Point::new(15, 0); |
1033 | |
1034 | for baseline in [ |
1035 | Baseline::Top, |
1036 | Baseline::Middle, |
1037 | Baseline::Alphabetic, |
1038 | Baseline::Bottom, |
1039 | ] |
1040 | .iter() |
1041 | { |
1042 | let mut display = MockDisplay::new(); |
1043 | let next = style |
1044 | .draw_whitespace(15, start, *baseline, &mut display) |
1045 | .unwrap(); |
1046 | |
1047 | assert_eq!( |
1048 | next, expected_next, |
1049 | "Unexpected next point for {:?}: {:?} (expected {:?})" , |
1050 | baseline, next, expected_next |
1051 | ); |
1052 | } |
1053 | } |
1054 | |
1055 | #[test ] |
1056 | fn latin1_text_dimensions_one_line() { |
1057 | let position = Point::new(5, 5); |
1058 | |
1059 | let style = MonoTextStyleBuilder::<BinaryColor>::new() |
1060 | .font(&FONT_6X9_LATIN1) |
1061 | .build(); |
1062 | let text = Text::with_baseline("123°§£" , position, style, Baseline::Top); |
1063 | |
1064 | assert_eq!( |
1065 | text.bounding_box(), |
1066 | Rectangle::new( |
1067 | position, |
1068 | FONT_6X9_LATIN1 |
1069 | .character_size |
1070 | .component_mul(Size::new(6, 1)) |
1071 | ) |
1072 | ); |
1073 | |
1074 | let mut display = MockDisplay::new(); |
1075 | let next = text.draw(&mut display).unwrap(); |
1076 | |
1077 | assert_eq!(next, position + FONT_6X9_LATIN1.character_size.x_axis() * 6); |
1078 | } |
1079 | |
1080 | #[test ] |
1081 | fn transparent_text_dimensions_one_line() { |
1082 | let position = Point::new(5, 5); |
1083 | |
1084 | let style = MonoTextStyleBuilder::<BinaryColor>::new() |
1085 | .font(&FONT_6X9) |
1086 | .build(); |
1087 | let text = Text::with_baseline("123" , position, style, Baseline::Top); |
1088 | |
1089 | assert_eq!( |
1090 | text.bounding_box(), |
1091 | Rectangle::new( |
1092 | position, |
1093 | FONT_6X9.character_size.component_mul(Size::new(3, 1)) |
1094 | ) |
1095 | ); |
1096 | |
1097 | let mut display = MockDisplay::new(); |
1098 | let next = text.draw(&mut display).unwrap(); |
1099 | |
1100 | assert_eq!(next, position + FONT_6X9.character_size.x_axis() * 3); |
1101 | } |
1102 | |
1103 | #[test ] |
1104 | fn transparent_text_dimensions_one_line_spaced() { |
1105 | let position = Point::new(5, 5); |
1106 | |
1107 | let style = MonoTextStyleBuilder::<BinaryColor>::new() |
1108 | .font(&SPACED_FONT) |
1109 | .build(); |
1110 | let text = Text::with_baseline("123" , position, style, Baseline::Top); |
1111 | |
1112 | assert_eq!( |
1113 | text.bounding_box(), |
1114 | Rectangle::new( |
1115 | position, |
1116 | SPACED_FONT.character_size.component_mul(Size::new(3, 1)) |
1117 | + Size::new(SPACED_FONT.character_spacing, 0) * 2 |
1118 | ) |
1119 | ); |
1120 | |
1121 | let mut display = MockDisplay::new(); |
1122 | let next = text.draw(&mut display).unwrap(); |
1123 | |
1124 | assert_eq!( |
1125 | next, |
1126 | position |
1127 | + (SPACED_FONT.character_size.x_axis() |
1128 | + Size::new(SPACED_FONT.character_spacing, 0)) |
1129 | * 3 |
1130 | ); |
1131 | } |
1132 | |
1133 | #[test ] |
1134 | fn transparent_text_dimensions_two_lines() { |
1135 | let position = Point::new(5, 5); |
1136 | |
1137 | let style = MonoTextStyleBuilder::<BinaryColor>::new() |
1138 | .font(&FONT_6X9) |
1139 | .build(); |
1140 | let text = Text::with_baseline("123 \n1" , position, style, Baseline::Top); |
1141 | |
1142 | assert_eq!( |
1143 | text.bounding_box(), |
1144 | Rectangle::new( |
1145 | position, |
1146 | FONT_6X9.character_size.component_mul(Size::new(3, 2)) |
1147 | ) |
1148 | ); |
1149 | |
1150 | let mut display = MockDisplay::new(); |
1151 | let next = text.draw(&mut display).unwrap(); |
1152 | |
1153 | assert_eq!(next, position + FONT_6X9.character_size); |
1154 | } |
1155 | |
1156 | #[test ] |
1157 | fn elements_iter() { |
1158 | let style = MonoTextStyle::new(&SPACED_FONT, BinaryColor::On); |
1159 | |
1160 | let mut iter = style.line_elements(Point::new(10, 20), "" ); |
1161 | assert_eq!(iter.next(), Some((Point::new(10, 20), LineElement::Done))); |
1162 | |
1163 | let mut iter = style.line_elements(Point::new(10, 20), "a" ); |
1164 | assert_eq!( |
1165 | iter.next(), |
1166 | Some((Point::new(10, 20), LineElement::Char('a' ))) |
1167 | ); |
1168 | assert_eq!(iter.next(), Some((Point::new(16, 20), LineElement::Done))); |
1169 | |
1170 | let mut iter = style.line_elements(Point::new(10, 20), "abc" ); |
1171 | assert_eq!( |
1172 | iter.next(), |
1173 | Some((Point::new(10, 20), LineElement::Char('a' ))) |
1174 | ); |
1175 | assert_eq!( |
1176 | iter.next(), |
1177 | Some((Point::new(16, 20), LineElement::Spacing)) |
1178 | ); |
1179 | assert_eq!( |
1180 | iter.next(), |
1181 | Some((Point::new(21, 20), LineElement::Char('b' ))) |
1182 | ); |
1183 | assert_eq!( |
1184 | iter.next(), |
1185 | Some((Point::new(27, 20), LineElement::Spacing)) |
1186 | ); |
1187 | assert_eq!( |
1188 | iter.next(), |
1189 | Some((Point::new(32, 20), LineElement::Char('c' ))) |
1190 | ); |
1191 | assert_eq!(iter.next(), Some((Point::new(38, 20), LineElement::Done))); |
1192 | } |
1193 | |
1194 | #[test ] |
1195 | fn builder_change_font() { |
1196 | let _style = { |
1197 | let font = MonoFont { |
1198 | image: ImageRaw::new(&[1, 2, 3], 1), |
1199 | character_size: Size::new(1, 2), |
1200 | character_spacing: 0, |
1201 | baseline: 0, |
1202 | strikethrough: DecorationDimensions::default_strikethrough(2), |
1203 | underline: DecorationDimensions::default_underline(2), |
1204 | glyph_mapping: &mapping::ASCII, |
1205 | }; |
1206 | |
1207 | let style = MonoTextStyleBuilder::new() |
1208 | .font(&font) |
1209 | .text_color(BinaryColor::On) |
1210 | .build(); |
1211 | |
1212 | // `style` cannot be returned from this block, because if is limited by the lifetime of |
1213 | // `font`. But it should be possible to return `style2` because `FONT_6X9` is a const. |
1214 | let style2 = MonoTextStyleBuilder::from(&style).font(&FONT_6X9).build(); |
1215 | |
1216 | style2 |
1217 | }; |
1218 | } |
1219 | } |
1220 | |