pub fn wrap_text(text: &str, max_width: f64, max_lines: usize, measure: impl Fn(&str) -> f64) -> Vec<String> {
if max_lines == 0 {
return Vec::new();
}
let mut lines: Vec<String> = Vec::new();
let mut current = String::new();
for word in text.split_whitespace() {
let candidate = if current.is_empty() { word.to_owned() } else { format!("{current} {word}") };
if current.is_empty() || measure(&candidate) <= max_width {
current = candidate;
} else {
lines.push(std::mem::replace(&mut current, word.to_owned()));
}
}
if !current.is_empty() {
lines.push(current);
}
if lines.len() > max_lines {
lines.truncate(max_lines);
if let Some(last) = lines.last_mut() {
*last = force_ellipsis(last, max_width, &measure);
}
}
lines
}
pub fn truncate_ellipsis(text: &str, max_width: f64, measure: impl Fn(&str) -> f64) -> String {
if measure(text) <= max_width {
return text.to_owned();
}
force_ellipsis(text, max_width, measure)
}
fn force_ellipsis(text: &str, max_width: f64, measure: impl Fn(&str) -> f64) -> String {
const ELLIPSIS: &str = "…";
let chars: Vec<char> = text.chars().collect();
for len in (0..=chars.len()).rev() {
let candidate: String = chars[..len].iter().collect::<String>() + ELLIPSIS;
if len == 0 || measure(&candidate) <= max_width {
return candidate;
}
}
ELLIPSIS.to_owned()
}
#[cfg(test)]
mod tests {
use super::*;
fn char_width(s: &str) -> f64 {
s.chars().count() as f64
}
#[test]
fn wrap_text_keeps_short_text_on_one_line() {
let lines = wrap_text("short summary", 40.0, 3, char_width);
assert_eq!(lines, vec!["short summary".to_owned()]);
}
#[test]
fn wrap_text_breaks_at_word_boundaries_when_a_line_would_overflow() {
let lines = wrap_text("the quick brown fox jumps", 10.0, 10, char_width);
for line in &lines {
assert!(char_width(line) <= 10.0, "line {line:?} exceeds max_width");
}
let rejoined: Vec<&str> = lines.iter().flat_map(|l| l.split_whitespace()).collect();
assert_eq!(rejoined, vec!["the", "quick", "brown", "fox", "jumps"]);
}
#[test]
fn wrap_text_never_splits_a_single_word_wider_than_max_width() {
let lines = wrap_text("supercalifragilisticexpialidocious short", 10.0, 10, char_width);
assert_eq!(lines[0], "supercalifragilisticexpialidocious");
}
#[test]
fn wrap_text_ellipsis_truncates_the_last_line_when_it_exceeds_max_lines() {
let lines = wrap_text("one two three four five six seven eight", 6.0, 2, char_width);
assert_eq!(lines.len(), 2);
assert!(lines[1].ends_with('…'), "overflow must be marked with a trailing ellipsis, got {:?}", lines[1]);
}
#[test]
fn wrap_text_returns_empty_for_blank_input_or_zero_max_lines() {
assert!(wrap_text("", 40.0, 3, char_width).is_empty());
assert!(wrap_text(" ", 40.0, 3, char_width).is_empty());
assert!(wrap_text("some text", 40.0, 0, char_width).is_empty());
}
#[test]
fn wrap_text_is_deterministic() {
let a = wrap_text("alpha beta gamma delta epsilon", 12.0, 4, char_width);
let b = wrap_text("alpha beta gamma delta epsilon", 12.0, 4, char_width);
assert_eq!(a, b);
}
#[test]
fn truncate_ellipsis_leaves_text_that_already_fits_unchanged() {
assert_eq!(truncate_ellipsis("fits fine", 40.0, char_width), "fits fine");
}
#[test]
fn truncate_ellipsis_shortens_and_appends_an_ellipsis_when_it_overflows() {
let result = truncate_ellipsis("a rather long piece of text", 10.0, char_width);
assert!(result.ends_with('…'));
assert!(char_width(&result) <= 10.0);
}
#[test]
fn truncate_ellipsis_degenerates_to_a_bare_ellipsis_when_max_width_cannot_fit_anything_else() {
let result = truncate_ellipsis("anything", 0.5, char_width);
assert_eq!(result, "…");
}
#[test]
fn truncate_ellipsis_handles_empty_input_without_panicking() {
assert_eq!(truncate_ellipsis("", 10.0, char_width), "");
}
}