use unicode_width::UnicodeWidthChar;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Wrap {
#[default]
None,
Char,
Word,
}
pub fn wrap_segments(line: &str, width: u16, mode: Wrap) -> Vec<(usize, usize)> {
let total = line.chars().count();
if matches!(mode, Wrap::None) || width == 0 || line.is_empty() {
return vec![(0, total)];
}
let chars: Vec<(char, u16)> = line
.chars()
.map(|c| (c, c.width().unwrap_or(1).max(1) as u16))
.collect();
let mut segs = Vec::new();
let mut start = 0usize;
while start < total {
let mut cells: u16 = 0;
let mut i = start;
while i < total {
let w = chars[i].1;
if cells + w > width {
break;
}
cells += w;
i += 1;
}
if i == total {
segs.push((start, total));
break;
}
let break_at = if matches!(mode, Wrap::Word) {
(start..i)
.rev()
.find(|&k| chars[k].0.is_whitespace())
.map(|k| k + 1)
.filter(|&end| end > start)
.unwrap_or(i)
} else {
i
};
segs.push((start, break_at));
start = break_at;
}
if segs.is_empty() {
segs.push((0, 0));
}
segs
}
pub fn segment_for_col(segments: &[(usize, usize)], col: usize) -> usize {
if segments.is_empty() {
return 0;
}
if let Some(idx) = segments.iter().position(|&(s, e)| col >= s && col < e) {
return idx;
}
segments.len() - 1
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn none_returns_full_line_segment() {
let segs = wrap_segments("hello world", 4, Wrap::None);
assert_eq!(segs, vec![(0, 11)]);
}
#[test]
fn segment_for_col_finds_containing_segment() {
let segs = vec![(0, 4), (4, 8), (8, 10)];
assert_eq!(segment_for_col(&segs, 0), 0);
assert_eq!(segment_for_col(&segs, 3), 0);
assert_eq!(segment_for_col(&segs, 4), 1);
assert_eq!(segment_for_col(&segs, 7), 1);
assert_eq!(segment_for_col(&segs, 9), 2);
assert_eq!(segment_for_col(&segs, 10), 2);
assert_eq!(segment_for_col(&segs, 99), 2);
}
}