use crate::{
alignment::HorizontalTextAlignment,
parser::{Parser, Token, SPEC_CHAR_NBSP},
rendering::{cursor::Cursor, space_config::*},
style::TabSize,
utils::font_ext::FontExt,
};
use core::{marker::PhantomData, str::Chars};
use embedded_graphics::prelude::*;
#[cfg(feature = "ansi")]
use super::ansi::{try_parse_sgr, Sgr};
#[cfg(feature = "ansi")]
use ansi_parser::AnsiSequence;
#[cfg(feature = "ansi")]
use as_slice::AsSlice;
#[derive(Debug)]
enum State<'a> {
ProcessToken(Token<'a>),
Word(Chars<'a>),
Done(Option<Token<'a>>),
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum RenderElement {
Space(u32, u32),
PrintedCharacter(char),
#[cfg(feature = "ansi")]
Sgr(Sgr),
}
#[derive(Debug)]
pub struct LineElementIterator<'a, F, SP, A> {
pub cursor: Cursor<F>,
pub parser: Parser<'a>,
pub(crate) pos: Point,
current_token: State<'a>,
config: SP,
first_word: bool,
alignment: PhantomData<A>,
tab_size: TabSize<F>,
}
impl<'a, F, SP, A> LineElementIterator<'a, F, SP, A>
where
F: Font + Copy,
SP: SpaceConfig<Font = F>,
A: HorizontalTextAlignment,
{
#[inline]
#[must_use]
pub fn new(
mut parser: Parser<'a>,
cursor: Cursor<F>,
config: SP,
carried_token: Option<Token<'a>>,
tab_size: TabSize<F>,
) -> Self {
let current_token = carried_token
.filter(|t| ![Token::NewLine, Token::CarriageReturn, Token::Break(None)].contains(t))
.or_else(|| parser.next())
.map_or(State::Done(None), State::ProcessToken);
Self {
parser,
current_token,
config,
cursor,
first_word: true,
alignment: PhantomData,
pos: Point::zero(),
tab_size,
}
}
fn next_token(&mut self) {
match self.parser.next() {
None => self.finish_end_of_string(),
Some(t) => self.current_token = State::ProcessToken(t),
}
}
#[must_use]
#[inline]
pub fn remaining_token(&self) -> Option<Token<'a>> {
match self.current_token {
State::Done(ref t) => t.clone(),
_ => None,
}
}
fn finish_end_of_string(&mut self) {
self.current_token = State::Done(None);
}
fn finish_wrapped(&mut self) {
self.finish(Token::Break(None));
}
fn finish(&mut self, t: Token<'a>) {
self.current_token = match t {
Token::NewLine => {
self.cursor.new_line();
self.cursor.carriage_return();
State::Done(Some(Token::NewLine))
}
Token::CarriageReturn => {
self.cursor.carriage_return();
State::Done(Some(Token::CarriageReturn))
}
c => {
self.cursor.new_line();
self.cursor.carriage_return();
State::Done(Some(c))
}
};
}
fn next_word_width(&mut self) -> Option<u32> {
let mut width = None;
let mut lookahead = self.parser.clone();
'lookahead: loop {
match lookahead.next() {
Some(Token::Word(w)) => {
let w = F::str_width_nocr(w);
width = width.map_or(Some(w), |acc| Some(acc + w));
}
Some(Token::Break(Some(c))) => {
let w = F::total_char_width(c);
width = width.map_or(Some(w), |acc| Some(acc + w));
break 'lookahead;
}
#[cfg(feature = "ansi")]
Some(Token::EscapeSequence(_)) => {}
_ => break 'lookahead,
}
}
width
}
fn count_widest_space_seq(&self, n: u32) -> u32 {
let mut spaces_to_render = 0;
let available = self.cursor.space();
while spaces_to_render < n && self.config.peek_next_width(spaces_to_render + 1) < available
{
spaces_to_render += 1;
}
spaces_to_render
}
}
impl<F, SP, A> Iterator for LineElementIterator<'_, F, SP, A>
where
F: Font + Copy,
SP: SpaceConfig<Font = F>,
A: HorizontalTextAlignment,
{
type Item = RenderElement;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
loop {
self.pos = self.cursor.position;
match self.current_token {
State::ProcessToken(ref token) => {
let token = token.clone();
match token {
Token::Whitespace(n) => {
let mut would_wrap = false;
let render_whitespace = if self.first_word {
if A::STARTING_SPACES {
self.first_word = false;
}
A::STARTING_SPACES
} else if let Some(word_width) = self.next_word_width() {
let space_width = self.config.peek_next_width(n);
let fits = self.cursor.fits_in_line(space_width + word_width);
would_wrap = !fits;
A::ENDING_SPACES || fits
} else {
A::ENDING_SPACES
};
if render_whitespace {
let n = if would_wrap { n.saturating_sub(1) } else { n };
let spaces_to_render = self.count_widest_space_seq(n);
if spaces_to_render > 0 {
let space_width = self.config.consume(spaces_to_render);
self.cursor.advance_unchecked(space_width);
let carried = n - spaces_to_render;
if carried == 0 {
self.next_token();
} else {
self.finish(Token::Whitespace(carried));
}
break Some(RenderElement::Space(
space_width,
spaces_to_render,
));
} else {
if n > 1 {
self.finish(Token::Whitespace(n - 1));
} else {
self.finish_wrapped();
}
}
} else if would_wrap {
self.finish_wrapped();
} else {
self.next_token();
}
}
Token::Break(c) => {
let fits = if let Some(word_width) = self.next_word_width() {
self.cursor.fits_in_line(word_width)
} else {
true
};
if fits {
self.next_token();
} else if let Some(c) = c {
if self.cursor.advance(F::total_char_width(c)) {
self.finish_wrapped();
break Some(RenderElement::PrintedCharacter(c));
} else {
self.finish(Token::ExtraCharacter(c));
}
} else {
self.finish_wrapped();
}
}
Token::ExtraCharacter(c) => {
if self.cursor.advance(F::total_char_width(c)) {
self.next_token();
break Some(RenderElement::PrintedCharacter(c));
}
self.finish_end_of_string();
}
Token::Word(w) => {
if self.first_word {
self.first_word = false;
self.current_token = State::Word(w.chars());
} else if self.cursor.fits_in_line(F::str_width_nocr(w)) {
self.current_token = State::Word(w.chars());
} else {
self.finish(token);
}
}
Token::Tab => {
let sp_width = self.tab_size.next_width(self.cursor.x_in_line());
let tab_width = if self.cursor.advance(sp_width) {
self.next_token();
sp_width
} else {
let available_space = self.cursor.space();
self.finish_wrapped();
available_space
};
break Some(RenderElement::Space(tab_width, 0));
}
#[cfg(feature = "ansi")]
Token::EscapeSequence(seq) => {
self.next_token();
match seq {
AnsiSequence::SetGraphicsMode(vec) => {
if let Some(sgr) = try_parse_sgr(vec.as_slice()) {
break Some(RenderElement::Sgr(sgr));
}
}
AnsiSequence::CursorForward(n) => {
let delta = n * F::total_char_width(' ');
let width = if self.cursor.advance(delta) {
delta
} else {
let space = self.cursor.space();
self.cursor.advance_unchecked(space);
space
};
break Some(RenderElement::Space(width, 0));
}
AnsiSequence::CursorBackward(n) => {
let delta = n * F::total_char_width(' ');
if !self.cursor.rewind(delta) {
self.cursor.carriage_return();
}
}
_ => {
}
}
}
Token::NewLine | Token::CarriageReturn => {
self.finish(token);
}
}
}
State::Word(ref mut chars) => {
let word = chars.as_str();
match chars.next() {
Some(c) => {
let mut ret_val = None;
let pos = self.cursor.position;
if c == SPEC_CHAR_NBSP {
let sp_width = self.config.peek_next_width(1);
if self.cursor.advance(sp_width) {
ret_val = Some(RenderElement::Space(sp_width, 1));
self.config.consume(1);
}
} else if self.cursor.advance(F::total_char_width(c)) {
ret_val = Some(RenderElement::PrintedCharacter(c));
}
if ret_val.is_some() {
self.pos = pos;
self.current_token = State::Word(chars.clone());
break ret_val;
} else if self.cursor.x_in_line() > 0 {
self.finish(Token::Word(word));
} else {
self.finish_end_of_string();
}
}
None => self.next_token(),
}
}
State::Done(_) => break None,
}
}
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::alignment::LeftAligned;
use embedded_graphics::fonts::Font6x8;
use embedded_graphics::primitives::Rectangle;
pub fn collect_mut<I: Iterator<Item = T>, T>(iter: &mut I) -> Vec<T> {
let mut v = Vec::new();
v.extend(iter);
v
}
#[test]
fn soft_hyphen_no_wrapping() {
let parser = Parser::parse("sam\u{00AD}ple");
let config: UniformSpaceConfig<Font6x8> = UniformSpaceConfig::default();
let cursor = Cursor::new(Rectangle::new(Point::zero(), Point::new(6 * 6 - 1, 8)), 0);
let iter: LineElementIterator<'_, _, _, LeftAligned> =
LineElementIterator::new(parser, cursor, config, None, TabSize::default());
assert_eq!(
iter.collect::<Vec<RenderElement>>(),
vec![
RenderElement::PrintedCharacter('s'),
RenderElement::PrintedCharacter('a'),
RenderElement::PrintedCharacter('m'),
RenderElement::PrintedCharacter('p'),
RenderElement::PrintedCharacter('l'),
RenderElement::PrintedCharacter('e'),
]
);
}
#[test]
fn soft_hyphen() {
let parser = Parser::parse("sam\u{00AD}ple");
let config: UniformSpaceConfig<Font6x8> = UniformSpaceConfig::default();
let cursor = Cursor::new(Rectangle::new(Point::zero(), Point::new(6 * 6 - 2, 16)), 0);
let mut line1: LineElementIterator<'_, _, _, LeftAligned> =
LineElementIterator::new(parser, cursor, config, None, TabSize::default());
assert_eq!(
collect_mut(&mut line1),
vec![
RenderElement::PrintedCharacter('s'),
RenderElement::PrintedCharacter('a'),
RenderElement::PrintedCharacter('m'),
RenderElement::PrintedCharacter('-'),
]
);
assert_eq!(line1.cursor.position, Point::new(0, 8));
let carried = line1.remaining_token();
let line2: LineElementIterator<'_, _, _, LeftAligned> = LineElementIterator::new(
line1.parser,
line1.cursor,
config,
carried,
TabSize::default(),
);
assert_eq!(
line2.collect::<Vec<RenderElement>>(),
vec![
RenderElement::PrintedCharacter('p'),
RenderElement::PrintedCharacter('l'),
RenderElement::PrintedCharacter('e'),
]
);
}
#[test]
fn soft_hyphen_issue_42() {
let parser =
Parser::parse("super\u{AD}cali\u{AD}fragi\u{AD}listic\u{AD}espeali\u{AD}docious");
let config: UniformSpaceConfig<Font6x8> = UniformSpaceConfig::default();
let cursor = Cursor::new(Rectangle::new(Point::zero(), Point::new(5 * 6 - 1, 16)), 0);
let mut line1: LineElementIterator<'_, _, _, LeftAligned> =
LineElementIterator::new(parser, cursor, config, None, TabSize::default());
assert_eq!(
collect_mut(&mut line1),
vec![
RenderElement::PrintedCharacter('s'),
RenderElement::PrintedCharacter('u'),
RenderElement::PrintedCharacter('p'),
RenderElement::PrintedCharacter('e'),
RenderElement::PrintedCharacter('r'),
]
);
assert_eq!(line1.cursor.position, Point::new(0, 8));
let carried = line1.remaining_token();
let line2: LineElementIterator<'_, _, _, LeftAligned> = LineElementIterator::new(
line1.parser,
line1.cursor,
config,
carried,
TabSize::default(),
);
assert_eq!(
line2.collect::<Vec<RenderElement>>(),
vec![
RenderElement::PrintedCharacter('-'),
RenderElement::PrintedCharacter('c'),
RenderElement::PrintedCharacter('a'),
RenderElement::PrintedCharacter('l'),
RenderElement::PrintedCharacter('i'),
]
);
}
#[test]
fn nbsp_is_rendered_as_space() {
let text = "glued\u{a0}words";
let parser = Parser::parse(text);
let config: UniformSpaceConfig<Font6x8> = UniformSpaceConfig::default();
let cursor = Cursor::new(
Rectangle::new(
Point::zero(),
Point::new(text.chars().count() as i32 * 6 - 1, 16),
),
0,
);
let mut line1: LineElementIterator<'_, _, _, LeftAligned> =
LineElementIterator::new(parser, cursor, config, None, TabSize::default());
assert_eq!(
collect_mut(&mut line1),
vec![
RenderElement::PrintedCharacter('g'),
RenderElement::PrintedCharacter('l'),
RenderElement::PrintedCharacter('u'),
RenderElement::PrintedCharacter('e'),
RenderElement::PrintedCharacter('d'),
RenderElement::Space(6, 1),
RenderElement::PrintedCharacter('w'),
RenderElement::PrintedCharacter('o'),
RenderElement::PrintedCharacter('r'),
RenderElement::PrintedCharacter('d'),
RenderElement::PrintedCharacter('s'),
]
);
}
#[test]
fn tabs() {
let text = "a\tword\nand\t\tanother\t";
let parser = Parser::parse(text);
let config: UniformSpaceConfig<Font6x8> = UniformSpaceConfig::default();
let cursor = Cursor::new(Rectangle::new(Point::zero(), Point::new(16 * 6 - 1, 16)), 0);
let mut line1: LineElementIterator<'_, _, _, LeftAligned> =
LineElementIterator::new(parser, cursor, config, None, TabSize::default());
assert_eq!(
collect_mut(&mut line1),
vec![
RenderElement::PrintedCharacter('a'),
RenderElement::Space(6 * 3, 0),
RenderElement::PrintedCharacter('w'),
RenderElement::PrintedCharacter('o'),
RenderElement::PrintedCharacter('r'),
RenderElement::PrintedCharacter('d'),
]
);
let carried = line1.remaining_token();
let mut line2: LineElementIterator<'_, _, _, LeftAligned> = LineElementIterator::new(
line1.parser,
line1.cursor,
config,
carried,
TabSize::default(),
);
assert_eq!(
collect_mut(&mut line2),
vec![
RenderElement::PrintedCharacter('a'),
RenderElement::PrintedCharacter('n'),
RenderElement::PrintedCharacter('d'),
RenderElement::Space(6, 0),
RenderElement::Space(6 * 4, 0),
RenderElement::PrintedCharacter('a'),
RenderElement::PrintedCharacter('n'),
RenderElement::PrintedCharacter('o'),
RenderElement::PrintedCharacter('t'),
RenderElement::PrintedCharacter('h'),
RenderElement::PrintedCharacter('e'),
RenderElement::PrintedCharacter('r'),
RenderElement::Space(6, 0),
]
);
}
}
#[cfg(all(test, feature = "ansi"))]
mod ansi_parser_tests {
use super::{test::collect_mut, *};
use crate::{alignment::LeftAligned, style::color::Rgb};
use embedded_graphics::fonts::Font6x8;
use embedded_graphics::primitives::Rectangle;
#[test]
fn colors() {
let text = "Lorem \x1b[92mIpsum";
let parser = Parser::parse(text);
let config: UniformSpaceConfig<Font6x8> = UniformSpaceConfig::default();
let cursor = Cursor::new(
Rectangle::new(Point::zero(), Point::new(100 * 6 - 1, 16)),
0,
);
let mut line1: LineElementIterator<'_, _, _, LeftAligned> =
LineElementIterator::new(parser, cursor, config, None, TabSize::default());
assert_eq!(
collect_mut(&mut line1),
vec![
RenderElement::PrintedCharacter('L'),
RenderElement::PrintedCharacter('o'),
RenderElement::PrintedCharacter('r'),
RenderElement::PrintedCharacter('e'),
RenderElement::PrintedCharacter('m'),
RenderElement::Space(6, 1),
RenderElement::Sgr(Sgr::ChangeTextColor(Rgb::new(22, 198, 12))),
RenderElement::PrintedCharacter('I'),
RenderElement::PrintedCharacter('p'),
RenderElement::PrintedCharacter('s'),
RenderElement::PrintedCharacter('u'),
RenderElement::PrintedCharacter('m'),
]
);
}
#[test]
fn ansi_code_does_not_break_word() {
let text = "Lorem foo\x1b[92mbarum";
let parser = Parser::parse(text);
let config: UniformSpaceConfig<Font6x8> = UniformSpaceConfig::default();
let cursor = Cursor::new(Rectangle::new(Point::zero(), Point::new(8 * 6 - 1, 16)), 0);
let mut line1: LineElementIterator<'_, _, _, LeftAligned> =
LineElementIterator::new(parser, cursor, config, None, TabSize::default());
assert_eq!(
collect_mut(&mut line1),
vec![
RenderElement::PrintedCharacter('L'),
RenderElement::PrintedCharacter('o'),
RenderElement::PrintedCharacter('r'),
RenderElement::PrintedCharacter('e'),
RenderElement::PrintedCharacter('m'),
]
);
let carried = line1.remaining_token();
let mut line2: LineElementIterator<'_, _, _, LeftAligned> = LineElementIterator::new(
line1.parser,
line1.cursor,
config,
carried,
TabSize::default(),
);
assert_eq!(
collect_mut(&mut line2),
vec![
RenderElement::PrintedCharacter('f'),
RenderElement::PrintedCharacter('o'),
RenderElement::PrintedCharacter('o'),
RenderElement::Sgr(Sgr::ChangeTextColor(Rgb::new(22, 198, 12))),
RenderElement::PrintedCharacter('b'),
RenderElement::PrintedCharacter('a'),
RenderElement::PrintedCharacter('r'),
RenderElement::PrintedCharacter('u'),
RenderElement::PrintedCharacter('m'),
]
);
}
}