use embedded_graphics::{
Drawable,
draw_target::DrawTarget,
mono_font::MonoTextStyleBuilder,
pixelcolor::Rgb565,
prelude::Point,
text::{Baseline, Text, TextStyleBuilder},
};
use heapless::Vec;
use super::{TextRunStyle, TextSpan, TextViewStyle, TextWrap};
pub(super) const MAX_LAYOUT_LINES: usize = 32;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
struct Cursor {
span: usize,
byte: usize,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum FragmentKind {
Word,
Space,
Newline,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct Fragment {
kind: FragmentKind,
start: Cursor,
end: Cursor,
width: i32,
height: i32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) struct LineLayout {
start: Cursor,
end: Cursor,
pub(super) width: i32,
pub(super) height: i32,
}
pub(super) fn layout_lines(
spans: &[TextSpan<'_>],
view_style: &TextViewStyle,
max_width: i32,
) -> Vec<LineLayout, MAX_LAYOUT_LINES> {
let mut lines = Vec::new();
let mut cursor = Cursor::default();
let mut line_start = cursor;
let mut line_end = cursor;
let mut line_width = 0;
let mut line_height = 0;
let mut saw_content = false;
let mut pending_space = None;
while let Some(fragment) = next_fragment(spans, cursor) {
cursor = fragment.end;
match fragment.kind {
FragmentKind::Newline => {
push_line(
&mut lines,
line_start,
line_end,
line_width,
line_height.max(fragment.height),
);
line_start = cursor;
line_end = cursor;
line_width = 0;
line_height = 0;
saw_content = false;
pending_space = None;
}
FragmentKind::Space => {
if !saw_content {
line_start = fragment.end;
} else if matches!(view_style.wrap, TextWrap::SingleLine) {
line_width += fragment.width;
line_height = line_height.max(fragment.height);
line_end = fragment.end;
} else {
pending_space = Some(fragment);
}
}
FragmentKind::Word => {
let pending_width = pending_space.map(|space| space.width).unwrap_or(0);
let next_width = line_width + pending_width + fragment.width;
if matches!(view_style.wrap, TextWrap::Multiline)
&& saw_content
&& max_width > 0
&& next_width > max_width
{
push_line(&mut lines, line_start, line_end, line_width, line_height);
line_start = fragment.start;
line_end = fragment.end;
line_width = fragment.width;
line_height = fragment.height;
saw_content = true;
pending_space = None;
continue;
}
if let Some(space) = pending_space.take() {
line_width += space.width;
line_height = line_height.max(space.height);
}
if !saw_content {
line_start = fragment.start;
}
line_width += fragment.width;
line_height = line_height.max(fragment.height);
line_end = fragment.end;
saw_content = true;
if matches!(view_style.wrap, TextWrap::SingleLine)
&& max_width > 0
&& line_width >= max_width
{
break;
}
}
}
}
if saw_content || (lines.is_empty() && line_start != cursor) {
push_line(
&mut lines,
line_start,
line_end,
line_width,
line_height.max(1),
);
}
lines
}
pub(super) fn draw_line<D>(display: &mut D, spans: &[TextSpan<'_>], line: LineLayout, origin: Point)
where
D: DrawTarget<Color = Rgb565>,
{
let text_style = TextStyleBuilder::new().baseline(Baseline::Top).build();
let mut cursor = line.start;
let mut pen = origin;
while cursor != line.end {
let span = &spans[cursor.span];
let end = if cursor.span == line.end.span {
line.end.byte
} else {
span.text.len()
};
if end > cursor.byte {
let text = &span.text[cursor.byte..end];
let style = MonoTextStyleBuilder::new()
.font(span.style.font.mono())
.text_color(span.style.color)
.build();
Text::with_text_style(text, pen, style, text_style)
.draw(display)
.ok();
pen.x += text.chars().count() as i32 * span.style.font.advance();
}
if cursor.span == line.end.span {
break;
}
cursor = Cursor {
span: cursor.span + 1,
byte: 0,
};
}
}
fn push_line(
lines: &mut Vec<LineLayout, MAX_LAYOUT_LINES>,
start: Cursor,
end: Cursor,
width: i32,
height: i32,
) {
let _ = lines.push(LineLayout {
start,
end,
width: width.max(0),
height: height.max(1),
});
}
fn next_fragment(spans: &[TextSpan<'_>], start: Cursor) -> Option<Fragment> {
let (first, style, mut end) = next_char(spans, start)?;
if first == '\n' {
return Some(Fragment {
kind: FragmentKind::Newline,
start,
end,
width: 0,
height: style.font.line_height(),
});
}
let whitespace = first.is_whitespace();
let mut width = style.font.advance();
let mut height = style.font.line_height();
while let Some((ch, next_style, next)) = next_char(spans, end) {
if ch == '\n' || ch.is_whitespace() != whitespace {
break;
}
width += next_style.font.advance();
height = height.max(next_style.font.line_height());
end = next;
}
Some(Fragment {
kind: if whitespace {
FragmentKind::Space
} else {
FragmentKind::Word
},
start,
end,
width,
height,
})
}
fn next_char(spans: &[TextSpan<'_>], mut cursor: Cursor) -> Option<(char, TextRunStyle, Cursor)> {
while cursor.span < spans.len() {
let span = &spans[cursor.span];
if cursor.byte >= span.text.len() {
cursor.span += 1;
cursor.byte = 0;
continue;
}
let text = &span.text[cursor.byte..];
let ch = text.chars().next()?;
let next = Cursor {
span: cursor.span,
byte: cursor.byte + ch.len_utf8(),
};
return Some((ch, span.style, next));
}
None
}