use crate::color::Style;
use crate::text::{Line, Span};
use alloc::string::String;
use alloc::vec::Vec;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
pub(super) struct WrappedGlyph {
pub(super) grapheme: String,
pub(super) style: Style,
pub(super) width: u16,
}
pub(super) struct WrappedLine {
pub(super) glyphs: Vec<WrappedGlyph>,
pub(super) width: u16,
}
pub(super) fn wrap_line(line: &Line, max_width: u16) -> Vec<WrappedLine> {
let mut lines: Vec<WrappedLine> = alloc::vec![WrappedLine {
glyphs: Vec::new(),
width: 0,
}];
let mut col: u16 = 0;
for span in &line.spans {
for grapheme in span.content.graphemes(true) {
if grapheme == "\n" {
lines.push(WrappedLine {
glyphs: Vec::new(),
width: 0,
});
col = 0;
continue;
}
#[allow(clippy::cast_possible_truncation)]
let gw = grapheme.width() as u16;
if gw == 0 {
continue; }
if col + gw > max_width && col > 0 {
let current = lines.last_mut().expect("always at least one line");
if let Some(space_idx) = current.glyphs.iter().rposition(|g| g.grapheme == " ") {
let remainder: Vec<WrappedGlyph> =
current.glyphs.drain(space_idx + 1..).collect();
current.glyphs.pop();
current.width = current.glyphs.iter().map(|g| g.width).sum();
let new_width: u16 = remainder.iter().map(|g| g.width).sum();
col = new_width;
lines.push(WrappedLine {
glyphs: remainder,
width: new_width,
});
} else {
lines.push(WrappedLine {
glyphs: Vec::new(),
width: 0,
});
col = 0;
if grapheme == " " {
continue;
}
}
}
let current = lines.last_mut().expect("always at least one line");
current.width += gw;
current.glyphs.push(WrappedGlyph {
grapheme: String::from(grapheme),
style: span.style,
width: gw,
});
col += gw;
}
}
lines
}
#[must_use]
pub fn wrap(line: &Line, max_width: u16) -> Vec<Line> {
wrap_line(line, max_width)
.into_iter()
.map(|wrapped| {
let mut spans: Vec<Span> = Vec::new();
for glyph in wrapped.glyphs {
if let Some(last) = spans.last_mut()
&& last.style == glyph.style
{
last.content.push_str(&glyph.grapheme);
continue;
}
spans.push(Span::styled(glyph.grapheme, glyph.style));
}
Line { spans }
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::color::Color;
fn red() -> Style {
Style::new().fg(Color::RED)
}
#[test]
fn test_wrap_no_wrap_needed() {
let line = Line::raw("hello");
let lines = wrap_line(&line, 10);
assert_eq!(lines.len(), 1);
assert_eq!(lines[0].width, 5);
}
#[test]
fn test_wrap_hard_newline() {
let line = Line::raw("hi\nthere");
let lines = wrap_line(&line, 20);
assert_eq!(lines.len(), 2);
assert_eq!(lines[0].width, 2);
assert_eq!(lines[1].width, 5);
}
#[test]
fn test_wrap_soft_break_on_space() {
let line = Line::raw("hello world");
let lines = wrap_line(&line, 7);
assert_eq!(lines.len(), 2);
assert_eq!(lines[0].width, 5); assert_eq!(lines[1].width, 5); }
#[test]
fn test_wrap_force_break_no_space() {
let line = Line::raw("abcdefgh");
let lines = wrap_line(&line, 4);
assert_eq!(lines.len(), 2);
assert_eq!(lines[0].width, 4);
assert_eq!(lines[1].width, 4);
}
#[test]
fn test_wrap_force_break_drops_the_triggering_space() {
let line = Line::raw("abcd e");
let lines = wrap_line(&line, 4);
assert_eq!(lines.len(), 2);
assert_eq!(lines[0].width, 4);
assert_eq!(lines[1].width, 1); }
#[test]
fn test_wrap_wide_chars() {
let line = Line::raw("中文中");
let lines = wrap_line(&line, 4);
assert_eq!(lines.len(), 2);
assert_eq!(lines[0].width, 4);
assert_eq!(lines[1].width, 2);
}
#[test]
fn test_wrap_multi_span() {
let line = Line::from(vec![Span::raw("foo "), Span::styled("bar", red())]);
let lines = wrap_line(&line, 20);
assert_eq!(lines.len(), 1);
assert_eq!(lines[0].width, 7);
let bar_count = lines[0].glyphs.iter().filter(|g| g.style == red()).count();
assert_eq!(bar_count, 3);
}
}