embedded_text/style/mod.rs
1//! `TextBox` styling.
2//! ==================
3//!
4//! To construct a `TextBox` object at least a text string, a bounding box and character style are
5//! required. For advanced formatting options an additional `TextBoxStyle` object might be used.
6//!
7//! Text rendering in `embedded-graphics` is designed to be extendable by text renderers for
8//! different font formats. `embedded-text` follows this philosophy by using the same text renderer
9//! infrastructure. To use a text renderer in an `embedded-text` project each renderer provides a
10//! character style object. See the [`embedded-graphics` documentation] for more information.
11//!
12//! TextBox style
13//! ---------------
14//!
15//! In addition to styling the individual characters the [`TextBox`] drawable also contains a
16//! [`TextBoxStyle`] setting. The text box style is used to set the horizontal and vertical
17//! alignment, line and paragraph spacing, tab size and some other advanced settings of text box
18//! objects.
19//!
20//! The [`alignment`] option sets the horizontal alignment of the text.
21//! **Note: alignment works differently from `embedded-graphics`.**
22//! With the default value `Left` the start of each line will be lined up with the left side of the
23//! bounding box. Similarly `Right` aligned text will line up the ends of the lines with the right
24//! side of the bounding box. `Center`ed text will be positioned at equal distance from the left and
25//! right sides. `Justified` text will distribute the text in such a way that both the start and end
26//! of a line will align with the respective sides of the bounding box.
27//!
28//! The [`vertical_alignment`] setting sets the vertical alignment of the text.
29//! With the default value `Top` the top of the text is lined up with the top of the bounding box.
30//! Similarly `Bottom` aligned text will line up the bottom of the last line of the text with the
31//! bottom edge of the bounding box. `Middle` aligned text will be positioned at equal distance from
32//! the top and bottom sides.
33//!
34//! The [`line_height`] option sets the distance between the baselines of the lines of text. It can
35//! be specified in either pixels or percentage of the line height defined by the font.
36//!
37//! The [`paragraph_spacing`] setting sets the distance between paragraphs of text, in addition to
38//! the line spacing.
39//!
40//! The [`tab_size`] setting sets the maximum width of a tab character. It can be specified in
41//! either pixels of number of space characters.
42//!
43//! Advanced settings
44//! -----------------
45//!
46//! The [`height_mode`] setting determines how the `TextBox` adjusts its height to its contents.
47//! The default value [`Exact`] does not adjust the height - the text box will be as tall as the
48//! bounding box given to it. [`FitToText`] will adjust the height to the height of the text,
49//! regardless of the initial bounding box's height. [`ShrinkToText`] will decrease the height
50//! of the text box to the height of the text, if the bounding box given to the text box is too
51//! tall.
52//!
53//! `Exact` and `ShrinkToText` have an additional [`VerticalOverdraw`] parameter. This setting
54//! specifies how the text outside of the adjusted bounding box is handled. [`Visible`] renders the
55//! text regardless of the bounding box. [`Hidden`] renders everything inside the bounding box. If a
56//! line is too tall to fit inside the bounding box, it will be drawn partially, the bottom part of
57//! the text clipped. [`FullRowsOnly`] only renders lines that are completely inside the bounding
58//! box.
59//!
60//! For examples on how to use height mode settings, see the documentation of [`HeightMode`].
61//!
62//! The [`leading_spaces`] and [`trailing_spaces`] settings set whether the spaces at the beginning
63//! or the end of a line are visible. The default values depend on the [`alignment`] setting.
64//!
65//! | `alignment` | `leading_spaces` | `trailing_spaces` |
66//! | ------------ | ---------------- | ----------------- |
67//! | `Left` | `true` | `false` |
68//! | `Right` | `false` | `false` |
69//! | `Center` | `false` | `false` |
70//! | `Justified` | `false` | `false` |
71//!
72//! # Ways to create and apply text box styles
73//!
74//! ## Example 1: Setting multiple options using the [`TextBoxStyleBuilder`] object:
75//!
76//! To build more complex styles, you can use the [`TextBoxStyleBuilder`] object and the
77//! [`TextBox::with_textbox_style`] constructor.
78//!
79//! ```rust
80//! # use embedded_graphics::{
81//! # mono_font::{ascii::FONT_6X9, MonoTextStyle},
82//! # pixelcolor::BinaryColor,
83//! # prelude::*,
84//! # primitives::Rectangle,
85//! # };
86//! # let character_style = MonoTextStyle::new(&FONT_6X9, BinaryColor::On);
87//! # let bounding_box = Rectangle::new(Point::zero(), Size::new(60, 10));
88//! # let text = "";
89//! #
90//! use embedded_text::{
91//! TextBox,
92//! style::TextBoxStyleBuilder,
93//! alignment::{
94//! HorizontalAlignment,
95//! VerticalAlignment,
96//! },
97//! };
98//!
99//! let textbox_style = TextBoxStyleBuilder::new()
100//! .alignment(HorizontalAlignment::Center)
101//! .vertical_alignment(VerticalAlignment::Middle)
102//! .build();
103//!
104//! let textbox = TextBox::with_textbox_style(
105//! text,
106//! bounding_box,
107//! character_style,
108//! textbox_style,
109//! );
110//! ```
111//!
112//! [`TextBox::with_textbox_style`]: crate::TextBox::with_textbox_style
113//!
114//! ## Examples 2 and 3: Only setting a single option:
115//!
116//! Both the [`TextBox`] and [`TextBoxStyle`] objects have different constructor methods in case you
117//! only want to set a single style option.
118//!
119//! ```rust
120//! # use embedded_graphics::{
121//! # mono_font::{ascii::FONT_6X9, MonoTextStyle},
122//! # pixelcolor::BinaryColor,
123//! # prelude::*,
124//! # primitives::Rectangle,
125//! # };
126//! # let character_style = MonoTextStyle::new(&FONT_6X9, BinaryColor::On);
127//! # let bounding_box = Rectangle::new(Point::zero(), Size::new(60, 10));
128//! # let text = "";
129//! #
130//! use embedded_text::{TextBox, alignment::HorizontalAlignment};
131//!
132//! let textbox = TextBox::with_alignment(
133//! text,
134//! bounding_box,
135//! character_style,
136//! HorizontalAlignment::Center,
137//! );
138//! ```
139//!
140//! ```rust
141//! # use embedded_graphics::{
142//! # mono_font::{ascii::FONT_6X9, MonoTextStyle},
143//! # pixelcolor::BinaryColor,
144//! # prelude::*,
145//! # primitives::Rectangle,
146//! # };
147//! # let character_style = MonoTextStyle::new(&FONT_6X9, BinaryColor::On);
148//! # let bounding_box = Rectangle::new(Point::zero(), Size::new(60, 10));
149//! # let text = "";
150//! #
151//! use embedded_text::{TextBox, style::TextBoxStyle, alignment::VerticalAlignment};
152//!
153//! let textbox_style = TextBoxStyle::with_vertical_alignment(VerticalAlignment::Middle);
154//!
155//! let textbox = TextBox::with_textbox_style(
156//! text,
157//! bounding_box,
158//! character_style,
159//! textbox_style,
160//! );
161//! ```
162//!
163//! [`TextBox`]: crate::TextBox
164//! [`alignment`]: TextBoxStyle::alignment
165//! [`vertical_alignment`]: TextBoxStyle::vertical_alignment
166//! [`line_height`]: TextBoxStyle::line_height
167//! [`paragraph_spacing`]: TextBoxStyle::paragraph_spacing
168//! [`tab_size`]: TextBoxStyle::tab_size
169//! [`height_mode`]: TextBoxStyle::height_mode
170//! [`leading_spaces`]: TextBoxStyle::leading_spaces
171//! [`trailing_spaces`]: TextBoxStyle::trailing_spaces
172//! [`Exact`]: HeightMode::Exact
173//! [`FitToText`]: HeightMode::FitToText
174//! [`ShrinkToText`]: HeightMode::ShrinkToText
175//! [`Visible`]: VerticalOverdraw::Visible
176//! [`Hidden`]: VerticalOverdraw::Hidden
177//! [`FullRowsOnly`]: VerticalOverdraw::FullRowsOnly
178//! [`embedded-graphics` documentation]: https://docs.rs/embedded-graphics/0.7.1/embedded_graphics/text/index.html
179
180mod builder;
181mod height_mode;
182mod vertical_overdraw;
183
184use core::convert::Infallible;
185
186use crate::{
187 alignment::{HorizontalAlignment, VerticalAlignment},
188 parser::Parser,
189 plugin::{NoPlugin, PluginMarker as Plugin, PluginWrapper, ProcessingState},
190 rendering::{
191 cursor::LineCursor,
192 line_iter::{ElementHandler, LineElementParser, LineEndType},
193 space_config::SpaceConfig,
194 },
195 utils::{str_width, str_width_and_left_offset},
196};
197use embedded_graphics::text::{renderer::TextRenderer, LineHeight};
198
199pub use self::{
200 builder::TextBoxStyleBuilder, height_mode::HeightMode, vertical_overdraw::VerticalOverdraw,
201};
202
203/// Tab size helper
204///
205/// This type makes it more obvious what unit is used to define the width of tabs.
206/// The default tab size is 4 spaces.
207#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
208pub enum TabSize {
209 /// Tab width as a number of pixels.
210 Pixels(u16),
211
212 /// Tab width as a number of space characters.
213 Spaces(u16),
214}
215
216impl TabSize {
217 /// Returns the default tab size, which is 4 spaces.
218 #[inline]
219 pub const fn default() -> Self {
220 Self::Spaces(4)
221 }
222
223 /// Calculate the rendered with of the next tab
224 #[inline]
225 pub(crate) fn into_pixels(self, renderer: &impl TextRenderer) -> u32 {
226 match self {
227 TabSize::Pixels(px) => px as u32,
228 TabSize::Spaces(n) => n as u32 * str_width(renderer, " "),
229 }
230 }
231}
232
233/// Styling options of a [`TextBox`].
234///
235/// `TextBoxStyle` contains the font, foreground and background `PixelColor`, line spacing,
236/// [`HeightMode`], [`HorizontalAlignment`] and [`VerticalAlignment`] information necessary
237/// to draw a [`TextBox`].
238///
239/// To construct a new `TextBoxStyle` object, use the [`TextBoxStyle::default`] method or
240/// the [`TextBoxStyleBuilder`] object.
241///
242/// [`TextBox`]: crate::TextBox
243#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
244#[non_exhaustive]
245#[must_use]
246pub struct TextBoxStyle {
247 /// Horizontal text alignment.
248 pub alignment: HorizontalAlignment,
249
250 /// Vertical text alignment.
251 pub vertical_alignment: VerticalAlignment,
252
253 /// The height behaviour.
254 pub height_mode: HeightMode,
255
256 /// Line height.
257 pub line_height: LineHeight,
258
259 /// Paragraph spacing.
260 pub paragraph_spacing: u32,
261
262 /// Desired column width for tabs
263 pub tab_size: TabSize,
264
265 /// True to render leading spaces
266 pub leading_spaces: bool,
267
268 /// True to render trailing spaces
269 pub trailing_spaces: bool,
270}
271
272impl TextBoxStyle {
273 /// Creates a new text box style object with default settings.
274 #[inline]
275 pub const fn default() -> Self {
276 TextBoxStyleBuilder::new().build()
277 }
278
279 /// Creates a new text box style with the given alignment.
280 #[inline]
281 pub const fn with_alignment(alignment: HorizontalAlignment) -> TextBoxStyle {
282 TextBoxStyleBuilder::new().alignment(alignment).build()
283 }
284
285 /// Creates a new text box style with the given vertical alignment.
286 #[inline]
287 pub const fn with_vertical_alignment(alignment: VerticalAlignment) -> TextBoxStyle {
288 TextBoxStyleBuilder::new()
289 .vertical_alignment(alignment)
290 .build()
291 }
292
293 /// Creates a new text box style with the given [height mode].
294 ///
295 /// [height mode]: HeightMode
296 #[inline]
297 pub const fn with_height_mode(mode: HeightMode) -> TextBoxStyle {
298 TextBoxStyleBuilder::new().height_mode(mode).build()
299 }
300
301 /// Creates a new text box style with the given line height.
302 #[inline]
303 pub const fn with_line_height(line_height: LineHeight) -> TextBoxStyle {
304 TextBoxStyleBuilder::new().line_height(line_height).build()
305 }
306
307 /// Creates a new text box style with the given paragraph spacing.
308 #[inline]
309 pub const fn with_paragraph_spacing(spacing: u32) -> TextBoxStyle {
310 TextBoxStyleBuilder::new()
311 .paragraph_spacing(spacing)
312 .build()
313 }
314
315 /// Creates a new text box style with the given tab size.
316 #[inline]
317 pub const fn with_tab_size(tab_size: TabSize) -> TextBoxStyle {
318 TextBoxStyleBuilder::new().tab_size(tab_size).build()
319 }
320}
321
322/// Information about a line.
323#[derive(Debug, Copy, Clone)]
324#[must_use]
325pub(crate) struct LineMeasurement {
326 /// Maximum line width in pixels.
327 pub max_line_width: u32,
328
329 /// Width in pixels, using the default space width returned by the text renderer.
330 pub width: u32,
331
332 /// What kind of line ending was encountered.
333 pub line_end_type: LineEndType,
334
335 /// Number of spaces in the current line.
336 pub space_count: u32,
337}
338
339impl LineMeasurement {
340 pub fn last_line(&self) -> bool {
341 matches!(
342 self.line_end_type,
343 LineEndType::NewLine | LineEndType::EndOfText
344 )
345 }
346
347 pub fn is_empty(&self) -> bool {
348 self.width == 0
349 }
350}
351
352struct MeasureLineElementHandler<'a, S> {
353 style: &'a S,
354 trailing_spaces: bool,
355 cursor: u32,
356 pos: u32,
357 right: u32,
358 partial_space_count: u32,
359 space_count: u32,
360}
361
362impl<'a, S> MeasureLineElementHandler<'a, S> {
363 fn space_count(&self) -> u32 {
364 if self.trailing_spaces {
365 self.partial_space_count
366 } else {
367 self.space_count
368 }
369 }
370
371 fn right(&self) -> u32 {
372 if self.trailing_spaces {
373 self.pos
374 } else {
375 self.right
376 }
377 }
378}
379
380impl<'a, S: TextRenderer> ElementHandler for MeasureLineElementHandler<'a, S> {
381 type Error = Infallible;
382 type Color = S::Color;
383
384 fn measure(&self, st: &str) -> u32 {
385 str_width(self.style, st)
386 }
387
388 fn measure_width_and_left_offset(&self, st: &str) -> (u32, u32) {
389 str_width_and_left_offset(self.style, st)
390 }
391
392 fn whitespace(&mut self, _st: &str, count: u32, width: u32) -> Result<(), Self::Error> {
393 self.cursor += width;
394 self.pos = self.pos.max(self.cursor);
395 self.partial_space_count += count;
396
397 Ok(())
398 }
399
400 fn printed_characters(&mut self, str: &str, width: Option<u32>) -> Result<(), Self::Error> {
401 self.cursor += width.unwrap_or_else(|| self.measure(str));
402 self.pos = self.pos.max(self.cursor);
403 self.right = self.pos;
404 self.space_count = self.partial_space_count;
405
406 Ok(())
407 }
408
409 fn move_cursor(&mut self, by: i32) -> Result<(), Self::Error> {
410 self.cursor = (self.cursor as i32 + by) as u32;
411
412 Ok(())
413 }
414}
415
416impl TextBoxStyle {
417 /// Measure the width and count spaces in a single line of text.
418 ///
419 /// Returns (width, rendered space count, carried token)
420 ///
421 /// Instead of peeking ahead when processing tokens, this function advances the parser before
422 /// processing a token. If a token opens a new line, it will be returned as the carried token.
423 /// If the carried token is `None`, the parser has finished processing the text.
424 #[inline]
425 pub(crate) fn measure_line<'a, S, M>(
426 &self,
427 plugin: &PluginWrapper<'a, M, S::Color>,
428 character_style: &S,
429 parser: &mut Parser<'a, S::Color>,
430 max_line_width: u32,
431 ) -> LineMeasurement
432 where
433 S: TextRenderer,
434 M: Plugin<'a, S::Color>,
435 {
436 let cursor = LineCursor::new(max_line_width, self.tab_size.into_pixels(character_style));
437
438 let mut iter = LineElementParser::new(
439 parser,
440 plugin,
441 cursor,
442 SpaceConfig::new(str_width(character_style, " "), None),
443 self,
444 );
445
446 let mut handler = MeasureLineElementHandler {
447 style: character_style,
448 trailing_spaces: self.trailing_spaces,
449
450 cursor: 0,
451 pos: 0,
452 right: 0,
453 partial_space_count: 0,
454 space_count: 0,
455 };
456 let last_token = iter.process(&mut handler).unwrap();
457
458 LineMeasurement {
459 max_line_width,
460 width: handler.right(),
461 space_count: handler.space_count(),
462 line_end_type: last_token,
463 }
464 }
465
466 /// Measures text height when rendered using a given width.
467 ///
468 /// # Example: measure height of text when rendered using a 6x9 MonoFont and 72px width.
469 ///
470 /// ```rust
471 /// # use embedded_text::style::TextBoxStyleBuilder;
472 /// # use embedded_graphics::{
473 /// # mono_font::{ascii::FONT_6X9, MonoTextStyleBuilder},
474 /// # pixelcolor::BinaryColor,
475 /// # };
476 /// #
477 /// let character_style = MonoTextStyleBuilder::new()
478 /// .font(&FONT_6X9)
479 /// .text_color(BinaryColor::On)
480 /// .build();
481 /// let style = TextBoxStyleBuilder::new().build();
482 ///
483 /// let height = style.measure_text_height(
484 /// &character_style,
485 /// "Lorem Ipsum is simply dummy text of the printing and typesetting industry.",
486 /// 72,
487 /// );
488 ///
489 /// // Expect 7 lines of text, wrapped in something like the following:
490 ///
491 /// // |Lorem Ipsum |
492 /// // |is simply |
493 /// // |dummy text |
494 /// // |of the |
495 /// // |printing and|
496 /// // |typesetting |
497 /// // |industry. |
498 ///
499 /// assert_eq!(7 * 9, height);
500 /// ```
501 #[inline]
502 #[must_use]
503 pub fn measure_text_height<S>(&self, character_style: &S, text: &str, max_width: u32) -> u32
504 where
505 S: TextRenderer,
506 {
507 let plugin = PluginWrapper::new(NoPlugin::new());
508 self.measure_text_height_impl(plugin, character_style, text, max_width)
509 }
510
511 pub(crate) fn measure_text_height_impl<'a, S, M>(
512 &self,
513 plugin: PluginWrapper<'a, M, S::Color>,
514 character_style: &S,
515 text: &'a str,
516 max_width: u32,
517 ) -> u32
518 where
519 S: TextRenderer,
520 M: Plugin<'a, S::Color>,
521 {
522 let mut parser = Parser::parse(text);
523 let base_line_height = character_style.line_height();
524 let line_height = self.line_height.to_absolute(base_line_height);
525 let mut height = base_line_height;
526
527 plugin.set_state(ProcessingState::Measure);
528
529 let mut prev_end = LineEndType::EndOfText;
530
531 loop {
532 plugin.new_line();
533 let lm = self.measure_line(&plugin, character_style, &mut parser, max_width);
534
535 if prev_end == LineEndType::LineBreak && !lm.is_empty() {
536 height += line_height;
537 }
538
539 match lm.line_end_type {
540 LineEndType::CarriageReturn | LineEndType::LineBreak => {}
541 LineEndType::NewLine => height += line_height + self.paragraph_spacing,
542 LineEndType::EndOfText => return height,
543 }
544 prev_end = lm.line_end_type;
545 }
546 }
547}
548
549#[cfg(test)]
550mod test {
551 use crate::{
552 alignment::*,
553 parser::Parser,
554 plugin::{NoPlugin, PluginWrapper},
555 style::{builder::TextBoxStyleBuilder, TextBoxStyle},
556 };
557 use embedded_graphics::{
558 mono_font::{ascii::FONT_6X9, MonoTextStyleBuilder},
559 pixelcolor::BinaryColor,
560 text::{renderer::TextRenderer, LineHeight},
561 };
562
563 #[test]
564 fn no_infinite_loop() {
565 let character_style = MonoTextStyleBuilder::new()
566 .font(&FONT_6X9)
567 .text_color(BinaryColor::On)
568 .build();
569
570 let _ = TextBoxStyleBuilder::new()
571 .build()
572 .measure_text_height(&character_style, "a", 5);
573 }
574
575 #[test]
576 fn test_measure_height() {
577 let data = [
578 // (text; max width in characters; number of expected lines)
579 ("", 0, 1),
580 (" ", 6, 1),
581 ("\r", 6, 1),
582 ("\n", 6, 2),
583 ("\n ", 6, 2),
584 ("word", 4 * 6, 1), // exact fit into 1 line
585 ("word\n", 4 * 6, 2), // newline
586 ("word", 4 * 6 - 1, 2),
587 ("word", 2 * 6, 2), // exact fit into 2 lines
588 ("word word", 4 * 6, 2), // exact fit into 2 lines
589 ("word\n", 2 * 6, 3),
590 ("word\nnext", 50, 2),
591 ("word\n\nnext", 50, 3),
592 ("word\n \nnext", 50, 3),
593 ("verylongword", 50, 2),
594 ("some verylongword", 50, 3),
595 ("1 23456 12345 61234 561", 36, 5),
596 (" Word ", 36, 2),
597 ("\rcr", 36, 1),
598 ("cr\r", 36, 1),
599 ("cr\rcr", 36, 1),
600 ("Longer\r", 36, 1),
601 ("Longer\rnowrap", 36, 1),
602 ];
603
604 let character_style = MonoTextStyleBuilder::new()
605 .font(&FONT_6X9)
606 .text_color(BinaryColor::On)
607 .build();
608
609 let style = TextBoxStyle::with_paragraph_spacing(2);
610
611 for (i, (text, width, expected_n_lines)) in data.iter().enumerate() {
612 let height = style.measure_text_height(&character_style, text, *width);
613 let expected_height = *expected_n_lines * character_style.line_height()
614 + (text.chars().filter(|&c| c == '\n').count() as u32) * style.paragraph_spacing;
615 assert_eq!(
616 height,
617 expected_height,
618 r#"#{}: Height of {:?} is {} but is expected to be {}"#,
619 i,
620 text.replace('\r', "\\r").replace('\n', "\\n"),
621 height,
622 expected_height
623 );
624 }
625 }
626
627 #[test]
628 fn test_measure_height_ignored_spaces() {
629 let data = [
630 ("", 0, 1),
631 (" ", 0, 1),
632 (" ", 6, 1),
633 ("\n ", 6, 2),
634 ("word\n \nnext", 50, 3),
635 (" Word ", 36, 1),
636 ];
637
638 let character_style = MonoTextStyleBuilder::new()
639 .font(&FONT_6X9)
640 .text_color(BinaryColor::On)
641 .build();
642
643 let style = TextBoxStyleBuilder::new()
644 .alignment(HorizontalAlignment::Center)
645 .build();
646
647 for (i, (text, width, expected_n_lines)) in data.iter().enumerate() {
648 let height = style.measure_text_height(&character_style, text, *width);
649 let expected_height = *expected_n_lines * character_style.line_height();
650 assert_eq!(
651 height, expected_height,
652 r#"#{}: Height of "{}" is {} but is expected to be {}"#,
653 i, text, height, expected_height
654 );
655 }
656 }
657
658 #[test]
659 fn test_measure_line() {
660 let character_style = MonoTextStyleBuilder::new()
661 .font(&FONT_6X9)
662 .text_color(BinaryColor::On)
663 .build();
664
665 let style = TextBoxStyleBuilder::new()
666 .alignment(HorizontalAlignment::Center)
667 .build();
668
669 let mut text = Parser::parse("123 45 67");
670
671 let mut plugin = PluginWrapper::new(NoPlugin::new());
672 let lm = style.measure_line(
673 &mut plugin,
674 &character_style,
675 &mut text,
676 6 * FONT_6X9.character_size.width,
677 );
678 assert_eq!(lm.width, 6 * FONT_6X9.character_size.width);
679 }
680
681 #[test]
682 fn test_measure_line_counts_nbsp() {
683 let character_style = MonoTextStyleBuilder::new()
684 .font(&FONT_6X9)
685 .text_color(BinaryColor::On)
686 .build();
687
688 let style = TextBoxStyleBuilder::new()
689 .alignment(HorizontalAlignment::Center)
690 .build();
691
692 let mut text = Parser::parse("123\u{A0}45");
693
694 let mut plugin = PluginWrapper::new(NoPlugin::new());
695 let lm = style.measure_line(
696 &mut plugin,
697 &character_style,
698 &mut text,
699 5 * FONT_6X9.character_size.width,
700 );
701 assert_eq!(lm.width, 5 * FONT_6X9.character_size.width);
702 }
703
704 #[test]
705 fn test_measure_height_nbsp() {
706 let character_style = MonoTextStyleBuilder::new()
707 .font(&FONT_6X9)
708 .text_color(BinaryColor::On)
709 .build();
710
711 let style = TextBoxStyleBuilder::new()
712 .alignment(HorizontalAlignment::Center)
713 .build();
714 let text = "123\u{A0}45 123";
715
716 let height =
717 style.measure_text_height(&character_style, text, 5 * FONT_6X9.character_size.width);
718 assert_eq!(height, 2 * character_style.line_height());
719
720 // bug discovered while using the interactive example
721 let style = TextBoxStyleBuilder::new()
722 .alignment(HorizontalAlignment::Left)
723 .build();
724
725 let text = "embedded-text also\u{A0}supports non-breaking spaces.";
726
727 let height = style.measure_text_height(&character_style, text, 79);
728 assert_eq!(height, 4 * character_style.line_height());
729 }
730
731 #[test]
732 fn height_with_line_spacing() {
733 let character_style = MonoTextStyleBuilder::new()
734 .font(&FONT_6X9)
735 .text_color(BinaryColor::On)
736 .build();
737
738 let style = TextBoxStyleBuilder::new()
739 .line_height(LineHeight::Pixels(11))
740 .build();
741
742 let height = style.measure_text_height(
743 &character_style,
744 "Lorem Ipsum is simply dummy text of the printing and typesetting industry.",
745 72,
746 );
747
748 assert_eq!(height, 6 * 11 + 9);
749 }
750
751 #[test]
752 fn soft_hyphenated_line_width_includes_hyphen_width() {
753 let character_style = MonoTextStyleBuilder::new()
754 .font(&FONT_6X9)
755 .text_color(BinaryColor::On)
756 .build();
757
758 let style = TextBoxStyleBuilder::new()
759 .line_height(LineHeight::Pixels(11))
760 .build();
761
762 let mut plugin = PluginWrapper::new(NoPlugin::new());
763 let lm = style.measure_line(
764 &mut plugin,
765 &character_style,
766 &mut Parser::parse("soft\u{AD}hyphen"),
767 50,
768 );
769
770 assert_eq!(lm.width, 30);
771 }
772}