use embedded_graphics::{prelude::*, style::TextStyle};
pub mod builder;
use crate::{
alignment::TextAlignment,
parser::{Parser, Token},
rendering::{StateFactory, StyledTextBoxIterator},
utils::{font_ext::FontExt, rect_ext::RectExt},
TextBox,
};
pub use builder::TextBoxStyleBuilder;
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
pub struct TextBoxStyle<C, F, A>
where
C: PixelColor,
F: Font + Copy,
A: TextAlignment,
{
pub text_style: TextStyle<C, F>,
pub alignment: A,
}
impl<C, F, A> TextBoxStyle<C, F, A>
where
C: PixelColor,
F: Font + Copy,
A: TextAlignment,
{
#[inline]
pub fn new(font: F, text_color: C, alignment: A) -> Self {
Self {
text_style: TextStyle::new(font, text_color),
alignment,
}
}
#[inline]
pub fn from_text_style(text_style: TextStyle<C, F>, alignment: A) -> Self {
Self {
text_style,
alignment,
}
}
fn measure_word(w: &str, max_width: u32) -> (u32, Option<&str>) {
let (width, consumed) = F::max_str_width(w, max_width);
let carried = if consumed == w {
None
} else {
Some(unsafe {
w.get_unchecked(consumed.len()..)
})
};
(width, carried)
}
#[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>>) {
let mut current_width = 0;
let mut last_spaces = 0;
let mut last_space_width = 0;
let mut first_word_processed = false;
if let Some(t) = carried_token {
match t {
Token::Word(w) => {
let (width, carried) = Self::measure_word(w, max_line_width);
if let Some(w) = carried {
return (width, 0, Some(Token::Word(w)));
}
first_word_processed = true;
current_width = width;
}
Token::Whitespace(n) => {
if A::STARTING_SPACES {
let (width, consumed) = F::max_space_width(n, max_line_width);
let carried = n - consumed;
if carried != 0 {
let token = Some(Token::Whitespace(carried));
return if A::ENDING_SPACES {
(width, consumed, token)
} else {
(0, 0, token)
};
}
last_spaces = n;
last_space_width = width;
}
}
Token::NewLine => {
}
}
}
let mut total_spaces = 0;
let mut carried_token = None;
for token in parser {
let available_space = max_line_width - current_width - last_space_width;
match token {
Token::Word(w) => {
let (width, carried) = Self::measure_word(w, available_space);
if let Some(carried_w) = carried {
let carried_word = if first_word_processed {
w
} else {
if width != 0 {
current_width += last_space_width + width;
total_spaces += last_spaces;
}
carried_w
};
carried_token.replace(Token::Word(carried_word));
break;
}
current_width += last_space_width + width;
total_spaces += last_spaces;
first_word_processed = true;
last_space_width = 0;
last_spaces = 0;
}
Token::Whitespace(n) => {
if A::STARTING_SPACES || first_word_processed {
let (width, consumed) = F::max_space_width(n, available_space);
last_spaces += n;
last_space_width += width;
if n != consumed {
carried_token.replace(Token::Whitespace(n - consumed));
break;
}
}
}
Token::NewLine => {
carried_token.replace(Token::NewLine);
break;
}
}
}
if A::ENDING_SPACES {
total_spaces += last_spaces;
current_width += last_space_width;
}
(current_width, total_spaces, carried_token)
}
#[inline]
#[must_use]
pub fn measure_text_height(&self, text: &str, max_width: u32) -> u32 {
let mut current_height = 0;
let mut parser = Parser::parse(text);
let mut carry = None;
let mut bytes = parser.remaining();
loop {
let (w, _, t) = self.measure_line(&mut parser, carry.clone(), max_width);
let remaining = parser.remaining();
if t.is_none() || (t == carry && bytes == remaining) {
if w != 0 {
current_height += F::CHARACTER_SIZE.height;
}
return current_height;
}
bytes = remaining;
current_height += F::CHARACTER_SIZE.height;
carry = t;
}
}
}
pub struct StyledTextBox<'a, C, F, A>
where
C: PixelColor,
F: Font + Copy,
A: TextAlignment,
{
pub text_box: TextBox<'a>,
pub style: TextBoxStyle<C, F, A>,
}
impl<'a, C, F, A> Drawable<C> for &'a StyledTextBox<'a, C, F, A>
where
C: PixelColor,
F: Font + Copy,
A: TextAlignment,
StyledTextBoxIterator<'a, C, F, A>: Iterator<Item = Pixel<C>>,
StyledTextBox<'a, C, F, A>: StateFactory,
{
#[inline]
fn draw<D: DrawTarget<C>>(self, display: &mut D) -> Result<(), D::Error> {
display.draw_iter(StyledTextBoxIterator::new(self))
}
}
impl<C, F, A> Transform for StyledTextBox<'_, C, F, A>
where
C: PixelColor,
F: Font + Copy,
A: TextAlignment,
{
#[inline]
#[must_use]
fn translate(&self, by: Point) -> Self {
Self {
text_box: self.text_box.translate(by),
style: self.style,
}
}
#[inline]
fn translate_mut(&mut self, by: Point) -> &mut Self {
self.text_box.bounds.translate_mut(by);
self
}
}
impl<C, F, A> Dimensions for StyledTextBox<'_, C, F, A>
where
C: PixelColor,
F: Font + Copy,
A: TextAlignment,
{
#[inline]
#[must_use]
fn top_left(&self) -> Point {
self.text_box.bounds.top_left
}
#[inline]
#[must_use]
fn bottom_right(&self) -> Point {
self.text_box.bounds.bottom_right
}
#[inline]
#[must_use]
fn size(&self) -> Size {
RectExt::size(self.text_box.bounds)
}
}
#[cfg(test)]
mod test {
use crate::{alignment::*, style::builder::TextBoxStyleBuilder};
use embedded_graphics::{fonts::Font6x8, pixelcolor::BinaryColor};
#[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\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),
];
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,
"#{}: 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,
"#{}: Height of \"{}\" is {} but is expected to be {}",
i, text, height, expected_height
);
}
}
}