use crate::{
parser::{Parser, Token},
rendering::{
character::StyledCharacterIterator, cursor::Cursor, whitespace::EmptySpaceIterator,
},
utils::font_ext::FontExt,
};
use core::str::Chars;
use embedded_graphics::{prelude::*, style::TextStyle};
#[derive(Debug)]
pub enum LineState<'a, C, F>
where
C: PixelColor,
F: Font + Copy,
{
FetchNext,
ProcessToken(Token<'a>),
Word(Chars<'a>, StyledCharacterIterator<C, F>),
Whitespace(u32, EmptySpaceIterator<C, F>),
Done(Option<Token<'a>>),
}
pub trait SpaceConfig: Copy {
fn starting_spaces(&self) -> bool;
fn ending_spaces(&self) -> bool;
fn peek_next_width(&self, n: u32) -> u32;
fn next_space_width(&mut self) -> u32;
}
#[derive(Copy, Clone, Debug)]
pub struct UniformSpaceConfig {
pub space_width: u32,
pub starting_spaces: bool,
pub ending_spaces: bool,
}
impl SpaceConfig for UniformSpaceConfig {
#[inline]
fn starting_spaces(&self) -> bool {
self.starting_spaces
}
#[inline]
fn ending_spaces(&self) -> bool {
self.ending_spaces
}
#[inline]
fn peek_next_width(&self, n: u32) -> u32 {
n * self.space_width
}
#[inline]
fn next_space_width(&mut self) -> u32 {
self.space_width
}
}
#[derive(Debug)]
pub struct StyledLineIterator<'a, C, F, SP: SpaceConfig>
where
C: PixelColor,
F: Font + Copy,
{
pub cursor: Cursor<F>,
pub parser: Parser<'a>,
current_token: LineState<'a, C, F>,
config: SP,
style: TextStyle<C, F>,
first_word: bool,
}
impl<'a, C, F, SP> StyledLineIterator<'a, C, F, SP>
where
C: PixelColor,
F: Font + Copy,
SP: SpaceConfig,
{
#[inline]
#[must_use]
pub fn new(
parser: Parser<'a>,
cursor: Cursor<F>,
config: SP,
style: TextStyle<C, F>,
carried_token: Option<Token<'a>>,
) -> Self {
Self {
parser,
current_token: carried_token
.map(LineState::ProcessToken)
.unwrap_or(LineState::FetchNext),
config,
cursor,
style,
first_word: true,
}
}
#[must_use]
#[inline]
pub fn remaining_token(&self) -> Option<Token<'a>> {
match self.current_token {
LineState::Done(ref t) => t.clone(),
_ => None,
}
}
fn fits_in_line(&self, width: u32) -> bool {
self.cursor.fits_in_line(width)
}
}
impl<C, F, SP> Iterator for StyledLineIterator<'_, C, F, SP>
where
C: PixelColor,
F: Font + Copy,
SP: SpaceConfig,
{
type Item = Pixel<C>;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
loop {
match self.current_token {
LineState::FetchNext => {
self.current_token = if let Some(token) = self.parser.next() {
LineState::ProcessToken(token)
} else {
LineState::Done(None)
}
}
LineState::ProcessToken(ref token) => {
match token.clone() {
Token::Whitespace(n) => {
let render_whitespace = if self.first_word {
self.config.starting_spaces()
} else if self.config.ending_spaces() {
true
} else if let Some(Token::Word(w)) = self.parser.peek() {
let space_width = self.config.peek_next_width(n);
let word_width = F::str_width(w);
self.fits_in_line(space_width + word_width)
} else {
false
};
if render_whitespace {
let mut space_width = 0;
let mut spaces = n;
while spaces > 0
&& self
.fits_in_line(space_width + self.config.peek_next_width(1))
{
spaces -= 1;
space_width += self.config.next_space_width();
}
self.current_token = if space_width > 0 {
let pos = self.cursor.position;
self.cursor.advance(space_width);
LineState::Whitespace(
spaces,
EmptySpaceIterator::new(space_width, pos, self.style),
)
} else if spaces > 1 {
LineState::Done(Some(Token::Whitespace(
spaces.saturating_sub(1),
)))
} else {
LineState::Done(None)
}
} else {
self.current_token = LineState::FetchNext;
}
}
Token::Word(w) => {
if self.first_word {
self.first_word = false;
} else if !self.fits_in_line(F::str_width(w)) {
self.current_token = LineState::Done(Some(Token::Word(w)));
break None;
}
let mut chars = w.chars();
let c = chars.next().unwrap();
let pos = self.cursor.position;
self.cursor.advance(F::total_char_width(c));
self.current_token = LineState::Word(
chars,
StyledCharacterIterator::new(c, pos, self.style),
);
}
Token::NewLine => {
self.current_token = LineState::Done(None);
}
}
}
LineState::Whitespace(ref n, ref mut iter) => {
if let pixel @ Some(_) = iter.next() {
break pixel;
}
self.current_token = if *n == 0 {
LineState::FetchNext
} else {
LineState::Done(Some(Token::Whitespace(*n)))
}
}
LineState::Word(ref chars, ref mut iter) => {
if let pixel @ Some(_) = iter.next() {
break pixel;
}
let mut lookahead = chars.clone();
self.current_token = if let Some(c) = lookahead.next() {
let char_width = F::total_char_width(c);
if self.fits_in_line(char_width) {
let pos = self.cursor.position;
self.cursor.advance(char_width);
LineState::Word(
lookahead,
StyledCharacterIterator::new(c, pos, self.style),
)
} else {
LineState::Done(Some(Token::Word(chars.as_str())))
}
} else {
LineState::FetchNext
}
}
LineState::Done(_) => {
break None;
}
}
}
}
}
#[cfg(test)]
mod test {
use crate::parser::{Parser, Token};
use crate::rendering::{
cursor::Cursor,
line::{StyledLineIterator, UniformSpaceConfig},
};
use embedded_graphics::{
fonts::Font6x8, mock_display::MockDisplay, pixelcolor::BinaryColor, prelude::*,
primitives::Rectangle, style::TextStyleBuilder,
};
#[test]
fn simple_render() {
let parser = Parser::parse(" Some sample text");
let config = UniformSpaceConfig {
starting_spaces: true,
ending_spaces: true,
space_width: 6,
};
let style = TextStyleBuilder::new(Font6x8)
.text_color(BinaryColor::On)
.background_color(BinaryColor::Off)
.build();
let cursor = Cursor::new(Rectangle::new(Point::zero(), Point::new(6 * 7 - 1, 8)));
let mut iter = StyledLineIterator::new(parser, cursor, config, style, None);
let mut display = MockDisplay::new();
iter.draw(&mut display).unwrap();
assert_eq!(
display,
MockDisplay::from_pattern(&[
".......###..........................",
"......#...#.........................",
"......#......###..##.#...###........",
".......###..#...#.#.#.#.#...#.......",
"..........#.#...#.#...#.#####.......",
"......#...#.#...#.#...#.#...........",
".......###...###..#...#..###........",
"....................................",
])
);
assert_eq!(Some(Token::Word("sample")), iter.remaining_token());
}
#[test]
fn simple_render_first_word_not_wrapped() {
let parser = Parser::parse(" Some sample text");
let config = UniformSpaceConfig {
starting_spaces: true,
ending_spaces: true,
space_width: 6,
};
let style = TextStyleBuilder::new(Font6x8)
.text_color(BinaryColor::On)
.background_color(BinaryColor::Off)
.build();
let cursor = Cursor::new(Rectangle::new(Point::zero(), Point::new(6 * 3 - 1, 8)));
let mut iter = StyledLineIterator::new(parser, cursor, config, style, None);
let mut display = MockDisplay::new();
iter.draw(&mut display).unwrap();
assert_eq!(
display,
MockDisplay::from_pattern(&[
".......###........",
"......#...#.......",
"......#......###..",
".......###..#...#.",
"..........#.#...#.",
"......#...#.#...#.",
".......###...###..",
"..................",
])
);
assert_eq!(Some(Token::Word("me")), iter.remaining_token());
}
#[test]
fn newline_stops_render() {
let parser = Parser::parse("Some \nsample text");
let config = UniformSpaceConfig {
starting_spaces: true,
ending_spaces: true,
space_width: 6,
};
let style = TextStyleBuilder::new(Font6x8)
.text_color(BinaryColor::On)
.background_color(BinaryColor::Off)
.build();
let cursor = Cursor::new(Rectangle::new(Point::zero(), Point::new(6 * 7 - 1, 8)));
let mut iter = StyledLineIterator::new(parser, cursor, config, style, None);
let mut display = MockDisplay::new();
iter.draw(&mut display).unwrap();
assert_eq!(
display,
MockDisplay::from_pattern(&[
".###..........................",
"#...#.........................",
"#......###..##.#...###........",
".###..#...#.#.#.#.#...#.......",
"....#.#...#.#...#.#####.......",
"#...#.#...#.#...#.#...........",
".###...###..#...#..###........",
"..............................",
])
);
}
#[test]
fn first_spaces_not_rendered() {
let parser = Parser::parse(" Some sample text");
let config = UniformSpaceConfig {
starting_spaces: false,
ending_spaces: true,
space_width: 6,
};
let style = TextStyleBuilder::new(Font6x8)
.text_color(BinaryColor::On)
.background_color(BinaryColor::Off)
.build();
let cursor = Cursor::new(Rectangle::new(Point::zero(), Point::new(6 * 3 - 1, 8)));
let mut iter = StyledLineIterator::new(parser, cursor, config, style, None);
let mut display = MockDisplay::new();
iter.draw(&mut display).unwrap();
assert_eq!(
display,
MockDisplay::from_pattern(&[
".###..............",
"#...#.............",
"#......###..##.#..",
".###..#...#.#.#.#.",
"....#.#...#.#...#.",
"#...#.#...#.#...#.",
".###...###..#...#.",
"..................",
])
);
}
#[test]
fn last_spaces() {
let style = TextStyleBuilder::new(Font6x8)
.text_color(BinaryColor::On)
.background_color(BinaryColor::Off)
.build();
let parser = Parser::parse("Some sample text");
let config = UniformSpaceConfig {
starting_spaces: true,
ending_spaces: false,
space_width: 6,
};
let cursor = Cursor::new(Rectangle::new(Point::zero(), Point::new(6 * 7 - 1, 8)));
let mut iter = StyledLineIterator::new(parser, cursor, config, style, None);
let mut display = MockDisplay::new();
iter.draw(&mut display).unwrap();
assert_eq!(
display,
MockDisplay::from_pattern(&[
".###....................",
"#...#...................",
"#......###..##.#...###..",
".###..#...#.#.#.#.#...#.",
"....#.#...#.#...#.#####.",
"#...#.#...#.#...#.#.....",
".###...###..#...#..###..",
"........................",
])
);
let parser = Parser::parse("Some sample text");
let config = UniformSpaceConfig {
starting_spaces: true,
ending_spaces: true,
space_width: 6,
};
let cursor = Cursor::new(Rectangle::new(Point::zero(), Point::new(6 * 7 - 1, 8)));
let mut iter = StyledLineIterator::new(parser, cursor, config, style, None);
let mut display = MockDisplay::new();
iter.draw(&mut display).unwrap();
assert_eq!(
display,
MockDisplay::from_pattern(&[
".###................................",
"#...#...............................",
"#......###..##.#...###..............",
".###..#...#.#.#.#.#...#.............",
"....#.#...#.#...#.#####.............",
"#...#.#...#.#...#.#.................",
".###...###..#...#..###..............",
"....................................",
])
);
}
#[test]
fn carried_over_spaces() {
let style = TextStyleBuilder::new(Font6x8)
.text_color(BinaryColor::On)
.background_color(BinaryColor::Off)
.build();
let parser = Parser::parse("Some sample text");
let config = UniformSpaceConfig {
starting_spaces: true,
ending_spaces: true,
space_width: 6,
};
let cursor = Cursor::new(Rectangle::new(Point::zero(), Point::new(6 * 5 - 1, 8)));
let mut iter = StyledLineIterator::new(parser, cursor, config, style, None);
let mut display = MockDisplay::new();
iter.draw(&mut display).unwrap();
assert_eq!(Some(Token::Whitespace(1)), iter.remaining_token());
let mut iter = StyledLineIterator::new(
iter.parser.clone(),
cursor,
config,
style,
iter.remaining_token(),
);
let mut display = MockDisplay::new();
iter.draw(&mut display).unwrap();
assert_eq!(
display,
MockDisplay::from_pattern(&[
"..............................",
"..............................",
".......####..###..##.#..####..",
"......#.........#.#.#.#.#...#.",
".......###...####.#...#.#...#.",
"..........#.#...#.#...#.####..",
"......####...####.#...#.#.....",
"........................#.....",
])
);
}
}