use crate::{
font::FontMetrics,
primitives::{Point, ProposedDimension, Size, geometry::Rectangle},
view::text::WrappedLine,
};
#[derive(Debug, Clone)]
pub struct CharacterWrap<'a, F> {
remaining: &'a str,
available_width: ProposedDimension,
font: &'a F,
calculate_precise_bounds: bool,
current_y: i32,
first_non_empty_line: Option<(&'a str, i32)>,
last_non_empty_line: Option<(&'a str, i32)>,
}
impl<'a, F: FontMetrics> CharacterWrap<'a, F> {
pub fn new(
text: &'a str,
available_width: impl Into<ProposedDimension>,
font: &'a F,
calculate_precise_bounds: bool,
) -> Self {
Self {
remaining: text,
available_width: available_width.into(),
font,
calculate_precise_bounds,
current_y: 0,
first_non_empty_line: None,
last_non_empty_line: None,
}
}
#[expect(clippy::ref_option)]
pub fn first_non_empty_line(&self) -> &'_ Option<(&'a str, i32)> {
&self.first_non_empty_line
}
#[expect(clippy::ref_option)]
pub fn last_non_empty_line(&self) -> &'_ Option<(&'a str, i32)> {
&self.last_non_empty_line
}
fn calculate_precise_width_and_extents(
&self,
text: &str,
advance_width: u32,
) -> (u32, i32, i32) {
if advance_width == 0 {
return (0, 0, 0);
}
let Some(first_char) = text.chars().next() else {
return (0, 0, 0);
};
let Some(last_char) = text.chars().next_back() else {
return (0, 0, 0);
};
let first_bounds = self.font.rendered_size(first_char).unwrap_or_else(|| {
Rectangle::new(Point::zero(), Size::new(self.font.advance(first_char), 0))
});
let last_char_advance = self.font.advance(last_char);
let last_bounds = self
.font
.rendered_size(last_char)
.unwrap_or_else(|| Rectangle::new(Point::zero(), Size::new(last_char_advance, 0)));
let min_x = first_bounds.origin.x;
let max_x = advance_width as i32 - last_char_advance as i32
+ last_bounds.origin.x
+ last_bounds.size.width as i32;
let precise_width =
(advance_width as i32 - first_bounds.origin.x - last_char_advance as i32
+ last_bounds.origin.x
+ last_bounds.size.width as i32)
.max(0) as u32;
(precise_width, min_x, max_x)
}
fn make_wrapped_line(&mut self, content: &'a str, width: u32) -> WrappedLine<'a> {
let (precise_width, min_x, max_x) = if self.calculate_precise_bounds {
self.calculate_precise_width_and_extents(content, width)
} else {
(0, 0, 0)
};
if self.calculate_precise_bounds && !content.is_empty() {
if self.first_non_empty_line.is_none() {
self.first_non_empty_line = Some((content, self.current_y));
}
self.last_non_empty_line = Some((content, self.current_y));
}
self.current_y += self.font.vertical_metrics().line_height() as i32;
WrappedLine {
content,
width,
precise_width,
min_x,
max_x,
}
}
}
impl<'a, F: FontMetrics + 'a> Iterator for CharacterWrap<'a, F> {
type Item = WrappedLine<'a>;
fn next(&mut self) -> Option<Self::Item> {
let mut remaining_iter = self.remaining.char_indices();
let (mut split_pos, mut ch) = remaining_iter.next()?;
let mut width = self.font.advance(ch);
loop {
if ch == '\n' {
let (line, rest) = self.remaining.split_at(split_pos);
self.remaining = &rest[1..];
return Some(self.make_wrapped_line(line, width));
}
if let Some((idx, character)) = remaining_iter.next() {
let new_width = width + self.font.advance(character);
ch = character;
split_pos = idx;
if ProposedDimension::Exact(new_width) > self.available_width {
break;
}
width = new_width;
} else {
split_pos = self.remaining.len();
break;
}
}
if ch == '\n' && split_pos != self.remaining.len() - 1 {
let (line, rest) = self.remaining.split_at(split_pos);
self.remaining = &rest[1..];
return Some(self.make_wrapped_line(line, width));
}
let (result, rest) = self.remaining.split_at(split_pos);
self.remaining = rest;
Some(self.make_wrapped_line(result, width))
}
}
#[cfg(test)]
mod tests {
use super::CharacterWrap;
use crate::font::{CharacterBufferFont, Font, FontMetrics, FontRender};
use crate::primitives::Size;
use crate::primitives::geometry::Rectangle;
use crate::primitives::{Point, ProposedDimension};
use crate::surface::Surface;
use std::vec::Vec;
use std::{self, vec};
static FONT: CharacterBufferFont = CharacterBufferFont;
#[test]
fn single_word() {
let metrics = &FONT.metrics(&());
let wrap = CharacterWrap::new("hello", 10, metrics, false);
assert_eq!(
wrap.map(|l| l.content).collect::<Vec<&str>>(),
vec!["hello"]
);
}
#[test]
fn breaks_anywhere_not_at_space() {
let metrics = &FONT.metrics(&());
let wrap = CharacterWrap::new("hello world", 10, metrics, false);
assert_eq!(
wrap.map(|l| l.content).collect::<Vec<&str>>(),
vec!["hello worl", "d"]
);
}
#[test]
fn partial_words_are_wrapped_2() {
let metrics = &FONT.metrics(&());
let wrap = CharacterWrap::new("hello world", 2, metrics, false);
assert_eq!(
wrap.map(|l| l.content).collect::<Vec<_>>(),
vec!["he", "ll", "o ", "wo", "rl", "d"]
);
}
#[test]
fn newlines_are_respected() {
let metrics = &FONT.metrics(&());
let wrap = CharacterWrap::new("hello\nworld", 3, metrics, false);
assert_eq!(
wrap.map(|l| l.content).collect::<Vec<_>>(),
vec!["hel", "lo", "wor", "ld"]
);
}
#[test]
fn compact_and_infinite_do_not_wrap_unless_newline() {
let metrics = &FONT.metrics(&());
let wrap = CharacterWrap::new("hello world", ProposedDimension::Compact, metrics, false);
assert_eq!(
wrap.map(|l| l.content).collect::<Vec<_>>(),
vec!["hello world"]
);
let wrap = CharacterWrap::new("hello\nworld", ProposedDimension::Compact, metrics, false);
assert_eq!(
wrap.map(|l| l.content).collect::<Vec<_>>(),
vec!["hello", "world"]
);
let wrap = CharacterWrap::new("hello world", ProposedDimension::Infinite, metrics, false);
assert_eq!(
wrap.map(|l| l.content).collect::<Vec<_>>(),
vec!["hello world"]
);
}
struct VariableWidthFont;
struct VariableWidthFontMetrics;
impl FontMetrics for VariableWidthFontMetrics {
fn rendered_size(&self, c: char) -> Option<Rectangle> {
let size = Size::new(self.advance(c), 1);
Some(Rectangle::new(Point::zero(), size))
}
fn vertical_metrics(&self) -> crate::font::VMetrics {
crate::font::VMetrics {
ascent: 1,
descent: 0,
line_spacing: 0,
}
}
fn advance(&self, character: char) -> u32 {
if character.is_whitespace() {
2
} else if character.is_ascii_digit() {
character.to_digit(10).unwrap_or(1)
} else {
1
}
}
}
impl Font for VariableWidthFont {
type Attributes = ();
fn metrics(&self, _attributes: &Self::Attributes) -> impl crate::font::FontMetrics {
VariableWidthFontMetrics
}
}
impl crate::font::Sealed for VariableWidthFont {}
impl<C> FontRender<C> for VariableWidthFont {
fn draw(
&self,
_: char,
_offset: Point,
_: C,
_: Option<C>,
_attributes: &Self::Attributes,
_: &mut impl Surface<Color = C>,
) {
}
}
#[test]
fn variable_width_respected() {
let metrics = &VariableWidthFont.metrics(&());
let wrap = CharacterWrap::new("1 2 3 4 5 6", 5, metrics, false);
let parts = wrap.map(|l| l.content).collect::<Vec<_>>();
assert_eq!(parts, vec!["1 2", " 3", " ", "4", " ", "5", " ", "6"]);
}
#[test]
fn zero_sized_offer() {
let metrics = &FONT.metrics(&());
let wrap_0 = CharacterWrap::new("he\nllo", 0, metrics, false);
assert_eq!(
wrap_0.map(|l| l.content).collect::<Vec<_>>(),
vec!["h", "e", "l", "l", "o"]
);
let wrap_1 = CharacterWrap::new("he\nllo", 1, metrics, false);
assert_eq!(
wrap_1.map(|l| l.content).collect::<Vec<_>>(),
vec!["h", "e", "l", "l", "o"]
);
}
#[test]
fn natural_breaks_consume_explicit_newlines() {
let metrics = &FONT.metrics(&());
let wrap = CharacterWrap::new("1\n\n3\n", 1, metrics, false);
assert_eq!(
wrap.map(|l| l.content).collect::<Vec<_>>(),
vec!["1", "", "3", ""]
);
}
#[test]
fn unicode_wraps_correctly() {
let metrics = &FONT.metrics(&());
let wrap = CharacterWrap::new("rº🦀_🦀 🦀\nyº ºº\t🦀", 4, metrics, false);
assert_eq!(
wrap.map(|l| l.content).collect::<Vec<_>>(),
vec!["rº🦀_", "🦀 🦀", "yº º", "º\t🦀"]
);
}
}