pub(crate) fn wrap_line(text: &str, width: usize) -> Vec<String> {
if width == 0 {
return vec![String::new()];
}
let chars: Vec<char> = text.chars().collect();
let n = chars.len();
let mut out: Vec<String> = Vec::new();
let mut line: Vec<char> = Vec::new();
let mut i = 0;
while i < n {
if chars[i].is_whitespace() {
let start = i;
while i < n && chars[i].is_whitespace() {
i += 1;
}
let ws = &chars[start..i];
if line.len() + ws.len() <= width {
line.extend_from_slice(ws);
} else {
let fits = width.saturating_sub(line.len());
if fits > 0 {
line.extend_from_slice(&ws[..fits.min(ws.len())]);
}
out.push(take_line(&mut line));
}
continue;
}
let start = i;
while i < n && !chars[i].is_whitespace() {
i += 1;
}
let word = &chars[start..i];
if word.len() > width {
hard_break(word, width, &mut out, &mut line);
continue;
}
if line.len() + word.len() <= width {
line.extend_from_slice(word);
} else {
out.push(take_line(&mut line));
line.extend_from_slice(word);
}
}
out.push(take_line(&mut line));
out
}
fn hard_break(word: &[char], width: usize, out: &mut Vec<String>, line: &mut Vec<char>) {
let mut k = 0;
while k < word.len() {
let take = width.min(word.len() - k);
if line.len() + take <= width {
line.extend_from_slice(&word[k..k + take]);
} else {
out.push(take_line(line));
line.extend_from_slice(&word[k..k + take]);
}
k += take;
}
}
fn take_line(line: &mut Vec<char>) -> String {
while line.last().is_some_and(|c| c.is_whitespace()) {
line.pop();
}
line.drain(..).collect()
}
pub(crate) fn visual_row_count(text: &str, width: usize) -> usize {
if width == 0 {
return 1;
}
text.split('\n')
.map(|line| wrap_line(line, width).len())
.sum::<usize>()
.max(1)
}
#[cfg(test)]
mod wrap_tests;