use core::ops::Range;
use crate::{
font::{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: Font> {
pub origin: Point,
pub size: (u16, u16),
pub font: &'a F,
pub text: T,
pub attributes: F::Attributes,
pub alignment: HorizontalTextAlignment,
pub max_lines: u16,
pub wrap: WrapStrategy,
}
impl<'a, T: AsRef<str>, F: Font> Text<'a, T, F> {
#[expect(clippy::too_many_arguments)]
pub fn new(
origin: Point,
size: (u16, u16),
font: &'a F,
text: T,
attributes: F::Attributes,
alignment: HorizontalTextAlignment,
max_lines: u16,
wrap: WrapStrategy,
) -> Self {
Self {
origin,
size,
font,
text,
attributes,
alignment,
max_lines,
wrap,
}
}
}
impl<T: Clone, F: Font> Clone for Text<'_, T, F> {
fn clone(&self) -> Self {
Self {
origin: self.origin,
size: self.size,
font: self.font,
text: self.text.clone(),
attributes: self.attributes.clone(),
alignment: self.alignment,
max_lines: self.max_lines,
wrap: self.wrap,
}
}
}
impl<T: AsRef<str>, F: Font> AnimatedJoin for Text<'_, T, F> {
fn join_from(&mut self, source: &Self, domain: &AnimationDomain) {
self.origin = Interpolate::interpolate(source.origin, self.origin, domain.factor);
self.attributes = Interpolate::interpolate(
source.attributes.clone(),
self.attributes.clone(),
domain.factor,
);
self.size.0 = Interpolate::interpolate(source.size.0, self.size.0, domain.factor);
self.size.1 = Interpolate::interpolate(source.size.1, self.size.1, domain.factor);
}
}
impl<C: Copy, T: AsRef<str> + Clone, F: FontRender<C>> Render<C> for Text<'_, T, F> {
fn render(&self, render_target: &mut impl RenderTarget<ColorFormat = C>, style: &C) {
let size: Size = self.size.into();
let clip_rect = render_target.clip_rect();
let bounding_box = Rectangle::new(self.origin, size);
if size.area() == 0 || !bounding_box.intersects(&clip_rect) {
return;
}
let metrics = self.font.metrics(&self.attributes);
let brush = SolidBrush::new(*style);
let line_height = metrics.vertical_metrics().line_height();
let mut height = 0;
let mut word_wrap = WordWrap::new(self.text.as_ref(), size.width, &metrics, false);
let mut character_wrap =
CharacterWrap::new(self.text.as_ref(), 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.into_iter().take(self.max_lines as usize) {
let width = line.width;
let line_x = self.alignment.align(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 >= 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,
&self.attributes,
);
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 attributes = Interpolate::interpolate(
source.attributes.clone(),
target.attributes.clone(),
domain.factor,
);
let size = (
Interpolate::interpolate(source.size.0, target.size.0, domain.factor),
Interpolate::interpolate(source.size.1, target.size.1, domain.factor),
);
Text {
text: target.text.as_ref(),
origin,
size,
font: target.font,
attributes,
alignment: target.alignment,
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),
(100, 50),
&font,
"Hello",
(),
HorizontalTextAlignment::Leading,
100,
WrapStrategy::Word,
);
let mut target = Text::new(
Point::new(50, 25),
(200, 100),
&font,
"World",
(),
HorizontalTextAlignment::Center,
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),
(100, 50),
&font,
"Hello",
(),
HorizontalTextAlignment::Leading,
100,
WrapStrategy::Word,
);
let original_target = Text::new(
Point::new(50, 25),
(200, 100),
&font,
"World",
(),
HorizontalTextAlignment::Center,
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),
(50, 25),
&font,
"Start",
(),
HorizontalTextAlignment::Leading,
100,
WrapStrategy::Word,
);
let original_target = Text::new(
Point::new(100, 50),
(150, 75),
&font,
"End",
(),
HorizontalTextAlignment::Trailing,
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.0 > source.size.0 && target.size.0 < original_target.size.0);
assert!(target.size.1 > source.size.1 && target.size.1 < original_target.size.1);
assert_eq!(target.text, original_target.text);
assert_eq!(target.alignment, original_target.alignment);
}
}