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