use unicode_segmentation::UnicodeSegmentation;
use super::display_width;
pub(in crate::tui) fn wrap_line_at_whitespace(line: &str, width: usize) -> Vec<&str> {
soft_wrap_visible_ranges(line, wrap_line_at_whitespace_ranges(line, width))
.map(|range| &line[range])
.collect()
}
pub(in crate::tui) fn wrap_line_at_whitespace_ranges(
line: &str,
width: usize,
) -> Vec<std::ops::Range<usize>> {
wrap_line_at_whitespace_ranges_with_protected_prefix(line, width, 0)
}
pub(in crate::tui) fn wrap_line_at_whitespace_ranges_with_protected_prefix(
line: &str,
width: usize,
protected_prefix_end: usize,
) -> Vec<std::ops::Range<usize>> {
let width = width.max(1);
if line.is_empty() {
return std::iter::once(0..0).collect();
}
let mut ranges = Vec::new();
let mut start = 0;
while start < line.len() {
let mut count = 0usize;
let mut last_fitting_split = None;
let mut whitespace_break = None;
let mut saw_non_whitespace = false;
let mut overflow = false;
let mut prefer_width_split = false;
for (relative_index, grapheme) in line[start..].grapheme_indices(true) {
let grapheme_width = display_width(grapheme);
let is_whitespace = grapheme.chars().all(char::is_whitespace);
if count > 0 && count + grapheme_width > width {
overflow = true;
prefer_width_split = is_whitespace;
break;
}
count += grapheme_width;
let next = start + relative_index + grapheme.len();
last_fitting_split = Some(next);
if is_whitespace {
if saw_non_whitespace {
whitespace_break = Some(next);
}
} else {
saw_non_whitespace = true;
}
}
if !overflow {
ranges.push(start..line.len());
break;
}
let split = if prefer_width_split
|| (start == 0 && whitespace_break.is_some_and(|split| split <= protected_prefix_end))
{
last_fitting_split.expect("overflow requires a fitting split")
} else {
whitespace_break
.filter(|split| *split > start)
.unwrap_or_else(|| last_fitting_split.expect("overflow requires a fitting split"))
};
ranges.push(start..split);
start = split;
}
ranges
}
pub(in crate::tui) fn soft_wrap_visible_ranges<'a>(
line: &'a str,
ranges: impl IntoIterator<Item = std::ops::Range<usize>> + 'a,
) -> impl Iterator<Item = std::ops::Range<usize>> + 'a {
let mut prev_had_non_whitespace = false;
ranges.into_iter().filter_map(move |range| {
let end = range.end;
let mut start = range.start;
if prev_had_non_whitespace {
for grapheme in line[start..end].graphemes(true) {
if !grapheme.chars().all(char::is_whitespace) {
break;
}
start += grapheme.len();
}
if start >= end {
return None;
}
}
prev_had_non_whitespace = line[start..end].chars().any(|ch| !ch.is_whitespace());
Some(start..end)
})
}
pub(in crate::tui) fn hard_wrap_ranges(text: &str, width: usize) -> Vec<std::ops::Range<usize>> {
let width = width.max(1);
if text.is_empty() {
return vec![std::ops::Range { start: 0, end: 0 }];
}
let mut ranges = Vec::new();
let mut chunk_start = 0usize;
let mut current_width = 0usize;
for (offset, grapheme) in text.grapheme_indices(true) {
let grapheme_width = display_width(grapheme);
if current_width > 0 && current_width + grapheme_width > width {
ranges.push(chunk_start..offset);
chunk_start = offset;
current_width = 0;
}
current_width += grapheme_width;
if current_width >= width {
let end = offset + grapheme.len();
ranges.push(chunk_start..end);
chunk_start = end;
current_width = 0;
}
}
if chunk_start < text.len() {
ranges.push(chunk_start..text.len());
}
ranges
}
pub(in crate::tui) fn wrap_line_hard(line: &str, width: usize) -> Vec<&str> {
hard_wrap_ranges(line, width)
.into_iter()
.map(|range| &line[range])
.collect()
}
#[cfg(test)]
#[path = "wrapping_tests.rs"]
mod tests;