embedded_text/rendering/
mod.rs

1//! Pixel iterators used for text rendering.
2
3pub(crate) mod cursor;
4pub(crate) mod line;
5pub(crate) mod line_iter;
6pub(crate) mod space_config;
7
8use crate::{
9    parser::Parser,
10    plugin::{PluginMarker as Plugin, ProcessingState},
11    rendering::{
12        cursor::Cursor,
13        line::{LineRenderState, StyledLineRenderer},
14    },
15    style::TextBoxStyle,
16    TextBox,
17};
18use az::SaturatingAs;
19use embedded_graphics::{
20    draw_target::{DrawTarget, DrawTargetExt},
21    prelude::{Dimensions, Point, Size},
22    primitives::Rectangle,
23    text::renderer::{CharacterStyle, TextRenderer},
24    Drawable,
25};
26use line_iter::LineEndType;
27
28/// Text box properties.
29///
30/// This struct holds information about the text box.
31#[derive(Clone)]
32pub struct TextBoxProperties<'a, S> {
33    /// The used text box style.
34    pub box_style: &'a TextBoxStyle,
35
36    /// The character style.
37    pub char_style: &'a S,
38
39    /// The height of the text.
40    pub text_height: i32,
41
42    /// The bounds of the text box.
43    pub bounding_box: Rectangle,
44}
45
46impl<'a, F, M> Drawable for TextBox<'a, F, M>
47where
48    F: TextRenderer<Color = <F as CharacterStyle>::Color> + CharacterStyle,
49    M: Plugin<'a, <F as TextRenderer>::Color> + Plugin<'a, <F as CharacterStyle>::Color>,
50    <F as CharacterStyle>::Color: Default,
51{
52    type Color = <F as CharacterStyle>::Color;
53    type Output = &'a str;
54
55    #[inline]
56    fn draw<D: DrawTarget<Color = Self::Color>>(
57        &self,
58        display: &mut D,
59    ) -> Result<&'a str, D::Error> {
60        let mut cursor = Cursor::new(
61            self.bounds,
62            self.character_style.line_height(),
63            self.style.line_height,
64            self.style.tab_size.into_pixels(&self.character_style),
65        );
66
67        let text_height = self
68            .style
69            .measure_text_height_impl(
70                self.plugin.clone(),
71                &self.character_style,
72                self.text,
73                cursor.line_width(),
74            )
75            .saturating_as::<i32>();
76
77        let box_height = self.bounding_box().size.height.saturating_as::<i32>();
78
79        self.style.vertical_alignment.apply_vertical_alignment(
80            &mut cursor,
81            text_height,
82            box_height,
83        );
84
85        cursor.y += self.vertical_offset;
86
87        let props = TextBoxProperties {
88            box_style: &self.style,
89            char_style: &self.character_style,
90            text_height,
91            bounding_box: self.bounding_box(),
92        };
93
94        self.plugin.on_start_render(&mut cursor, props);
95
96        let mut state = LineRenderState {
97            text_renderer: self.character_style.clone(),
98            parser: Parser::parse(self.text),
99            end_type: LineEndType::EndOfText,
100            plugin: &self.plugin,
101        };
102
103        state.plugin.set_state(ProcessingState::Render);
104
105        let mut anything_drawn = false;
106        loop {
107            state.plugin.new_line();
108
109            let display_range = self
110                .style
111                .height_mode
112                .calculate_displayed_row_range(&cursor);
113            let display_range_start = display_range.start.saturating_as::<i32>();
114            let display_range_count = display_range.count() as u32;
115            let display_size = Size::new(cursor.line_width(), display_range_count);
116
117            let line_start = cursor.line_start();
118
119            // FIXME: cropping isn't necessary for whole lines, but make sure not to blow up the
120            // binary size as well. We could also use a different way to consume invisible text.
121            let mut display = display.clipped(&Rectangle::new(
122                line_start + Point::new(0, display_range_start),
123                display_size,
124            ));
125            if display_range_count == 0 {
126                // Display range can be empty if we are above, or below the visible text section
127                if anything_drawn {
128                    // We are below, so we won't be drawing anything else
129                    let remaining_bytes = state.parser.as_str().len();
130                    let consumed_bytes = self.text.len() - remaining_bytes;
131
132                    state.plugin.post_render(
133                        &mut display,
134                        &self.character_style,
135                        None,
136                        Rectangle::new(line_start, Size::new(0, cursor.line_height())),
137                    )?;
138                    state.plugin.on_rendering_finished();
139                    return Ok(self.text.get(consumed_bytes..).unwrap());
140                }
141            } else {
142                anything_drawn = true;
143            }
144
145            StyledLineRenderer {
146                cursor: cursor.line(),
147                state: &mut state,
148                style: &self.style,
149            }
150            .draw(&mut display)?;
151
152            match state.end_type {
153                LineEndType::EndOfText => {
154                    state.plugin.on_rendering_finished();
155                    break;
156                }
157                LineEndType::CarriageReturn => {}
158                _ => {
159                    cursor.new_line();
160
161                    if state.end_type == LineEndType::NewLine {
162                        cursor.y += self.style.paragraph_spacing.saturating_as::<i32>();
163                    }
164                }
165            }
166        }
167
168        Ok("")
169    }
170}
171
172#[cfg(test)]
173pub mod test {
174    use embedded_graphics::{
175        mock_display::MockDisplay,
176        mono_font::{
177            ascii::{FONT_6X10, FONT_6X9},
178            MonoTextStyleBuilder,
179        },
180        pixelcolor::BinaryColor,
181        prelude::*,
182        primitives::Rectangle,
183    };
184
185    use crate::{
186        alignment::HorizontalAlignment,
187        style::{HeightMode, TextBoxStyle, TextBoxStyleBuilder, VerticalOverdraw},
188        utils::test::{size_for, TestFont},
189        TextBox,
190    };
191
192    #[track_caller]
193    pub fn assert_rendered(
194        alignment: HorizontalAlignment,
195        text: &str,
196        size: Size,
197        pattern: &[&str],
198    ) {
199        assert_styled_rendered(
200            TextBoxStyleBuilder::new().alignment(alignment).build(),
201            text,
202            size,
203            pattern,
204        );
205    }
206
207    #[track_caller]
208    pub fn assert_styled_rendered(style: TextBoxStyle, text: &str, size: Size, pattern: &[&str]) {
209        let mut display = MockDisplay::new();
210
211        let character_style = MonoTextStyleBuilder::new()
212            .font(&FONT_6X9)
213            .text_color(BinaryColor::On)
214            .background_color(BinaryColor::Off)
215            .build();
216
217        TextBox::with_textbox_style(
218            text,
219            Rectangle::new(Point::zero(), size),
220            character_style,
221            style,
222        )
223        .draw(&mut display)
224        .unwrap();
225
226        display.assert_pattern(pattern);
227    }
228
229    #[test]
230    fn nbsp_doesnt_break() {
231        assert_rendered(
232            HorizontalAlignment::Left,
233            "a b c\u{a0}d e f",
234            size_for(&FONT_6X9, 5, 3),
235            &[
236                "..................            ",
237                ".............#....            ",
238                ".............#....            ",
239                "..###........###..            ",
240                ".#..#........#..#.            ",
241                ".#..#........#..#.            ",
242                "..###........###..            ",
243                "..................            ",
244                "..................            ",
245                "..............................",
246                "................#.............",
247                "................#.............",
248                "..###.........###.........##..",
249                ".#...........#..#........#.##.",
250                ".#...........#..#........##...",
251                "..###.........###.........###.",
252                "..............................",
253                "..............................",
254                "......                        ",
255                "...#..                        ",
256                "..#.#.                        ",
257                "..#...                        ",
258                ".###..                        ",
259                "..#...                        ",
260                "..#...                        ",
261                "......                        ",
262                "......                        ",
263            ],
264        );
265    }
266
267    #[test]
268    fn vertical_offset() {
269        let mut display = MockDisplay::new();
270
271        let character_style = MonoTextStyleBuilder::new()
272            .font(&FONT_6X9)
273            .text_color(BinaryColor::On)
274            .background_color(BinaryColor::Off)
275            .build();
276
277        TextBox::new(
278            "hello",
279            Rectangle::new(Point::zero(), size_for(&FONT_6X9, 5, 3)),
280            character_style,
281        )
282        .set_vertical_offset(6)
283        .draw(&mut display)
284        .unwrap();
285
286        display.assert_pattern(&[
287            "                              ",
288            "                              ",
289            "                              ",
290            "                              ",
291            "                              ",
292            "                              ",
293            "..............................",
294            ".#...........##....##.........",
295            ".#............#.....#.........",
296            ".###....##....#.....#.....##..",
297            ".#..#..#.##...#.....#....#..#.",
298            ".#..#..##.....#.....#....#..#.",
299            ".#..#...###..###...###....##..",
300            "..............................",
301            "..............................",
302        ]);
303    }
304
305    #[test]
306    fn vertical_offset_negative() {
307        let mut display = MockDisplay::new();
308
309        let character_style = MonoTextStyleBuilder::new()
310            .font(&FONT_6X9)
311            .text_color(BinaryColor::On)
312            .background_color(BinaryColor::Off)
313            .build();
314
315        TextBox::with_textbox_style(
316            "hello",
317            Rectangle::new(Point::zero(), size_for(&FONT_6X9, 5, 3)),
318            character_style,
319            TextBoxStyleBuilder::new()
320                .height_mode(HeightMode::Exact(VerticalOverdraw::Hidden))
321                .build(),
322        )
323        .set_vertical_offset(-4)
324        .draw(&mut display)
325        .unwrap();
326
327        display.assert_pattern(&[
328            ".#..#..#.##...#.....#....#..#.",
329            ".#..#..##.....#.....#....#..#.",
330            ".#..#...###..###...###....##..",
331            "..............................",
332            "..............................",
333        ]);
334    }
335
336    #[test]
337    fn rendering_not_stopped_prematurely() {
338        let mut display = MockDisplay::new();
339
340        let character_style = MonoTextStyleBuilder::new()
341            .font(&FONT_6X10)
342            .text_color(BinaryColor::On)
343            .background_color(BinaryColor::Off)
344            .build();
345
346        TextBox::with_textbox_style(
347            "hello\nbuggy\nworld",
348            Rectangle::new(Point::zero(), size_for(&FONT_6X10, 5, 3)),
349            character_style,
350            TextBoxStyleBuilder::new()
351                .height_mode(HeightMode::Exact(VerticalOverdraw::Hidden))
352                .build(),
353        )
354        .set_vertical_offset(-20)
355        .draw(&mut display)
356        .unwrap();
357
358        display.assert_pattern(&[
359            "..............................",
360            "...................##.......#.",
361            "....................#.......#.",
362            "#...#..###..#.##....#....##.#.",
363            "#...#.#...#.##..#...#...#..##.",
364            "#.#.#.#...#.#.......#...#...#.",
365            "#.#.#.#...#.#.......#...#..##.",
366            ".#.#...###..#......###...##.#.",
367            "..............................",
368            "..............................",
369        ]);
370    }
371
372    #[test]
373    fn space_wrapping_issue() {
374        let mut display = MockDisplay::new();
375
376        let character_style = MonoTextStyleBuilder::new()
377            .font(&FONT_6X10)
378            .text_color(BinaryColor::On)
379            .background_color(BinaryColor::Off)
380            .build();
381
382        TextBox::with_textbox_style(
383            "Hello,      s",
384            Rectangle::new(Point::zero(), size_for(&FONT_6X10, 10, 2)),
385            character_style,
386            TextBoxStyleBuilder::new()
387                .height_mode(HeightMode::Exact(VerticalOverdraw::Hidden))
388                .trailing_spaces(true)
389                .build(),
390        )
391        .draw(&mut display)
392        .unwrap();
393
394        display.assert_pattern(&[
395            "............................................................",
396            "#...#........##....##.......................................",
397            "#...#.........#.....#.......................................",
398            "#...#..###....#.....#....###................................",
399            "#####.#...#...#.....#...#...#...............................",
400            "#...#.#####...#.....#...#...#...............................",
401            "#...#.#.......#.....#...#...#...##..........................",
402            "#...#..###...###...###...###....#...........................",
403            "...............................#............................",
404            "............................................................",
405            "............                                                ",
406            "............                                                ",
407            "............                                                ",
408            ".......###..                                                ",
409            "......#.....                                                ",
410            ".......###..                                                ",
411            "..........#.                                                ",
412            "......####..                                                ",
413            "............                                                ",
414            "............                                                ",
415        ]);
416    }
417
418    #[test]
419    fn rendering_justified_text_with_negative_left_side_bearing() {
420        let mut display: MockDisplay<BinaryColor> = MockDisplay::new();
421        display.set_allow_overdraw(true);
422
423        let text = "j000 0 j00 00j00 0";
424        let character_style = TestFont::new(BinaryColor::On, BinaryColor::Off);
425        let size = Size::new(50, 0);
426
427        TextBox::with_textbox_style(
428            text,
429            Rectangle::new(Point::zero(), size),
430            character_style,
431            TextBoxStyleBuilder::new()
432                .alignment(HorizontalAlignment::Justified)
433                .height_mode(HeightMode::FitToText)
434                .build(),
435        )
436        .draw(&mut display)
437        .unwrap();
438
439        display.assert_pattern(&[
440            "..#.####.####.####.........####........#.####.####",
441            "....#..#.#..#.#..#.........#..#..........#..#.#..#",
442            "..#.#..#.#..#.#..#.........#..#........#.#..#.#..#",
443            "..#.#..#.#..#.#..#.........#..#........#.#..#.#..#",
444            "..#.#..#.#..#.#..#.........#..#........#.#..#.#..#",
445            "..#.#..#.#..#.#..#.........#..#........#.#..#.#..#",
446            "..#.####.####.####.........####........#.####.####",
447            "..#....................................#..........",
448            "..#....................................#..........",
449            "##...................................##...........",
450            "####.####.#.####.####....####                     ",
451            "#..#.#..#...#..#.#..#....#..#                     ",
452            "#..#.#..#.#.#..#.#..#....#..#                     ",
453            "#..#.#..#.#.#..#.#..#....#..#                     ",
454            "#..#.#..#.#.#..#.#..#....#..#                     ",
455            "#..#.#..#.#.#..#.#..#....#..#                     ",
456            "####.####.#.####.####....####                     ",
457            "..........#..................                     ",
458            "..........#..................                     ",
459            "........##...................                     ",
460        ]);
461    }
462
463    #[test]
464    fn correctly_breaks_long_words_for_monospace_fonts() {
465        let mut display: MockDisplay<BinaryColor> = MockDisplay::new();
466        display.set_allow_overdraw(true);
467
468        let text = "000000000000000000";
469        let character_style = MonoTextStyleBuilder::new()
470            .font(&FONT_6X10)
471            .text_color(BinaryColor::On)
472            .background_color(BinaryColor::Off)
473            .build();
474
475        TextBox::with_textbox_style(
476            text,
477            Rectangle::new(Point::zero(), size_for(&FONT_6X10, 10, 2)),
478            character_style,
479            TextBoxStyleBuilder::new()
480                .alignment(HorizontalAlignment::Left)
481                .height_mode(HeightMode::FitToText)
482                .build(),
483        )
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        ]);
509    }
510
511    #[test]
512    fn correctly_breaks_long_words_for_fonts_with_letter_spacing() {
513        let mut display: MockDisplay<BinaryColor> = MockDisplay::new();
514        display.set_allow_overdraw(true);
515
516        let text = "000000000000000000";
517        let character_style = TestFont::new(BinaryColor::On, BinaryColor::Off);
518        let size = Size::new(49, 0);
519
520        TextBox::with_textbox_style(
521            text,
522            Rectangle::new(Point::zero(), size),
523            character_style,
524            TextBoxStyleBuilder::new()
525                .alignment(HorizontalAlignment::Left)
526                .height_mode(HeightMode::FitToText)
527                .build(),
528        )
529        .draw(&mut display)
530        .unwrap();
531
532        display.assert_pattern(&[
533            "####.####.####.####.####.####.####.####.####.####",
534            "#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#",
535            "#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#",
536            "#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#",
537            "#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#",
538            "#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#",
539            "####.####.####.####.####.####.####.####.####.####",
540            ".................................................",
541            ".................................................",
542            ".................................................",
543            "####.####.####.####.####.####.####.####          ",
544            "#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#          ",
545            "#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#          ",
546            "#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#          ",
547            "#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#          ",
548            "#..#.#..#.#..#.#..#.#..#.#..#.#..#.#..#          ",
549            "####.####.####.####.####.####.####.####          ",
550            ".......................................          ",
551            ".......................................          ",
552            ".......................................          ",
553        ]);
554    }
555
556    #[test]
557    fn correctly_breaks_long_words_with_wide_utf8_characters() {
558        let mut display = MockDisplay::new();
559        let character_style = MonoTextStyleBuilder::new()
560            .font(&FONT_6X10)
561            .text_color(BinaryColor::On)
562            .background_color(BinaryColor::Off)
563            .build();
564
565        TextBox::with_textbox_style(
566            "広広広", // MonoText replaces unrecognized characters with "?"
567            Rectangle::new(Point::zero(), size_for(&FONT_6X10, 2, 2)),
568            character_style,
569            TextBoxStyleBuilder::new().build(),
570        )
571        .draw(&mut display)
572        .unwrap();
573
574        display.assert_pattern(&[
575            "............",
576            ".###...###..",
577            "#...#.#...#.",
578            "...#.....#..",
579            "..#.....#...",
580            "..#.....#...",
581            "............",
582            "..#.....#...",
583            "............",
584            "............",
585            "......      ",
586            ".###..      ",
587            "#...#.      ",
588            "...#..      ",
589            "..#...      ",
590            "..#...      ",
591            "......      ",
592            "..#...      ",
593            "......      ",
594            "......      ",
595        ]);
596    }
597}