use crate::render::dimension::Pt;
use crate::render::layout::fragment::Fragment;
#[derive(Debug)]
pub struct FittedLine {
pub start: usize,
pub end: usize,
pub width: Pt,
pub height: Pt,
pub text_height: Pt,
pub ascent: Pt,
pub has_break: bool,
}
pub fn fit_lines(fragments: &[Fragment], max_width: Pt) -> Vec<FittedLine> {
fit_lines_with_first(fragments, max_width, max_width)
}
pub fn fit_lines_with_first(
fragments: &[Fragment],
first_line_width: Pt,
remaining_width: Pt,
) -> Vec<FittedLine> {
if fragments.is_empty() {
return Vec::new();
}
let mut lines = Vec::new();
let mut line_start = 0;
let mut line_width = Pt::ZERO;
let mut line_height = Pt::ZERO;
let mut line_text_height = Pt::ZERO;
let mut line_ascent = Pt::ZERO;
let mut last_break_point = None;
let mut i = 0;
while i < fragments.len() {
let frag = &fragments[i];
if frag.is_line_break() {
line_height = line_height.max(frag.height());
line_text_height = line_text_height.max(frag.height());
lines.push(FittedLine {
start: line_start,
end: i + 1,
width: line_width,
height: line_height,
text_height: line_text_height,
ascent: line_ascent,
has_break: true,
});
line_start = i + 1;
line_width = Pt::ZERO;
line_height = Pt::ZERO;
line_text_height = Pt::ZERO;
line_ascent = Pt::ZERO;
last_break_point = None;
i += 1;
continue;
}
let frag_width = frag.width();
let new_width = line_width + frag_width;
let current_max = if lines.is_empty() {
first_line_width
} else {
remaining_width
};
let check_width = line_width + frag.trimmed_width();
if check_width > current_max && line_start < i {
let break_at = last_break_point.unwrap_or(i);
let m = measure_range(fragments, line_start, break_at);
lines.push(FittedLine {
start: line_start,
end: break_at,
width: m.width,
height: m.height,
text_height: m.text_height,
ascent: m.ascent,
has_break: false,
});
line_start = break_at;
line_width = Pt::ZERO;
line_height = Pt::ZERO;
line_text_height = Pt::ZERO;
line_ascent = Pt::ZERO;
last_break_point = None;
continue;
}
line_width = new_width;
line_height = line_height.max(frag.height());
if !matches!(frag, Fragment::Image { .. }) {
line_text_height = line_text_height.max(frag.height());
}
if let Fragment::Text { metrics, .. } = frag {
line_ascent = line_ascent.max(metrics.ascent);
}
let is_break_point = match frag {
Fragment::Text { text, .. } => {
text.ends_with(' ') || text.ends_with('\t')
|| text.ends_with('-')
|| text.ends_with('\u{2010}') || text.ends_with('\u{2011}') || text.ends_with('\u{2013}') || text.ends_with('\u{2014}') }
_ => true, };
if is_break_point {
last_break_point = Some(i + 1);
}
i += 1;
}
if line_start < fragments.len() {
lines.push(FittedLine {
start: line_start,
end: fragments.len(),
width: line_width,
height: line_height,
text_height: line_text_height,
ascent: line_ascent,
has_break: false,
});
}
lines
}
struct RangeMeasure {
width: Pt,
height: Pt,
text_height: Pt,
ascent: Pt,
}
fn measure_range(fragments: &[Fragment], start: usize, end: usize) -> RangeMeasure {
let mut m = RangeMeasure {
width: Pt::ZERO,
height: Pt::ZERO,
text_height: Pt::ZERO,
ascent: Pt::ZERO,
};
for frag in &fragments[start..end] {
m.width += frag.width();
m.height = m.height.max(frag.height());
if !matches!(frag, Fragment::Image { .. }) {
m.text_height = m.text_height.max(frag.height());
}
if let Fragment::Text { metrics, .. } = frag {
m.ascent = m.ascent.max(metrics.ascent);
}
}
m
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render::layout::fragment::{FontProps, TextMetrics};
use crate::render::resolve::color::RgbColor;
use std::rc::Rc;
fn text_frag(text: &str, width: f32) -> Fragment {
Fragment::Text {
text: text.into(),
font: FontProps {
family: Rc::from("Test"),
size: Pt::new(12.0),
bold: false,
italic: false,
underline: false,
char_spacing: Pt::ZERO,
underline_position: Pt::ZERO,
underline_thickness: Pt::ZERO,
},
color: RgbColor::BLACK,
width: Pt::new(width),
trimmed_width: Pt::new(width),
metrics: TextMetrics {
ascent: Pt::new(10.0),
descent: Pt::new(4.0),
},
hyperlink_url: None,
shading: None,
border: None,
baseline_offset: Pt::ZERO,
text_offset: Pt::ZERO,
}
}
#[test]
fn empty_fragments_no_lines() {
let lines = fit_lines(&[], Pt::new(100.0));
assert!(lines.is_empty());
}
#[test]
fn single_fragment_fits() {
let frags = vec![text_frag("hello", 30.0)];
let lines = fit_lines(&frags, Pt::new(100.0));
assert_eq!(lines.len(), 1);
assert_eq!(lines[0].start, 0);
assert_eq!(lines[0].end, 1);
assert_eq!(lines[0].width.raw(), 30.0);
assert_eq!(lines[0].height.raw(), 14.0);
}
#[test]
fn two_fragments_fit_on_one_line() {
let frags = vec![text_frag("hello ", 35.0), text_frag("world", 30.0)];
let lines = fit_lines(&frags, Pt::new(100.0));
assert_eq!(lines.len(), 1);
assert_eq!(lines[0].end, 2);
assert_eq!(lines[0].width.raw(), 65.0);
}
#[test]
fn overflow_breaks_at_boundary() {
let frags = vec![
text_frag("hello ", 60.0),
text_frag("world ", 60.0),
text_frag("end", 30.0),
];
let lines = fit_lines(&frags, Pt::new(100.0));
assert_eq!(lines.len(), 2);
assert_eq!(lines[0].start, 0);
assert_eq!(lines[0].end, 1); assert_eq!(lines[1].start, 1);
assert_eq!(lines[1].end, 3); }
#[test]
fn oversized_fragment_gets_own_line() {
let frags = vec![text_frag("verylongword", 200.0)];
let lines = fit_lines(&frags, Pt::new(100.0));
assert_eq!(lines.len(), 1, "oversized fragment still produces a line");
assert_eq!(lines[0].end, 1);
}
#[test]
fn line_break_forces_new_line() {
let frags = vec![
text_frag("before", 30.0),
Fragment::LineBreak {
line_height: Pt::new(14.0),
},
text_frag("after", 25.0),
];
let lines = fit_lines(&frags, Pt::new(100.0));
assert_eq!(lines.len(), 2);
assert_eq!(lines[0].end, 2); assert!(lines[0].has_break);
assert_eq!(lines[1].start, 2);
assert_eq!(lines[1].end, 3); }
#[test]
fn exact_fit_no_overflow() {
let frags = vec![text_frag("a", 50.0), text_frag("b", 50.0)];
let lines = fit_lines(&frags, Pt::new(100.0));
assert_eq!(lines.len(), 1);
assert_eq!(lines[0].width.raw(), 100.0);
}
#[test]
fn tab_uses_min_width_for_fitting() {
let frags = vec![
text_frag("text", 80.0),
Fragment::Tab {
line_height: Pt::new(14.0),
fitting_width: None,
},
text_frag("more", 30.0),
];
let lines = fit_lines(&frags, Pt::new(100.0));
assert_eq!(lines.len(), 2);
}
#[test]
fn height_is_max_of_fragments() {
let frags = vec![
Fragment::Text {
text: "small".into(),
font: FontProps {
family: Rc::from("Test"),
size: Pt::new(10.0),
bold: false,
italic: false,
underline: false,
char_spacing: Pt::ZERO,
underline_position: Pt::ZERO,
underline_thickness: Pt::ZERO,
},
color: RgbColor::BLACK,
width: Pt::new(20.0),
trimmed_width: Pt::new(20.0),
metrics: TextMetrics {
ascent: Pt::new(9.0),
descent: Pt::new(3.0),
},
hyperlink_url: None,
shading: None,
border: None,
baseline_offset: Pt::ZERO,
text_offset: Pt::ZERO,
},
Fragment::Text {
text: "big".into(),
font: FontProps {
family: Rc::from("Test"),
size: Pt::new(24.0),
bold: false,
italic: false,
underline: false,
char_spacing: Pt::ZERO,
underline_position: Pt::ZERO,
underline_thickness: Pt::ZERO,
},
color: RgbColor::BLACK,
width: Pt::new(30.0),
trimmed_width: Pt::new(30.0),
metrics: TextMetrics {
ascent: Pt::new(22.0),
descent: Pt::new(6.0),
},
hyperlink_url: None,
shading: None,
border: None,
baseline_offset: Pt::ZERO,
text_offset: Pt::ZERO,
},
];
let lines = fit_lines(&frags, Pt::new(100.0));
assert_eq!(lines.len(), 1);
assert_eq!(lines[0].height.raw(), 28.0, "max of 12 and 28");
assert_eq!(lines[0].ascent.raw(), 22.0, "max of 9 and 22");
}
#[test]
fn multiple_overflows_produce_multiple_lines() {
let frags = vec![
text_frag("a ", 40.0),
text_frag("b ", 40.0),
text_frag("c ", 40.0),
text_frag("d ", 40.0),
text_frag("e", 40.0),
];
let lines = fit_lines(&frags, Pt::new(70.0));
assert!(lines.len() >= 3, "should produce at least 3 lines");
}
}