use crate::color::Style;
use crate::text::{Line, Span};
use alloc::borrow::Cow;
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,
}];
for span in &line.spans {
for grapheme in span.content.graphemes(true) {
if grapheme == "\n" {
lines.push(WrappedLine {
glyphs: Vec::new(),
width: 0,
});
continue;
}
#[allow(clippy::cast_possible_truncation)]
let gw = grapheme.width() as u16;
if gw == 0 {
continue; }
let col = lines.last().expect("always at least one line").width;
if u32::from(col) + u32::from(gw) > u32::from(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();
let new_width: u16 = remainder.iter().map(|g| g.width).sum();
current.width -= new_width + 1;
lines.push(WrappedLine {
glyphs: remainder,
width: new_width,
});
} else {
lines.push(WrappedLine {
glyphs: Vec::new(),
width: 0,
});
if grapheme == " " {
continue;
}
}
}
let current = lines.last_mut().expect("always at least one line");
current.width = current.width.saturating_add(gw);
current.glyphs.push(WrappedGlyph {
grapheme: String::from(grapheme),
style: span.style,
width: 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()
}
#[must_use]
pub fn wrap_str(text: &str, max_width: u16) -> Vec<Cow<'_, str>> {
wrap_line(&Line::raw(text), max_width)
.into_iter()
.map(|wrapped| {
let mut row = String::new();
for glyph in wrapped.glyphs {
row.push_str(&glyph.grapheme);
}
Cow::Owned(row)
})
.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);
}
#[test]
fn test_wrap_str_soft_break_on_space() {
let rows = wrap_str("hello world", 7);
assert_eq!(rows, alloc::vec!["hello", "world"]);
}
#[test]
fn test_wrap_str_hard_newline() {
let rows = wrap_str("hi\nthere", 20);
assert_eq!(rows, alloc::vec!["hi", "there"]);
}
#[test]
fn test_wrap_str_wide_chars() {
let rows = wrap_str("中文中", 4);
assert_eq!(rows, alloc::vec!["中文", "中"]);
}
#[test]
fn test_wrap_str_no_wrap_needed_returns_owned() {
let rows = wrap_str("hello", 10);
assert_eq!(rows.len(), 1);
assert!(matches!(rows[0], Cow::Owned(_)));
}
#[test]
fn test_wrap_str_max_width_does_not_overflow() {
let text = "a".repeat(70_000);
let rows = wrap_str(&text, u16::MAX);
assert_eq!(rows.len(), 2);
assert_eq!(rows[0].len(), u16::MAX as usize);
}
}