use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Style;
use ratatui::text::{Line, Span};
pub fn wrap_spans(spans: &[Span<'_>], width: u16) -> Vec<Line<'static>> {
if width == 0 {
return Vec::new();
}
let width = width as usize;
let mut lines: Vec<Vec<Span<'static>>> = Vec::new();
let mut line: Vec<Span<'static>> = Vec::new();
let mut column = 0usize;
let mut separator: Option<Style> = None;
for span in spans {
let mut rest: &str = span.content.as_ref();
while !rest.is_empty() {
let gap = rest
.find(|c: char| !c.is_whitespace())
.unwrap_or(rest.len());
if gap > 0 {
for _ in 0..rest[..gap].matches('\n').count() {
lines.push(std::mem::take(&mut line));
column = 0;
}
separator = Some(span.style);
rest = &rest[gap..];
continue;
}
let end = rest.find(char::is_whitespace).unwrap_or(rest.len());
let (mut word, after) = rest.split_at(end);
rest = after;
while word.chars().count() > width {
if column > 0 {
lines.push(std::mem::take(&mut line));
column = 0;
}
let cut = word
.char_indices()
.nth(width)
.map_or(word.len(), |(index, _)| index);
lines.push(vec![Span::styled(word[..cut].to_string(), span.style)]);
word = &word[cut..];
}
let room = width - column;
let wanted = word.chars().count() + usize::from(column > 0);
if wanted > room && column > 0 {
lines.push(std::mem::take(&mut line));
column = 0;
}
if column > 0 {
line.push(Span::styled(" ", separator.unwrap_or(span.style)));
column += 1;
}
separator = None;
column += word.chars().count();
line.push(Span::styled(word.to_string(), span.style));
}
}
lines.push(line);
if lines.len() == 1 && lines[0].is_empty() {
return Vec::new();
}
lines.into_iter().map(Line::from).collect()
}
pub fn wrap(text: &str, width: u16) -> Vec<String> {
wrap_spans(&[Span::raw(text.to_string())], width)
.into_iter()
.map(|line| {
line.spans
.iter()
.map(|span| span.content.as_ref())
.collect()
})
.collect()
}
pub fn height(text: &str, width: u16) -> u16 {
u16::try_from(wrap(text, width).len()).unwrap_or(u16::MAX)
}
pub fn spans_height(spans: &[Span<'_>], width: u16) -> u16 {
u16::try_from(wrap_spans(spans, width).len()).unwrap_or(u16::MAX)
}
pub fn draw(text: &str, style: Style, area: Rect, buf: &mut Buffer) -> u16 {
let mut used = 0;
for line in wrap(text, area.width) {
if used >= area.height {
break;
}
buf.set_stringn(area.x, area.y + used, &line, area.width as usize, style);
used += 1;
}
used
}
pub fn draw_spans(spans: &[Span<'_>], area: Rect, buf: &mut Buffer) -> u16 {
let mut used = 0;
for line in wrap_spans(spans, area.width) {
if used >= area.height {
break;
}
let mut column = 0u16;
for span in &line.spans {
let room = area.width.saturating_sub(column) as usize;
if room == 0 {
break;
}
buf.set_stringn(
area.x + column,
area.y + used,
&span.content,
room,
span.style,
);
column += u16::try_from(span.content.chars().count().min(room)).unwrap_or(u16::MAX);
}
used += 1;
}
used
}
pub fn draw_line(line: &Line<'_>, area: Rect, buf: &mut Buffer) -> u16 {
draw_spans(&line.spans, area, buf)
}
pub fn line_height(line: &Line<'_>, width: u16) -> u16 {
spans_height(&line.spans, width)
}
#[cfg(test)]
mod tests {
use super::*;
use ratatui::style::Modifier;
#[test]
fn a_paragraph_wraps_on_words_and_counts_the_rows_it_took() {
assert_eq!(wrap("the quick brown fox", 10), ["the quick", "brown fox"]);
assert_eq!(height("the quick brown fox", 10), 2);
}
#[test]
fn nothing_to_say_costs_no_rows_rather_than_one_blank_one() {
assert_eq!(height("", 10), 0);
assert!(wrap("", 10).is_empty());
assert!(wrap("anything", 0).is_empty());
}
#[test]
fn an_authored_break_is_a_break() {
assert_eq!(wrap("one\ntwo", 20), ["one", "two"]);
}
#[test]
fn a_word_wider_than_the_region_is_cut_rather_than_overflowed() {
assert_eq!(
wrap("supercalifragilistic", 6),
["superc", "alifra", "gilist", "ic"]
);
}
#[test]
fn the_space_between_two_runs_belongs_to_the_run_that_held_it() {
let struck = Style::new().add_modifier(Modifier::CROSSED_OUT);
let spans = [Span::raw("lean "), Span::styled("gone", struck)];
let lines = wrap_spans(&spans, 20);
assert_eq!(lines.len(), 1);
let separator = lines[0]
.spans
.iter()
.find(|span| span.content.as_ref() == " ")
.expect("a separator between the two words");
assert!(!separator.style.add_modifier.contains(Modifier::CROSSED_OUT));
}
#[test]
fn a_drawing_stops_at_the_bottom_of_the_area_it_was_given() {
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 2));
let used = draw(
"the quick brown fox jumps over",
Style::new(),
buf.area,
&mut buf,
);
assert_eq!(used, 2);
}
}