embedded_text/rendering/
line.rs

1//! Line rendering.
2
3use crate::{
4    parser::{ChangeTextStyle, Parser},
5    plugin::{PluginMarker as Plugin, PluginWrapper, ProcessingState},
6    rendering::{
7        cursor::LineCursor,
8        line_iter::{ElementHandler, LineElementParser, LineEndType},
9    },
10    style::TextBoxStyle,
11    utils::{str_width, str_width_and_left_offset},
12};
13use embedded_graphics::{
14    draw_target::DrawTarget,
15    geometry::Point,
16    prelude::{PixelColor, Size},
17    primitives::Rectangle,
18    text::{
19        renderer::{CharacterStyle, TextRenderer},
20        Baseline, DecorationColor,
21    },
22};
23
24impl<C> ChangeTextStyle<C>
25where
26    C: PixelColor,
27{
28    pub(crate) fn apply<S: CharacterStyle<Color = C>>(self, text_renderer: &mut S) {
29        match self {
30            ChangeTextStyle::Reset => {
31                text_renderer.set_text_color(None);
32                text_renderer.set_background_color(None);
33                text_renderer.set_underline_color(DecorationColor::None);
34                text_renderer.set_strikethrough_color(DecorationColor::None);
35            }
36            ChangeTextStyle::TextColor(color) => text_renderer.set_text_color(color),
37            ChangeTextStyle::BackgroundColor(color) => text_renderer.set_background_color(color),
38            ChangeTextStyle::Underline(color) => text_renderer.set_underline_color(color),
39            ChangeTextStyle::Strikethrough(color) => text_renderer.set_strikethrough_color(color),
40        }
41    }
42}
43
44/// Render a single line of styled text.
45pub(crate) struct StyledLineRenderer<'a, 'b, 'c, S, M>
46where
47    S: TextRenderer + Clone,
48    M: Plugin<'a, <S as TextRenderer>::Color>,
49{
50    pub(crate) cursor: LineCursor,
51    pub(crate) state: &'c mut LineRenderState<'a, 'b, S, M>,
52    pub(crate) style: &'c TextBoxStyle,
53}
54
55#[derive(Clone)]
56pub(crate) struct LineRenderState<'a, 'b, S, M>
57where
58    S: TextRenderer + Clone,
59    M: Plugin<'a, S::Color>,
60{
61    pub parser: Parser<'a, S::Color>,
62    pub text_renderer: S,
63    pub end_type: LineEndType,
64    pub plugin: &'b PluginWrapper<'a, M, S::Color>,
65}
66
67struct RenderElementHandler<'a, 'b, F, D, M>
68where
69    F: TextRenderer,
70    D: DrawTarget<Color = F::Color>,
71{
72    text_renderer: &'b mut F,
73    display: &'b mut D,
74    pos: Point,
75    plugin: &'b PluginWrapper<'a, M, F::Color>,
76}
77
78impl<'a, 'b, F, D, M> RenderElementHandler<'a, 'b, F, D, M>
79where
80    F: CharacterStyle + TextRenderer,
81    D: DrawTarget<Color = <F as TextRenderer>::Color>,
82    M: Plugin<'a, <F as TextRenderer>::Color>,
83{
84    fn post_print(&mut self, width: u32, st: &str) -> Result<(), D::Error> {
85        let bounds = Rectangle::new(self.pos, Size::new(width, self.text_renderer.line_height()));
86
87        self.pos += Point::new(width as i32, 0);
88
89        self.plugin
90            .post_render(self.display, self.text_renderer, Some(st), bounds)
91    }
92}
93
94impl<'a, 'c, F, D, M> ElementHandler for RenderElementHandler<'a, 'c, F, D, M>
95where
96    F: CharacterStyle + TextRenderer,
97    D: DrawTarget<Color = <F as TextRenderer>::Color>,
98    M: Plugin<'a, <F as TextRenderer>::Color>,
99{
100    type Error = D::Error;
101    type Color = <F as CharacterStyle>::Color;
102
103    fn measure(&self, st: &str) -> u32 {
104        str_width(self.text_renderer, st)
105    }
106
107    fn measure_width_and_left_offset(&self, st: &str) -> (u32, u32) {
108        str_width_and_left_offset(self.text_renderer, st)
109    }
110
111    fn whitespace(&mut self, st: &str, _space_count: u32, width: u32) -> Result<(), Self::Error> {
112        if width > 0 {
113            self.text_renderer
114                .draw_whitespace(width, self.pos, Baseline::Top, self.display)?;
115        }
116
117        self.post_print(width, st)
118    }
119
120    fn printed_characters(&mut self, st: &str, width: Option<u32>) -> Result<(), Self::Error> {
121        let render_width =
122            self.text_renderer
123                .draw_string(st, self.pos, Baseline::Top, self.display)?;
124
125        let width = width.unwrap_or((render_width - self.pos).x as u32);
126
127        self.post_print(width, st)
128    }
129
130    fn move_cursor(&mut self, by: i32) -> Result<(), Self::Error> {
131        // LineElementIterator ensures this new pos is valid.
132        self.pos += Point::new(by, 0);
133        Ok(())
134    }
135
136    fn change_text_style(
137        &mut self,
138        change: ChangeTextStyle<<F as CharacterStyle>::Color>,
139    ) -> Result<(), Self::Error> {
140        change.apply(self.text_renderer);
141        Ok(())
142    }
143}
144
145impl<'a, 'b, 'c, F, M> StyledLineRenderer<'a, 'b, 'c, F, M>
146where
147    F: TextRenderer<Color = <F as CharacterStyle>::Color> + CharacterStyle,
148    M: Plugin<'a, <F as TextRenderer>::Color> + Plugin<'a, <F as CharacterStyle>::Color>,
149{
150    #[inline]
151    pub(crate) fn draw<D>(mut self, display: &mut D) -> Result<(), D::Error>
152    where
153        D: DrawTarget<Color = <F as CharacterStyle>::Color>,
154    {
155        let LineRenderState {
156            ref mut parser,
157            ref mut text_renderer,
158            plugin,
159            ..
160        } = self.state;
161
162        let lm = {
163            // Ensure the clone lives for as short as possible.
164            let mut cloned_parser = parser.clone();
165            let measure_plugin = plugin.clone();
166            measure_plugin.set_state(ProcessingState::Measure);
167            self.style.measure_line(
168                &measure_plugin,
169                text_renderer,
170                &mut cloned_parser,
171                self.cursor.line_width(),
172            )
173        };
174
175        let (left, space_config) = self.style.alignment.place_line(text_renderer, lm);
176
177        self.cursor.move_cursor(left).ok();
178
179        let mut render_element_handler = RenderElementHandler {
180            text_renderer,
181            display,
182            pos: self.cursor.pos(),
183            plugin: *plugin,
184        };
185        let end_type =
186            LineElementParser::new(parser, plugin, self.cursor, space_config, self.style)
187                .process(&mut render_element_handler)?;
188
189        if end_type == LineEndType::EndOfText {
190            let end_pos = render_element_handler.pos;
191            plugin.post_render(
192                display,
193                text_renderer,
194                None,
195                Rectangle::new(end_pos, Size::new(0, text_renderer.line_height())),
196            )?;
197        }
198
199        self.state.end_type = end_type;
200
201        Ok(())
202    }
203}
204
205#[cfg(test)]
206mod test {
207    use crate::{
208        parser::Parser,
209        plugin::{NoPlugin, PluginWrapper},
210        rendering::{
211            cursor::LineCursor,
212            line::{LineRenderState, StyledLineRenderer},
213            line_iter::LineEndType,
214        },
215        style::{TabSize, TextBoxStyle, TextBoxStyleBuilder},
216        utils::test::size_for,
217    };
218    use embedded_graphics::{
219        geometry::Point,
220        mock_display::MockDisplay,
221        mono_font::{ascii::FONT_6X9, MonoTextStyleBuilder},
222        pixelcolor::BinaryColor,
223        primitives::Rectangle,
224        text::renderer::{CharacterStyle, TextRenderer},
225    };
226
227    fn test_rendered_text<'a, S>(
228        text: &'a str,
229        bounds: Rectangle,
230        character_style: S,
231        style: TextBoxStyle,
232        pattern: &[&str],
233    ) where
234        S: TextRenderer<Color = <S as CharacterStyle>::Color> + CharacterStyle,
235        <S as CharacterStyle>::Color: embedded_graphics::mock_display::ColorMapping,
236    {
237        let parser = Parser::parse(text);
238        let cursor = LineCursor::new(
239            bounds.size.width,
240            TabSize::Spaces(4).into_pixels(&character_style),
241        );
242
243        let plugin = PluginWrapper::new(NoPlugin::new());
244
245        let mut state = LineRenderState {
246            parser,
247            text_renderer: character_style,
248            end_type: LineEndType::EndOfText,
249            plugin: &plugin,
250        };
251
252        let renderer = StyledLineRenderer {
253            cursor,
254            state: &mut state,
255            style: &style,
256        };
257        let mut display = MockDisplay::new();
258        display.set_allow_overdraw(true);
259
260        renderer.draw(&mut display).unwrap();
261
262        display.assert_pattern(pattern);
263    }
264
265    #[test]
266    fn simple_render() {
267        let character_style = MonoTextStyleBuilder::new()
268            .font(&FONT_6X9)
269            .text_color(BinaryColor::On)
270            .background_color(BinaryColor::Off)
271            .build();
272
273        let style = TextBoxStyleBuilder::new().build();
274
275        test_rendered_text(
276            "Some sample text",
277            Rectangle::new(Point::zero(), size_for(&FONT_6X9, 7, 1)),
278            character_style,
279            style,
280            &[
281                "........................",
282                "..##....................",
283                ".#..#...................",
284                "..#.....##..##.#....##..",
285                "...#...#..#.#.#.#..#.##.",
286                ".#..#..#..#.#.#.#..##...",
287                "..##....##..#...#...###.",
288                "........................",
289                "........................",
290            ],
291        );
292    }
293
294    #[test]
295    fn simple_render_nbsp() {
296        let character_style = MonoTextStyleBuilder::new()
297            .font(&FONT_6X9)
298            .text_color(BinaryColor::On)
299            .background_color(BinaryColor::Off)
300            .build();
301
302        let style = TextBoxStyleBuilder::new().build();
303
304        test_rendered_text(
305            "Some\u{A0}sample text",
306            Rectangle::new(Point::zero(), size_for(&FONT_6X9, 7, 1)),
307            character_style,
308            style,
309            &[
310                "..........................................",
311                "..##......................................",
312                ".#..#.....................................",
313                "..#.....##..##.#....##..........###...###.",
314                "...#...#..#.#.#.#..#.##........##....#..#.",
315                ".#..#..#..#.#.#.#..##............##..#..#.",
316                "..##....##..#...#...###........###....###.",
317                "..........................................",
318                "..........................................",
319            ],
320        );
321    }
322
323    #[test]
324    fn simple_render_first_word_not_wrapped() {
325        let character_style = MonoTextStyleBuilder::new()
326            .font(&FONT_6X9)
327            .text_color(BinaryColor::On)
328            .background_color(BinaryColor::Off)
329            .build();
330
331        let style = TextBoxStyleBuilder::new().build();
332
333        test_rendered_text(
334            "Some sample text",
335            Rectangle::new(Point::zero(), size_for(&FONT_6X9, 2, 1)),
336            character_style,
337            style,
338            &[
339                "............",
340                "..##........",
341                ".#..#.......",
342                "..#.....##..",
343                "...#...#..#.",
344                ".#..#..#..#.",
345                "..##....##..",
346                "............",
347                "............",
348            ],
349        );
350    }
351
352    #[test]
353    fn newline_stops_render() {
354        let character_style = MonoTextStyleBuilder::new()
355            .font(&FONT_6X9)
356            .text_color(BinaryColor::On)
357            .background_color(BinaryColor::Off)
358            .build();
359
360        let style = TextBoxStyleBuilder::new().build();
361
362        test_rendered_text(
363            "Some \nsample text",
364            Rectangle::new(Point::zero(), size_for(&FONT_6X9, 7, 1)),
365            character_style,
366            style,
367            &[
368                "........................",
369                "..##....................",
370                ".#..#...................",
371                "..#.....##..##.#....##..",
372                "...#...#..#.#.#.#..#.##.",
373                ".#..#..#..#.#.#.#..##...",
374                "..##....##..#...#...###.",
375                "........................",
376                "........................",
377            ],
378        );
379    }
380}