use std::rc::Rc;
use super::{FontProps, Fragment, TextMetrics};
use crate::render::dimension::Pt;
pub type MeasureFn<'a> = Option<&'a dyn Fn(&str, &FontProps) -> (Pt, TextMetrics)>;
fn needs_split(fragment: &Fragment, max_width: Pt) -> bool {
matches!(
fragment,
Fragment::Text { width, text, .. } if *width > max_width && text.chars().count() > 1
)
}
pub fn split_oversized_fragments(
fragments: &[Fragment],
max_width: Pt,
measure: MeasureFn<'_>,
) -> Option<Vec<Fragment>> {
if max_width <= Pt::ZERO {
return None;
}
if !fragments.iter().any(|f| needs_split(f, max_width)) {
return None;
}
let mut result = Vec::with_capacity(fragments.len());
let mut ch_buf = [0u8; 4];
for frag in fragments {
let Fragment::Text {
text,
width,
font,
color,
shading,
border,
metrics,
hyperlink_url,
baseline_offset,
..
} = frag
else {
result.push(frag.clone());
continue;
};
if !needs_split(frag, max_width) {
result.push(frag.clone());
continue;
}
let char_count = text.chars().count();
let per_char_fallback = *width / char_count as f32;
for ch in text.chars() {
let ch_str = ch.encode_utf8(&mut ch_buf);
let (w, char_metrics) = match measure {
Some(m) => m(ch_str, font),
None => (per_char_fallback, *metrics),
};
result.push(Fragment::Text {
text: Rc::from(&*ch_str),
font: font.clone(),
color: *color,
shading: *shading,
border: *border,
width: w,
trimmed_width: w,
metrics: char_metrics,
hyperlink_url: hyperlink_url.clone(),
baseline_offset: *baseline_offset,
text_offset: Pt::ZERO,
is_footnote_ref: false,
});
}
}
Some(result)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render::resolve::color::RgbColor;
fn text_frag(text: &str, width: f32) -> Fragment {
Fragment::Text {
text: Rc::from(text),
font: Rc::new(FontProps {
family: Rc::from("Test"),
size: Pt::new(12.0),
bold: false,
italic: false,
underline: false,
char_spacing: Pt::ZERO,
text_scale: 1.0,
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),
leading: Pt::ZERO,
},
hyperlink_url: None,
shading: None,
border: None,
baseline_offset: Pt::ZERO,
text_offset: Pt::ZERO,
is_footnote_ref: false,
}
}
fn texts(frags: &[Fragment]) -> Vec<String> {
frags
.iter()
.map(|f| match f {
Fragment::Text { text, .. } => text.to_string(),
_ => panic!("expected Text fragment"),
})
.collect()
}
#[test]
fn splits_into_one_fragment_per_character() {
let frags = vec![text_frag("ab", 60.0)];
let result = split_oversized_fragments(&frags, Pt::new(20.0), None).expect("splits");
assert_eq!(
texts(&result),
["a", "b"],
"one fragment per character, in order"
);
for frag in &result {
let Fragment::Text { width, .. } = frag else {
unreachable!()
};
assert!((width.raw() - 30.0).abs() < 1e-4, "uniform fallback 60/2");
}
}
#[test]
fn measurer_supplies_per_character_widths() {
let measure = |t: &str, _: &FontProps| {
let w = if t == "w" { 40.0 } else { 10.0 };
(
Pt::new(w),
TextMetrics {
ascent: Pt::new(9.0),
descent: Pt::new(3.0),
leading: Pt::ZERO,
},
)
};
let frags = vec![text_frag("wi", 50.0)];
let result =
split_oversized_fragments(&frags, Pt::new(20.0), Some(&measure)).expect("splits");
let widths: Vec<f32> = result
.iter()
.map(|f| match f {
Fragment::Text { width, .. } => width.raw(),
_ => unreachable!(),
})
.collect();
assert_eq!(widths, [40.0, 10.0], "measured, not evenly divided");
let Fragment::Text { metrics, .. } = &result[0] else {
unreachable!()
};
assert_eq!(metrics.ascent.raw(), 9.0);
}
#[test]
fn nothing_to_split_returns_none() {
let frags = vec![text_frag("hi", 10.0)];
assert!(split_oversized_fragments(&frags, Pt::new(100.0), None).is_none());
}
#[test]
fn single_character_is_never_split() {
let frags = vec![text_frag("M", 200.0)];
assert!(split_oversized_fragments(&frags, Pt::new(10.0), None).is_none());
}
#[test]
fn multi_byte_single_character_is_never_split() {
for ch in ["é", "😀", "字"] {
let frags = vec![text_frag(ch, 200.0)];
assert!(
split_oversized_fragments(&frags, Pt::new(10.0), None).is_none(),
"{ch:?} is one character regardless of its byte length"
);
}
}
#[test]
fn non_positive_max_width_returns_none() {
let frags = vec![text_frag("ab", 60.0)];
assert!(split_oversized_fragments(&frags, Pt::ZERO, None).is_none());
assert!(split_oversized_fragments(&frags, Pt::new(-5.0), None).is_none());
}
#[test]
fn fragments_that_fit_pass_through_unchanged() {
let frags = vec![
text_frag("ab", 60.0), text_frag("ok", 5.0), ];
let result = split_oversized_fragments(&frags, Pt::new(20.0), None).expect("splits");
assert_eq!(texts(&result), ["a", "b", "ok"]);
}
}