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}