mod builder;
mod height_mode;
mod vertical_overdraw;
use core::convert::Infallible;
use crate::{
alignment::{HorizontalAlignment, VerticalAlignment},
parser::{Parser, Token},
rendering::{
cursor::LineCursor,
line_iter::{ElementHandler, LineElementParser},
space_config::SpaceConfig,
},
utils::str_width,
};
use az::SaturatingAs;
use embedded_graphics::text::{renderer::TextRenderer, LineHeight};
pub use self::{
builder::TextBoxStyleBuilder, height_mode::HeightMode, vertical_overdraw::VerticalOverdraw,
};
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub enum TabSize {
Pixels(u16),
Spaces(u16),
}
impl Default for TabSize {
#[inline]
fn default() -> Self {
Self::Spaces(4)
}
}
impl TabSize {
#[inline]
pub(crate) fn into_pixels(self, renderer: &impl TextRenderer) -> u32 {
match self {
TabSize::Pixels(px) => px as u32,
TabSize::Spaces(n) => n as u32 * str_width(renderer, " "),
}
}
}
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
#[non_exhaustive]
#[must_use]
pub struct TextBoxStyle {
pub alignment: HorizontalAlignment,
pub vertical_alignment: VerticalAlignment,
pub height_mode: HeightMode,
pub line_height: LineHeight,
pub paragraph_spacing: u32,
pub tab_size: TabSize,
}
impl TextBoxStyle {
#[inline]
pub const fn with_alignment(alignment: HorizontalAlignment) -> TextBoxStyle {
TextBoxStyleBuilder::new().alignment(alignment).build()
}
#[inline]
pub const fn with_vertical_alignment(alignment: VerticalAlignment) -> TextBoxStyle {
TextBoxStyleBuilder::new()
.vertical_alignment(alignment)
.build()
}
}
impl Default for TextBoxStyle {
#[inline]
fn default() -> Self {
TextBoxStyleBuilder::new().build()
}
}
#[derive(Debug)]
#[must_use]
pub struct LineMeasurement {
pub max_line_width: u32,
pub width: u32,
pub last_line: bool,
}
struct MeasureLineElementHandler<'a, S> {
style: &'a S,
right: u32,
max_line_width: u32,
pos: u32,
}
impl<'a, S: TextRenderer> ElementHandler for MeasureLineElementHandler<'a, S> {
type Error = Infallible;
fn measure(&self, st: &str) -> u32 {
str_width(self.style, st)
}
fn whitespace(&mut self, width: u32) -> Result<(), Self::Error> {
self.pos += width;
Ok(())
}
fn printed_characters(&mut self, _: &str, width: u32) -> Result<(), Self::Error> {
self.right = self.right.max(self.pos + width);
self.pos += width;
Ok(())
}
fn move_cursor(&mut self, by: i32) -> Result<(), Self::Error> {
self.pos = (self.pos.saturating_as::<i32>() + by)
.max(0)
.min(self.max_line_width.saturating_as()) as u32;
Ok(())
}
}
impl TextBoxStyle {
#[inline]
pub(crate) fn measure_line<'a, S>(
&self,
character_style: &S,
parser: &mut Parser<'a>,
carried_token: &mut Option<Token<'a>>,
max_line_width: u32,
) -> LineMeasurement
where
S: TextRenderer,
{
let cursor = LineCursor::new(max_line_width, self.tab_size.into_pixels(character_style));
let mut iter = LineElementParser::new(
parser,
cursor,
SpaceConfig::new(str_width(character_style, " "), None),
carried_token.clone(),
self.alignment,
);
let mut handler = MeasureLineElementHandler {
style: character_style,
right: 0,
pos: 0,
max_line_width,
};
*carried_token = iter.process(&mut handler).unwrap();
LineMeasurement {
max_line_width,
width: handler.right,
last_line: carried_token.is_none() || *carried_token == Some(Token::NewLine),
}
}
#[inline]
#[must_use]
pub fn measure_text_height<S>(&self, character_style: &S, text: &str, max_width: u32) -> u32
where
S: TextRenderer,
{
let mut n_lines = 0_u32;
let mut parser = Parser::parse(text);
let mut carry = None;
let mut cr_width = None;
let mut empty_lines = 0;
let mut closed_paragraphs: u32 = 0;
let line_height = self.line_height.to_absolute(character_style.line_height());
let last_line_height = character_style.line_height();
let mut paragraph_ended = false;
loop {
let lm = self.measure_line(character_style, &mut parser, &mut carry, max_width);
if paragraph_ended {
closed_paragraphs += 1;
}
paragraph_ended = carry == Some(Token::NewLine);
if carry == Some(Token::CarriageReturn) {
cr_width = cr_width.map_or(Some(lm.width), |width: u32| Some(width.max(lm.width)));
} else {
let line_width = match cr_width.take() {
Some(width) => width.max(lm.width),
None => lm.width,
};
if line_width > 0 || carry == Some(Token::NewLine) {
n_lines += empty_lines + 1;
empty_lines = 0;
} else {
empty_lines += 1;
}
}
if carry.is_none() {
return n_lines.saturating_sub(1) * line_height
+ (n_lines > 0) as u32 * last_line_height
+ closed_paragraphs * self.paragraph_spacing;
}
}
}
}
#[cfg(test)]
mod test {
use crate::{alignment::*, parser::Parser, style::builder::TextBoxStyleBuilder};
use embedded_graphics::{
mono_font::{ascii::FONT_6X9, MonoTextStyleBuilder},
pixelcolor::BinaryColor,
text::{renderer::TextRenderer, LineHeight},
};
#[test]
fn no_infinite_loop() {
let character_style = MonoTextStyleBuilder::new()
.font(&FONT_6X9)
.text_color(BinaryColor::On)
.build();
let _ = TextBoxStyleBuilder::new()
.build()
.measure_text_height(&character_style, "a", 5);
}
#[test]
fn test_measure_height() {
let data = [
("", 0, 0),
(" ", 0, 0),
(" ", 5, 0),
(" ", 6, 0),
("\r", 6, 0),
("\n", 6, 1),
("\n ", 6, 1),
("word", 4 * 6, 1), ("word", 4 * 6 - 1, 2),
("word", 2 * 6, 2), ("word word", 4 * 6, 2), ("word\n", 2 * 6, 2),
("word\nnext", 50, 2),
("word\n\nnext", 50, 3),
("word\n \nnext", 50, 3),
("verylongword", 50, 2),
("some verylongword", 50, 3),
("1 23456 12345 61234 561", 36, 5),
(" Word ", 36, 2),
("\rcr", 36, 1),
("cr\r", 36, 1),
("cr\rcr", 36, 1),
("Longer\r", 36, 1),
("Longer\rnowrap", 36, 1),
];
let character_style = MonoTextStyleBuilder::new()
.font(&FONT_6X9)
.text_color(BinaryColor::On)
.build();
let style = TextBoxStyleBuilder::new().build();
for (i, (text, width, expected_n_lines)) in data.iter().enumerate() {
let height = style.measure_text_height(&character_style, text, *width);
let expected_height = *expected_n_lines * character_style.line_height();
assert_eq!(
height,
expected_height,
r#"#{}: Height of "{}" is {} but is expected to be {}"#,
i,
text.replace('\r', "\\r").replace('\n', "\\n"),
height,
expected_height
);
}
}
#[test]
fn test_measure_height_ignored_spaces() {
let data = [
("", 0, 0),
(" ", 0, 0),
(" ", 6, 0),
("\n ", 6, 1),
("word\n", 2 * 6, 2),
("word\n \nnext", 50, 3),
(" Word ", 36, 1),
];
let character_style = MonoTextStyleBuilder::new()
.font(&FONT_6X9)
.text_color(BinaryColor::On)
.build();
let style = TextBoxStyleBuilder::new()
.alignment(HorizontalAlignment::Center)
.build();
for (i, (text, width, expected_n_lines)) in data.iter().enumerate() {
let height = style.measure_text_height(&character_style, text, *width);
let expected_height = *expected_n_lines * character_style.line_height();
assert_eq!(
height, expected_height,
r#"#{}: Height of "{}" is {} but is expected to be {}"#,
i, text, height, expected_height
);
}
}
#[test]
fn test_measure_line() {
let character_style = MonoTextStyleBuilder::new()
.font(&FONT_6X9)
.text_color(BinaryColor::On)
.build();
let style = TextBoxStyleBuilder::new()
.alignment(HorizontalAlignment::Center)
.build();
let mut text = Parser::parse("123 45 67");
let lm = style.measure_line(
&character_style,
&mut text,
&mut None,
6 * FONT_6X9.character_size.width,
);
assert_eq!(lm.width, 6 * FONT_6X9.character_size.width);
}
#[test]
#[cfg(feature = "ansi")]
fn test_measure_line_cursor_back() {
let character_style = MonoTextStyleBuilder::new()
.font(&FONT_6X9)
.text_color(BinaryColor::On)
.build();
let style = TextBoxStyleBuilder::new()
.alignment(HorizontalAlignment::Center)
.build();
let mut text = Parser::parse("123\x1b[2D");
let lm = style.measure_line(
&character_style,
&mut text,
&mut None,
5 * FONT_6X9.character_size.width,
);
assert_eq!(lm.width, 3 * FONT_6X9.character_size.width);
let mut text = Parser::parse("123\x1b[2D456");
let lm = style.measure_line(
&character_style,
&mut text,
&mut None,
5 * FONT_6X9.character_size.width,
);
assert_eq!(lm.width, 4 * FONT_6X9.character_size.width);
}
#[test]
fn test_measure_line_counts_nbsp() {
let character_style = MonoTextStyleBuilder::new()
.font(&FONT_6X9)
.text_color(BinaryColor::On)
.build();
let style = TextBoxStyleBuilder::new()
.alignment(HorizontalAlignment::Center)
.build();
let mut text = Parser::parse("123\u{A0}45");
let lm = style.measure_line(
&character_style,
&mut text,
&mut None,
5 * FONT_6X9.character_size.width,
);
assert_eq!(lm.width, 5 * FONT_6X9.character_size.width);
}
#[test]
fn test_measure_height_nbsp() {
let character_style = MonoTextStyleBuilder::new()
.font(&FONT_6X9)
.text_color(BinaryColor::On)
.build();
let style = TextBoxStyleBuilder::new()
.alignment(HorizontalAlignment::Center)
.build();
let text = "123\u{A0}45 123";
let height =
style.measure_text_height(&character_style, text, 5 * FONT_6X9.character_size.width);
assert_eq!(height, 2 * character_style.line_height());
let style = TextBoxStyleBuilder::new()
.alignment(HorizontalAlignment::Left)
.build();
let text = "embedded-text also\u{A0}supports non-breaking spaces.";
let height = style.measure_text_height(&character_style, text, 79);
assert_eq!(height, 4 * character_style.line_height());
}
#[test]
fn height_with_line_spacing() {
let character_style = MonoTextStyleBuilder::new()
.font(&FONT_6X9)
.text_color(BinaryColor::On)
.build();
let style = TextBoxStyleBuilder::new()
.line_height(LineHeight::Pixels(11))
.build();
let height = style.measure_text_height(
&character_style,
"Lorem Ipsum is simply dummy text of the printing and typesetting industry.",
72,
);
assert_eq!(height, 6 * 11 + 9);
}
#[test]
fn soft_hyphenated_line_width_includes_hyphen_width() {
let character_style = MonoTextStyleBuilder::new()
.font(&FONT_6X9)
.text_color(BinaryColor::On)
.build();
let style = TextBoxStyleBuilder::new()
.line_height(LineHeight::Pixels(11))
.build();
let lm = style.measure_line(
&character_style,
&mut Parser::parse("soft\u{AD}hyphen"),
&mut None,
50,
);
assert_eq!(lm.width, 30);
}
}