pub mod builder;
pub mod color;
pub mod height_mode;
pub mod vertical_overdraw;
use crate::{
alignment::{HorizontalTextAlignment, VerticalTextAlignment},
parser::{Parser, Token},
rendering::{
cursor::Cursor,
line_iter::{LineElementIterator, RenderElement},
space_config::UniformSpaceConfig,
},
style::height_mode::HeightMode,
utils::font_ext::FontExt,
};
use core::marker::PhantomData;
use embedded_graphics::{prelude::*, primitives::Rectangle, style::TextStyle};
#[cfg(feature = "ansi")]
use crate::rendering::ansi::Sgr;
pub use builder::TextBoxStyleBuilder;
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct TabSize<F> {
pub(crate) width: i32,
_font: PhantomData<F>,
}
impl<F: Font> Default for TabSize<F> {
#[inline]
fn default() -> Self {
Self::spaces(4)
}
}
impl<F: Font> TabSize<F> {
#[inline]
pub fn spaces(n: u32) -> Self {
let space = F::total_char_width(' ') as i32;
let size = (n.max(1) as i32).checked_mul(space).unwrap_or(4 * space);
Self::pixels(size)
}
#[inline]
pub fn pixels(px: i32) -> Self {
Self {
width: px,
_font: PhantomData,
}
}
#[inline]
pub fn next_width(self, pos: i32) -> u32 {
let next_tab_pos = (pos / self.width + 1) * self.width;
(next_tab_pos - pos) as u32
}
}
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Default)]
pub struct TextBoxStyle<C, F, A, V, H>
where
C: PixelColor,
F: Font + Copy,
{
pub text_style: TextStyle<C, F>,
pub alignment: A,
pub vertical_alignment: V,
pub height_mode: H,
pub line_spacing: i32,
pub tab_size: TabSize<F>,
pub underlined: bool,
pub strikethrough: bool,
}
impl<C, F, A, V, H> TextBoxStyle<C, F, A, V, H>
where
C: PixelColor,
F: Font + Copy,
A: HorizontalTextAlignment,
V: VerticalTextAlignment,
H: HeightMode,
{
#[inline]
pub fn new(
font: F,
text_color: C,
alignment: A,
vertical_alignment: V,
height_mode: H,
) -> Self {
Self::from_text_style(
TextStyle::new(font, text_color),
alignment,
vertical_alignment,
height_mode,
)
}
#[inline]
pub fn from_text_style(
text_style: TextStyle<C, F>,
alignment: A,
vertical_alignment: V,
height_mode: H,
) -> Self {
Self {
text_style,
alignment,
vertical_alignment,
height_mode,
line_spacing: 0,
tab_size: TabSize::default(),
underlined: false,
strikethrough: false,
}
}
#[inline]
#[must_use]
pub fn measure_line<'a>(
&self,
parser: &mut Parser<'a>,
carried_token: Option<Token<'a>>,
max_line_width: u32,
) -> (u32, u32, Option<Token<'a>>, bool) {
let cursor: Cursor<F> = Cursor::new(
Rectangle::new(
Point::zero(),
Point::new(
max_line_width.saturating_sub(1) as i32,
F::CHARACTER_SIZE.height.saturating_sub(1) as i32,
),
),
self.line_spacing,
);
let mut iter: LineElementIterator<'_, F, _, A> = LineElementIterator::new(
parser.clone(),
cursor,
UniformSpaceConfig::default(),
carried_token.clone(),
self.tab_size,
);
let mut current_width = 0;
let mut last_spaces = 0;
let mut last_spaces_width = 0;
let mut total_spaces = 0;
#[cfg(feature = "ansi")]
let mut underlined = self.underlined;
#[cfg(not(feature = "ansi"))]
let underlined = self.underlined;
while let Some(token) = iter.next() {
match token {
RenderElement::Space(width, count) => {
if A::ENDING_SPACES {
current_width += width;
total_spaces += count;
} else {
last_spaces = total_spaces + count;
last_spaces_width = width;
}
}
RenderElement::PrintedCharacter(c) => {
current_width += F::total_char_width(c);
if c == '\u{A0}' {
total_spaces += 1;
} else if !A::ENDING_SPACES {
current_width += last_spaces_width;
last_spaces_width = 0;
total_spaces = last_spaces;
}
}
#[cfg(feature = "ansi")]
RenderElement::Sgr(Sgr::Underline) => underlined = true,
#[cfg(feature = "ansi")]
RenderElement::Sgr(_) => {}
}
}
let carried = iter.remaining_token();
*parser = iter.parser;
(current_width as u32, total_spaces, carried, underlined)
}
#[inline]
#[must_use]
pub fn measure_text_height(&self, text: &str, max_width: u32) -> u32 {
let mut n_lines = 0_i32;
let mut parser = Parser::parse(text);
let mut carry = None;
loop {
let (w, _, t, underlined) = self.measure_line(&mut parser, carry.clone(), max_width);
if (w != 0 || t.is_some()) && carry != Some(Token::CarriageReturn) {
n_lines += 1;
}
if t.is_none() {
let mut height = (n_lines * F::CHARACTER_SIZE.height as i32
+ n_lines.saturating_sub(1) * self.line_spacing)
as u32;
if underlined {
height += 1;
}
return height;
}
carry = t;
}
}
}
#[cfg(test)]
mod test {
use crate::{alignment::*, parser::Parser, style::builder::TextBoxStyleBuilder};
use embedded_graphics::{
fonts::{Font, Font6x8},
pixelcolor::BinaryColor,
};
#[test]
fn no_infinite_loop() {
let _ = TextBoxStyleBuilder::new(Font6x8)
.text_color(BinaryColor::On)
.build()
.measure_text_height("a", 5);
}
#[test]
fn test_measure_height() {
let data = [
("", 0, 0),
(" ", 0, 8),
(" ", 5, 8),
(" ", 6, 8),
("\n", 6, 8),
("\n ", 6, 16),
("word", 4 * 6, 8), ("word", 4 * 6 - 1, 16),
("word", 2 * 6, 16), ("word word", 4 * 6, 16), ("word\n", 2 * 6, 16),
("word\nnext", 50, 16),
("word\n\nnext", 50, 24),
("word\n \nnext", 50, 24),
("verylongword", 50, 16),
("some verylongword", 50, 24),
("1 23456 12345 61234 561", 36, 40),
(" Word ", 36, 24),
("\rcr", 36, 8),
("Longer\r", 36, 8),
("Longer\rnowrap", 36, 8),
];
let textbox_style = TextBoxStyleBuilder::new(Font6x8)
.text_color(BinaryColor::On)
.build();
for (i, (text, width, expected_height)) in data.iter().enumerate() {
let height = textbox_style.measure_text_height(text, *width);
assert_eq!(
height, *expected_height,
r#"#{}: Height of "{}" is {} but is expected to be {}"#,
i, text, height, expected_height
);
}
}
#[test]
fn test_measure_height_ignored_spaces() {
let data = [
("", 0, 0),
(" ", 0, 0),
(" ", 6, 0),
("\n ", 6, 8),
("word\n", 2 * 6, 16),
("word\n \nnext", 50, 24),
(" Word ", 36, 8),
];
let textbox_style = TextBoxStyleBuilder::new(Font6x8)
.alignment(CenterAligned)
.text_color(BinaryColor::On)
.build();
for (i, (text, width, expected_height)) in data.iter().enumerate() {
let height = textbox_style.measure_text_height(text, *width);
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 textbox_style = TextBoxStyleBuilder::new(Font6x8)
.alignment(CenterAligned)
.text_color(BinaryColor::On)
.build();
let mut text = Parser::parse("123 45 67");
let (w, s, _, _) =
textbox_style.measure_line(&mut text, None, 6 * Font6x8::CHARACTER_SIZE.width);
assert_eq!(w, 6 * Font6x8::CHARACTER_SIZE.width);
assert_eq!(s, 1);
}
#[test]
fn test_measure_line_counts_nbsp() {
let textbox_style = TextBoxStyleBuilder::new(Font6x8)
.alignment(CenterAligned)
.text_color(BinaryColor::On)
.build();
let mut text = Parser::parse("123\u{A0}45");
let (w, s, _, _) =
textbox_style.measure_line(&mut text, None, 5 * Font6x8::CHARACTER_SIZE.width);
assert_eq!(w, 5 * Font6x8::CHARACTER_SIZE.width);
assert_eq!(s, 1);
}
#[test]
fn test_measure_height_nbsp() {
let textbox_style = TextBoxStyleBuilder::new(Font6x8)
.alignment(CenterAligned)
.text_color(BinaryColor::On)
.build();
let text = "123\u{A0}45 123";
let height = textbox_style.measure_text_height(text, 5 * Font6x8::CHARACTER_SIZE.width);
assert_eq!(height, 16);
let textbox_style = TextBoxStyleBuilder::new(Font6x8)
.alignment(LeftAligned)
.text_color(BinaryColor::On)
.build();
let text = "embedded-text also\u{A0}supports non-breaking spaces.";
let height = textbox_style.measure_text_height(text, 79);
assert_eq!(height, 4 * Font6x8::CHARACTER_SIZE.height);
}
#[test]
fn height_with_line_spacing() {
let style = TextBoxStyleBuilder::new(Font6x8)
.text_color(BinaryColor::On)
.line_spacing(2)
.build();
let height = style.measure_text_height(
"Lorem Ipsum is simply dummy text of the printing and typesetting industry.",
72,
);
assert_eq!(height, 7 * 8 + 6 * 2);
}
#[test]
fn soft_hyphenated_line_width_includes_hyphen_width() {
let style = TextBoxStyleBuilder::new(Font6x8)
.text_color(BinaryColor::On)
.line_spacing(2)
.build();
let (width, _, _, _) = style.measure_line(&mut Parser::parse("soft\u{AD}hyphen"), None, 50);
assert_eq!(width, 30);
}
}