mod builder;
mod height_mode;
mod vertical_overdraw;
use core::convert::Infallible;
use crate::{
alignment::{HorizontalAlignment, VerticalAlignment},
parser::{Parser, SPEC_CHAR_NBSP},
plugin::{NoPlugin, Plugin, PluginWrapper, ProcessingState},
rendering::{
cursor::LineCursor,
line_iter::{ElementHandler, LineElementParser, LineEndType},
space_config::SpaceConfig,
},
utils::str_width,
};
use az::SaturatingAs;
use embedded_graphics::{
pixelcolor::Rgb888,
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(crate) struct LineMeasurement {
pub max_line_width: u32,
pub width: u32,
pub last_line: bool,
pub line_end_type: LineEndType,
pub space_count: u32,
}
struct MeasureLineElementHandler<'a, S> {
style: &'a S,
right: u32,
max_line_width: u32,
pos: u32,
space_count: u32,
partial_space_count: u32,
}
impl<'a, S: TextRenderer> ElementHandler for MeasureLineElementHandler<'a, S> {
type Error = Infallible;
type Color = S::Color;
fn measure(&self, st: &str) -> u32 {
str_width(self.style, st)
}
fn whitespace(&mut self, st: &str, _count: u32, width: u32) -> Result<(), Self::Error> {
self.pos += width;
self.partial_space_count += st
.chars()
.filter(|c| [' ', SPEC_CHAR_NBSP].contains(c))
.count()
.saturating_as::<u32>();
Ok(())
}
fn printed_characters(&mut self, _: &str, width: u32) -> Result<(), Self::Error> {
self.right = self.right.max(self.pos + width);
self.pos += width;
self.space_count = self.partial_space_count;
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, M>(
&self,
plugin: &PluginWrapper<'a, M, S::Color>,
character_style: &S,
parser: &mut Parser<'a, S::Color>,
max_line_width: u32,
) -> LineMeasurement
where
S: TextRenderer,
M: Plugin<'a, S::Color>,
S::Color: From<Rgb888>,
{
let cursor = LineCursor::new(max_line_width, self.tab_size.into_pixels(character_style));
let mut iter = LineElementParser::new(
parser,
plugin,
cursor,
SpaceConfig::new(str_width(character_style, " "), None),
self.alignment,
);
let mut handler = MeasureLineElementHandler {
style: character_style,
right: 0,
pos: 0,
max_line_width,
space_count: 0,
partial_space_count: 0,
};
let last_token = iter.process(&mut handler).unwrap();
LineMeasurement {
max_line_width,
width: handler.right,
space_count: handler.space_count,
last_line: matches!(last_token, LineEndType::NewLine | LineEndType::EndOfText),
line_end_type: last_token,
}
}
#[inline]
#[must_use]
pub fn measure_text_height<S>(&self, character_style: &S, text: &str, max_width: u32) -> u32
where
S: TextRenderer,
S::Color: From<Rgb888>,
{
let plugin = PluginWrapper::new(NoPlugin::new());
self.measure_text_height_impl(plugin, character_style, text, max_width)
}
pub(crate) fn measure_text_height_impl<'a, S, M>(
&self,
plugin: PluginWrapper<'a, M, S::Color>,
character_style: &S,
text: &'a str,
max_width: u32,
) -> u32
where
S: TextRenderer,
M: Plugin<'a, S::Color>,
S::Color: From<Rgb888>,
{
let mut parser = Parser::parse(text);
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 height = last_line_height;
let mut paragraph_ended = false;
plugin.set_state(ProcessingState::Measure);
let mut prev_end = LineEndType::EndOfText;
loop {
plugin.new_line();
let lm = self.measure_line(&plugin, character_style, &mut parser, max_width);
if paragraph_ended {
closed_paragraphs += 1;
}
paragraph_ended = lm.last_line;
if prev_end == LineEndType::LineBreak {
if lm.width != 0 {
height += line_height;
}
}
match lm.line_end_type {
LineEndType::CarriageReturn => {}
LineEndType::LineBreak => {}
LineEndType::NewLine => {
height += line_height;
}
LineEndType::EndOfText => {
return height + closed_paragraphs * self.paragraph_spacing;
}
}
prev_end = lm.line_end_type;
}
}
}
#[cfg(test)]
mod test {
use crate::{
alignment::*,
parser::Parser,
plugin::{NoPlugin, PluginWrapper},
style::{builder::TextBoxStyleBuilder, TextBoxStyle},
};
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, 1),
(" ", 6, 1),
("\r", 6, 1),
("\n", 6, 2),
("\n ", 6, 2),
("word", 4 * 6, 1), ("word\n", 4 * 6, 2), ("word", 4 * 6 - 1, 2),
("word", 2 * 6, 2), ("word word", 4 * 6, 2), ("word\n", 2 * 6, 3),
("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 = TextBoxStyle::default();
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, 1),
(" ", 0, 1),
(" ", 6, 1),
("\n ", 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 mut plugin = PluginWrapper::new(NoPlugin::new());
let lm = style.measure_line(
&mut plugin,
&character_style,
&mut text,
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 mut plugin = PluginWrapper::new(NoPlugin::new());
let lm = style.measure_line(
&mut plugin,
&character_style,
&mut text,
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 mut plugin = PluginWrapper::new(NoPlugin::new());
let lm = style.measure_line(
&mut plugin,
&character_style,
&mut text,
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 mut plugin = PluginWrapper::new(NoPlugin::new());
let lm = style.measure_line(
&mut plugin,
&character_style,
&mut text,
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 mut plugin = PluginWrapper::new(NoPlugin::new());
let lm = style.measure_line(
&mut plugin,
&character_style,
&mut Parser::parse("soft\u{AD}hyphen"),
50,
);
assert_eq!(lm.width, 30);
}
}