mod builder;
mod height_mode;
mod vertical_overdraw;
use core::convert::Infallible;
use crate::{
alignment::{HorizontalAlignment, VerticalAlignment},
parser::Parser,
plugin::{NoPlugin, PluginMarker as Plugin, PluginWrapper, ProcessingState},
rendering::{
cursor::LineCursor,
line_iter::{ElementHandler, LineElementParser, LineEndType},
space_config::SpaceConfig,
},
utils::{str_width, str_width_and_left_offset},
};
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 TabSize {
#[inline]
pub const fn default() -> Self {
Self::Spaces(4)
}
#[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,
pub leading_spaces: bool,
pub trailing_spaces: bool,
}
impl TextBoxStyle {
#[inline]
pub const fn default() -> Self {
TextBoxStyleBuilder::new().build()
}
#[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()
}
#[inline]
pub const fn with_height_mode(mode: HeightMode) -> TextBoxStyle {
TextBoxStyleBuilder::new().height_mode(mode).build()
}
#[inline]
pub const fn with_line_height(line_height: LineHeight) -> TextBoxStyle {
TextBoxStyleBuilder::new().line_height(line_height).build()
}
#[inline]
pub const fn with_paragraph_spacing(spacing: u32) -> TextBoxStyle {
TextBoxStyleBuilder::new()
.paragraph_spacing(spacing)
.build()
}
#[inline]
pub const fn with_tab_size(tab_size: TabSize) -> TextBoxStyle {
TextBoxStyleBuilder::new().tab_size(tab_size).build()
}
}
#[derive(Debug, Copy, Clone)]
#[must_use]
pub(crate) struct LineMeasurement {
pub max_line_width: u32,
pub width: u32,
pub line_end_type: LineEndType,
pub space_count: u32,
}
impl LineMeasurement {
pub fn last_line(&self) -> bool {
matches!(
self.line_end_type,
LineEndType::NewLine | LineEndType::EndOfText
)
}
pub fn is_empty(&self) -> bool {
self.width == 0
}
}
struct MeasureLineElementHandler<'a, S> {
style: &'a S,
trailing_spaces: bool,
cursor: u32,
pos: u32,
right: u32,
partial_space_count: u32,
space_count: u32,
}
impl<'a, S> MeasureLineElementHandler<'a, S> {
fn space_count(&self) -> u32 {
if self.trailing_spaces {
self.partial_space_count
} else {
self.space_count
}
}
fn right(&self) -> u32 {
if self.trailing_spaces {
self.pos
} else {
self.right
}
}
}
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 measure_width_and_left_offset(&self, st: &str) -> (u32, u32) {
str_width_and_left_offset(self.style, st)
}
fn whitespace(&mut self, _st: &str, count: u32, width: u32) -> Result<(), Self::Error> {
self.cursor += width;
self.pos = self.pos.max(self.cursor);
self.partial_space_count += count;
Ok(())
}
fn printed_characters(&mut self, str: &str, width: Option<u32>) -> Result<(), Self::Error> {
self.cursor += width.unwrap_or_else(|| self.measure(str));
self.pos = self.pos.max(self.cursor);
self.right = self.pos;
self.space_count = self.partial_space_count;
Ok(())
}
fn move_cursor(&mut self, by: i32) -> Result<(), Self::Error> {
self.cursor = (self.cursor as i32 + by) 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>,
{
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,
);
let mut handler = MeasureLineElementHandler {
style: character_style,
trailing_spaces: self.trailing_spaces,
cursor: 0,
pos: 0,
right: 0,
partial_space_count: 0,
space_count: 0,
};
let last_token = iter.process(&mut handler).unwrap();
LineMeasurement {
max_line_width,
width: handler.right(),
space_count: handler.space_count(),
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,
{
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>,
{
let mut parser = Parser::parse(text);
let base_line_height = character_style.line_height();
let line_height = self.line_height.to_absolute(base_line_height);
let mut height = base_line_height;
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 prev_end == LineEndType::LineBreak && !lm.is_empty() {
height += line_height;
}
match lm.line_end_type {
LineEndType::CarriageReturn | LineEndType::LineBreak => {}
LineEndType::NewLine => height += line_height + self.paragraph_spacing,
LineEndType::EndOfText => return height,
}
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::with_paragraph_spacing(2);
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()
+ (text.chars().filter(|&c| c == '\n').count() as u32) * style.paragraph_spacing;
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]
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);
}
}