use core::ops::Range;
use heapless::Vec;
use crate::{
font::{FontMetrics, FontRender},
primitives::{Interpolate, Point, Size, geometry::Rectangle},
render::{AnimatedJoin, AnimationDomain, Render},
render_target::{Glyph, RenderTarget, SolidBrush},
view::{CharacterWrap, HorizontalTextAlignment, WordWrap, WrapStrategy},
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Line {
pub range: Range<usize>,
pub pixel_width: u32,
}
#[derive(Debug, PartialEq, Eq)]
pub struct Text<'a, T, F, const LINES: usize> {
pub origin: Point,
pub size: Size,
pub font: &'a F,
pub text: T,
pub alignment: HorizontalTextAlignment,
pub lines: Vec<Line, LINES>,
pub max_lines: u32,
pub wrap: WrapStrategy,
}
impl<'a, T: AsRef<str>, F> Text<'a, T, F, 8> {
#[expect(clippy::too_many_arguments)]
pub fn new(
origin: Point,
size: Size,
font: &'a F,
text: T,
alignment: HorizontalTextAlignment,
lines: Vec<Line, 8>,
max_lines: u32,
wrap: WrapStrategy,
) -> Self {
Self {
origin,
size,
font,
text,
alignment,
lines,
max_lines,
wrap,
}
}
}
impl<T: Clone, F, const N: usize> Clone for Text<'_, T, F, N> {
fn clone(&self) -> Self {
Self {
origin: self.origin,
size: self.size,
font: self.font,
text: self.text.clone(),
alignment: self.alignment,
lines: self.lines.clone(),
max_lines: self.max_lines,
wrap: self.wrap,
}
}
}
impl<T: AsRef<str>, F, const N: usize> AnimatedJoin for Text<'_, T, F, N> {
fn join_from(&mut self, source: &Self, domain: &AnimationDomain) {
self.origin = Interpolate::interpolate(source.origin, self.origin, domain.factor);
self.size = Interpolate::interpolate(source.size, self.size, domain.factor);
}
}
impl<C: Copy, T: AsRef<str> + Clone, F: FontRender<C>, const LINE_BREAKS: usize> Render<C>
for Text<'_, T, F, LINE_BREAKS>
{
fn render(&self, render_target: &mut impl RenderTarget<ColorFormat = C>, style: &C) {
let clip_rect = render_target.clip_rect();
let bounding_box = Rectangle::new(self.origin, self.size);
if self.size.area() == 0 || !bounding_box.intersects(&clip_rect) {
return;
}
let metrics = self.font.metrics();
let brush = SolidBrush::new(*style);
let line_height = metrics.default_line_height();
let mut height = 0;
let mut line_count = 0;
for line in &self.lines {
if line_count >= self.max_lines {
break;
}
line_count += 1;
let line_x = self
.alignment
.align(self.size.width as i32, line.pixel_width as i32)
+ self.origin.x;
let mut x = 0;
let line_offset = Point::new(line_x, self.origin.y + height);
let line_bounding_box =
Rectangle::new(line_offset, Size::new(line.pixel_width, line_height));
if !line_bounding_box.intersects(&clip_rect) {
height += line_height as i32;
if height >= self.size.height as i32 {
break;
}
continue;
}
let Some(s) = self.text.as_ref().get(line.range.clone()) else {
continue; };
render_target.draw_glyphs(
line_offset,
&brush,
s.chars().map(|c| {
let glyph = Glyph {
character: c,
offset: Point::new(x, 0),
};
x += metrics.advance(glyph.character) as i32;
glyph
}),
self.font,
);
height += line_height as i32;
}
let remaining_text = self.lines.last().map_or(self.text.as_ref(), |last_range| {
self.text.as_ref().get(last_range.range.end..).unwrap_or("")
});
if remaining_text.is_empty() {
return;
}
let mut word_wrap = WordWrap::new(remaining_text, self.size.width, &metrics, false);
let mut character_wrap =
CharacterWrap::new(remaining_text, self.size.width, &metrics, false);
let wrap = core::iter::from_fn(|| match self.wrap {
WrapStrategy::Word => word_wrap.next(),
WrapStrategy::Character => character_wrap.next(),
});
let clip_rect = render_target.clip_rect();
for line in wrap {
if line_count >= self.max_lines {
break;
}
line_count += 1;
let width = line.width;
let line_x = self.alignment.align(self.size.width as i32, width as i32) + self.origin.x;
let mut x = 0;
let line_offset = Point::new(line_x, self.origin.y + height);
let line_bounding_box = Rectangle::new(line_offset, Size::new(width, line_height));
if line_bounding_box.origin.y > clip_rect.origin.y + clip_rect.size.height as i32 {
break;
}
if (line_bounding_box.origin.y + line_bounding_box.size.height as i32)
< clip_rect.origin.y
{
height += line_height as i32;
if height >= self.size.height as i32 {
break;
}
continue;
}
render_target.draw_glyphs(
line_offset,
&brush,
line.content.chars().map(|c| {
let glyph = Glyph {
character: c,
offset: Point::new(x, 0),
};
x += metrics.advance(glyph.character) as i32;
glyph
}),
self.font,
);
height += line_height as i32;
}
}
fn render_animated(
render_target: &mut impl RenderTarget<ColorFormat = C>,
source: &Self,
target: &Self,
style: &C,
domain: &AnimationDomain,
) {
let origin = Interpolate::interpolate(source.origin, target.origin, domain.factor);
let size = Interpolate::interpolate(source.size, target.size, domain.factor);
Text {
text: target.text.as_ref(),
origin,
size,
font: target.font,
alignment: target.alignment,
lines: target.lines.clone(),
max_lines: target.max_lines,
wrap: target.wrap,
}
.render(render_target, style);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::font::CharacterBufferFont;
use crate::view::HorizontalTextAlignment;
use core::time::Duration;
fn animation_domain(factor: u8) -> AnimationDomain {
AnimationDomain::new(factor, Duration::from_millis(100))
}
#[test]
fn animated_join_at_start() {
let font = CharacterBufferFont;
let source = Text::new(
Point::new(0, 0),
Size::new(100, 50),
&font,
"Hello",
HorizontalTextAlignment::Leading,
Vec::new(),
100,
WrapStrategy::Word,
);
let mut target = Text::new(
Point::new(50, 25),
Size::new(200, 100),
&font,
"World",
HorizontalTextAlignment::Center,
Vec::new(),
100,
WrapStrategy::Word,
);
target.join_from(&source, &animation_domain(0));
assert_eq!(target.origin, source.origin);
assert_eq!(target.size, source.size);
assert_eq!(target.text, target.text);
assert_eq!(target.alignment, target.alignment);
}
#[test]
fn animated_join_at_end() {
let font = CharacterBufferFont;
let source = Text::new(
Point::new(0, 0),
Size::new(100, 50),
&font,
"Hello",
HorizontalTextAlignment::Leading,
Vec::new(),
100,
WrapStrategy::Word,
);
let original_target = Text::new(
Point::new(50, 25),
Size::new(200, 100),
&font,
"World",
HorizontalTextAlignment::Center,
Vec::new(),
100,
WrapStrategy::Word,
);
let mut target = original_target.clone();
target.join_from(&source, &animation_domain(255));
assert_eq!(target.origin, original_target.origin);
assert_eq!(target.size, original_target.size);
assert_eq!(target.text, original_target.text);
assert_eq!(target.alignment, original_target.alignment);
}
#[test]
fn animated_join_interpolates_position_and_size() {
let font = CharacterBufferFont;
let source = Text::new(
Point::new(0, 0),
Size::new(50, 25),
&font,
"Start",
HorizontalTextAlignment::Leading,
Vec::new(),
100,
WrapStrategy::Word,
);
let original_target = Text::new(
Point::new(100, 50),
Size::new(150, 75),
&font,
"End",
HorizontalTextAlignment::Trailing,
Vec::new(),
100,
WrapStrategy::Word,
);
let mut target = original_target.clone();
target.join_from(&source, &animation_domain(128));
assert!(target.origin.x > source.origin.x && target.origin.x < original_target.origin.x);
assert!(target.origin.y > source.origin.y && target.origin.y < original_target.origin.y);
assert!(
target.size.width > source.size.width && target.size.width < original_target.size.width
);
assert!(
target.size.height > source.size.height
&& target.size.height < original_target.size.height
);
assert_eq!(target.text, original_target.text);
assert_eq!(target.alignment, original_target.alignment);
}
}