1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
//! Display the last lines of the text.

use az::SaturatingAs;
use embedded_graphics::{
    prelude::PixelColor,
    text::renderer::{CharacterStyle, TextRenderer},
};

use crate::{
    plugin::Plugin,
    rendering::{cursor::Cursor, TextBoxProperties},
};

/// Text tail display plugin.
///
/// Aligns the last line of the text to be always visible. If the text fits inside the text box,
/// it will be top aligned. If the text is longer, it will be bottom aligned.
#[derive(Clone)]
pub struct Tail;

impl<'a, C: PixelColor> Plugin<'a, C> for Tail {
    fn on_start_render<S: CharacterStyle + TextRenderer>(
        &mut self,
        cursor: &mut Cursor,
        props: &TextBoxProperties<'_, S>,
    ) {
        let box_height = props.bounding_box.size.height.saturating_as();
        if props.text_height > box_height {
            let offset = box_height - props.text_height;

            cursor.y += offset
        }
    }
}

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

    use crate::{plugin::tail::Tail, style::TextBoxStyle, utils::test::size_for, TextBox};

    #[track_caller]
    pub fn assert_rendered(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 = TextBoxStyle::default();

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

        display.assert_pattern(pattern);
    }

    #[test]
    fn scrolling_behaves_as_top_if_lines_dont_overflow() {
        assert_rendered(
            "word",
            size_for(&FONT_6X9, 4, 2),
            &[
                "........................",
                "......................#.",
                "......................#.",
                "#...#...##...#.#....###.",
                "#.#.#..#..#..##.#..#..#.",
                "#.#.#..#..#..#.....#..#.",
                ".#.#....##...#......###.",
                "........................",
                "........................",
                "                        ",
                "                        ",
                "                        ",
                "                        ",
                "                        ",
                "                        ",
                "                        ",
                "                        ",
                "                        ",
            ],
        );
    }

    #[test]
    fn scrolling_behaves_as_bottom_if_lines_overflow() {
        assert_rendered(
            "word word2 word3 word4",
            size_for(&FONT_6X9, 5, 2),
            &[
                "..............................",
                "......................#..####.",
                "......................#....#..",
                "#...#...##...#.#....###...##..",
                "#.#.#..#..#..##.#..#..#.....#.",
                "#.#.#..#..#..#.....#..#.....#.",
                ".#.#....##...#......###..###..",
                "..............................",
                "..............................",
                "..............................",
                "......................#....#..",
                "......................#...##..",
                "#...#...##...#.#....###..#.#..",
                "#.#.#..#..#..##.#..#..#.#..#..",
                "#.#.#..#..#..#.....#..#.#####.",
                ".#.#....##...#......###....#..",
                "..............................",
                "..............................",
            ],
        );
    }
}