embedded-text 0.5.0-beta.2

TextBox for embedded-graphics
Documentation
//! Pixel iterators used for text rendering.
#[cfg(feature = "ansi")]
mod ansi;
pub(crate) mod cursor;
mod line;
pub(crate) mod line_iter;
pub(crate) mod space_config;

use crate::{
    parser::Parser,
    plugin::ProcessingState,
    rendering::{
        cursor::Cursor,
        line::{LineRenderState, StyledLineRenderer},
    },
    Plugin, TextBox,
};
use az::SaturatingAs;
use embedded_graphics::{
    draw_target::{DrawTarget, DrawTargetExt},
    pixelcolor::Rgb888,
    prelude::{Point, Size},
    primitives::Rectangle,
    text::renderer::{CharacterStyle, TextRenderer},
    Drawable,
};
use line_iter::LineEndType;

impl<'a, F, M> Drawable for TextBox<'a, F, M>
where
    F: TextRenderer<Color = <F as CharacterStyle>::Color> + CharacterStyle,
    <F as CharacterStyle>::Color: From<Rgb888>,
    M: Plugin<'a, <F as TextRenderer>::Color> + Plugin<'a, <F as CharacterStyle>::Color>,
{
    type Color = <F as CharacterStyle>::Color;
    type Output = &'a str;

    #[inline]
    fn draw<D: DrawTarget<Color = Self::Color>>(
        &self,
        display: &mut D,
    ) -> Result<&'a str, D::Error> {
        let mut cursor = Cursor::new(
            self.bounds,
            self.character_style.line_height(),
            self.style.line_height,
            self.style.tab_size.into_pixels(&self.character_style),
        );

        self.style
            .vertical_alignment
            .apply_vertical_alignment(&mut cursor, self);

        cursor.y += self.vertical_offset;
        self.plugin.on_start_render(self, &mut cursor);

        let mut state = LineRenderState {
            style: self.style,
            character_style: self.character_style.clone(),
            parser: Parser::parse(self.text),
            end_type: LineEndType::EndOfText,
            plugin: &self.plugin,
        };

        state.plugin.set_state(ProcessingState::Render);

        let mut anything_drawn = false;
        loop {
            state.plugin.new_line();
            let line_cursor = cursor.line();

            let display_range = self
                .style
                .height_mode
                .calculate_displayed_row_range(&cursor);
            let display_size = Size::new(
                cursor.line_width(),
                display_range.clone().count().saturating_as(),
            );

            let line_start = line_cursor.pos();

            // FIXME: cropping isn't necessary for whole lines, but make sure not to blow up the
            // binary size as well.
            let mut display = display.clipped(&Rectangle::new(
                line_start + Point::new(0, display_range.start),
                display_size,
            ));
            if display_range.start == display_range.end {
                if anything_drawn {
                    let remaining_bytes = state.parser.as_str().len();
                    let consumed_bytes = self.text.len() - remaining_bytes;

                    state.plugin.post_render(
                        &mut display,
                        &self.character_style,
                        "",
                        Rectangle::new(
                            line_start,
                            Size::new(0, cursor.line_height().saturating_as()),
                        ),
                    )?;
                    return Ok(self.text.get(consumed_bytes..).unwrap());
                }
            } else {
                anything_drawn = true;
            }

            state = StyledLineRenderer::new(line_cursor, state).draw(&mut display)?;

            match state.end_type {
                LineEndType::EndOfText => break,
                LineEndType::CarriageReturn => {}
                _ => {
                    cursor.new_line();

                    if state.end_type == LineEndType::NewLine {
                        cursor.y += self.style.paragraph_spacing.saturating_as::<i32>();
                    }
                }
            }
        }

        Ok("")
    }
}

#[cfg(test)]
pub mod test {
    use embedded_graphics::{
        mock_display::MockDisplay,
        mono_font::{ascii::FONT_6X9, MonoTextStyleBuilder},
        pixelcolor::BinaryColor,
        prelude::*,
        primitives::Rectangle,
    };

    use crate::{
        alignment::HorizontalAlignment,
        style::{HeightMode, TextBoxStyleBuilder, VerticalOverdraw},
        utils::test::size_for,
        TextBox,
    };

    #[track_caller]
    pub fn assert_rendered(
        alignment: HorizontalAlignment,
        text: &str,
        size: Size,
        pattern: &[&str],
    ) {
        let mut display = MockDisplay::new();

        let character_style = MonoTextStyleBuilder::new()
            .font(&FONT_6X9)
            .text_color(BinaryColor::On)
            .background_color(BinaryColor::Off)
            .build();

        let style = TextBoxStyleBuilder::new().alignment(alignment).build();

        TextBox::with_textbox_style(
            text,
            Rectangle::new(Point::zero(), size),
            character_style,
            style,
        )
        .draw(&mut display)
        .unwrap();

        display.assert_pattern(pattern);
    }

    #[test]
    fn nbsp_doesnt_break() {
        assert_rendered(
            HorizontalAlignment::Left,
            "a b c\u{a0}d e f",
            size_for(&FONT_6X9, 5, 3),
            &[
                "..................            ",
                ".............#....            ",
                ".............#....            ",
                "..###........###..            ",
                ".#..#........#..#.            ",
                ".#..#........#..#.            ",
                "..###........###..            ",
                "..................            ",
                "..................            ",
                "..............................",
                "................#.............",
                "................#.............",
                "..###.........###.........##..",
                ".#...........#..#........#.##.",
                ".#...........#..#........##...",
                "..###.........###.........###.",
                "..............................",
                "..............................",
                "......                        ",
                "...#..                        ",
                "..#.#.                        ",
                "..#...                        ",
                ".###..                        ",
                "..#...                        ",
                "..#...                        ",
                "......                        ",
                "......                        ",
            ],
        );
    }

    #[test]
    fn vertical_offset() {
        let mut display = MockDisplay::new();

        let character_style = MonoTextStyleBuilder::new()
            .font(&FONT_6X9)
            .text_color(BinaryColor::On)
            .background_color(BinaryColor::Off)
            .build();

        TextBox::new(
            "hello",
            Rectangle::new(Point::zero(), size_for(&FONT_6X9, 5, 3)),
            character_style,
        )
        .set_vertical_offset(6)
        .draw(&mut display)
        .unwrap();

        display.assert_pattern(&[
            "                              ",
            "                              ",
            "                              ",
            "                              ",
            "                              ",
            "                              ",
            "..............................",
            ".#...........##....##.........",
            ".#............#.....#.........",
            ".###....##....#.....#.....##..",
            ".#..#..#.##...#.....#....#..#.",
            ".#..#..##.....#.....#....#..#.",
            ".#..#...###..###...###....##..",
            "..............................",
            "..............................",
        ]);
    }

    #[test]
    fn vertical_offset_negative() {
        let mut display = MockDisplay::new();

        let character_style = MonoTextStyleBuilder::new()
            .font(&FONT_6X9)
            .text_color(BinaryColor::On)
            .background_color(BinaryColor::Off)
            .build();

        TextBox::with_textbox_style(
            "hello",
            Rectangle::new(Point::zero(), size_for(&FONT_6X9, 5, 3)),
            character_style,
            TextBoxStyleBuilder::new()
                .height_mode(HeightMode::Exact(VerticalOverdraw::Hidden))
                .build(),
        )
        .set_vertical_offset(-4)
        .draw(&mut display)
        .unwrap();

        display.assert_pattern(&[
            ".#..#..#.##...#.....#....#..#.",
            ".#..#..##.....#.....#....#..#.",
            ".#..#...###..###...###....##..",
            "..............................",
            "..............................",
        ]);
    }
}