chore(wrap): implement trim for CharWrapper and add some unit tests

This commit is contained in:
Eyesonjune18 2023-05-31 00:53:36 -07:00
parent b0b1ac85b5
commit 7908e944f6
3 changed files with 135 additions and 7 deletions

View file

@ -340,7 +340,7 @@ mod test {
Paragraph::new(text).block(Block::default().title("Title").borders(Borders::ALL));
let char_wrapped_paragraph = truncated_paragraph
.clone()
.wrap(Wrap::WordBoundary)
.wrap(Wrap::CharBoundary)
.trim(false);
let word_wrapped_paragraph = truncated_paragraph
.clone()
@ -394,6 +394,15 @@ mod test {
"└───────────┘",
]),
);
test_case(
&char_wrapped_paragraph,
Buffer::with_lines(vec![
"┌Title──────┐",
"│Hello, worl│",
"│d! │",
"└───────────┘",
]),
);
test_case(
&word_wrapped_paragraph,
Buffer::with_lines(vec![

View file

@ -7,8 +7,14 @@ use unicode_width::UnicodeWidthStr;
use crate::layout::Alignment;
use crate::text::StyledGrapheme;
// NBSP is a non-breaking space which is essentially a whitespace character that is treated
// the same as non-whitespace characters in wrapping algorithms
const NBSP: &str = "\u{00a0}";
fn is_whitespace(symbol: &str) -> bool {
symbol.chars().all(&char::is_whitespace) && symbol != NBSP
}
/// A state machine to pack styled symbols into lines.
/// Cannot implement it as Iterator since it yields slices of the internal buffer (need streaming
/// iterators for that).
@ -59,6 +65,8 @@ where
let mut current_line = vec![];
let mut current_line_width = 0;
let mut has_encountered_non_whitespace_this_line = false;
// Iterate over all characters in the line
for StyledGrapheme { symbol, style } in line {
let symbol_width = symbol.width() as u16;
@ -67,8 +75,20 @@ where
continue;
}
// If the current line is not empty, we need to check if the current character fits
// into the current line
let symbol_whitespace = is_whitespace(symbol);
// If the current character is whitespace and no non-whitespace character has been
// encountered yet on this line, skip it
if self.trim && !has_encountered_non_whitespace_this_line {
if symbol_whitespace {
continue;
} else {
has_encountered_non_whitespace_this_line = true;
}
}
// If the current line is not empty, we need to check if the current character
// fits into the current line
if current_line_width + symbol_width <= self.max_line_width {
// If it fits, add it to the current line
current_line.push(StyledGrapheme { symbol, style });
@ -76,7 +96,16 @@ where
} else {
// If it doesn't fit, wrap the current line and start a new one
wrapped_lines.push(current_line);
current_line = vec![StyledGrapheme { symbol, style }];
current_line = vec![];
// If the wrapped symbol is whitespace, start trimming whitespace
if self.trim && symbol_whitespace {
has_encountered_non_whitespace_this_line = false;
current_line_width = 0;
continue;
}
current_line.push(StyledGrapheme { symbol, style });
current_line_width = symbol_width;
}
}
@ -236,8 +265,7 @@ where
let mut has_seen_non_whitespace = false;
for StyledGrapheme { symbol, style } in line_symbols {
let symbol_whitespace =
symbol.chars().all(&char::is_whitespace) && symbol != NBSP;
let symbol_whitespace = is_whitespace(symbol);
let symbol_width = symbol.width() as u16;
// Ignore characters wider than the total max width
if symbol_width > self.max_line_width {

View file

@ -144,7 +144,63 @@ const SAMPLE_STRING: &str = "The library is based on the principle of immediate
interactive UI, this may introduce overhead for highly dynamic content.";
#[test]
fn widgets_paragraph_can_wrap_its_content() {
fn widgets_paragraph_can_char_wrap_its_content() {
let text = vec![Line::from(SAMPLE_STRING)];
let paragraph = Paragraph::new(text)
.block(Block::default().borders(Borders::ALL))
.wrap(Wrap::CharBoundary)
.trim(true);
// If char wrapping is used, all alignments should be the same except on the last line.
test_case(
paragraph.clone().alignment(Alignment::Left),
Buffer::with_lines(vec![
"┌──────────────────┐",
"│The library is bas│",
"│ed on the principl│",
"│e of immediate ren│",
"│dering with interm│",
"│ediate buffers. Th│",
"│is means that at e│",
"│ach new frame you │",
"│should build all w│",
"└──────────────────┘",
]),
);
test_case(
paragraph.clone().alignment(Alignment::Center),
Buffer::with_lines(vec![
"┌──────────────────┐",
"│The library is bas│",
"│ed on the principl│",
"│e of immediate ren│",
"│dering with interm│",
"│ediate buffers. Th│",
"│is means that at e│",
"│ach new frame you │",
"│should build all w│",
"└──────────────────┘",
]),
);
test_case(
paragraph.clone().alignment(Alignment::Right),
Buffer::with_lines(vec![
"┌──────────────────┐",
"│The library is bas│",
"│ed on the principl│",
"│e of immediate ren│",
"│dering with interm│",
"│ediate buffers. Th│",
"│is means that at e│",
"│ach new frame you │",
"│should build all w│",
"└──────────────────┘",
]),
);
}
#[test]
fn widgets_paragraph_can_word_wrap_its_content() {
let text = vec![Line::from(SAMPLE_STRING)];
let paragraph = Paragraph::new(text)
.block(Block::default().borders(Borders::ALL))
@ -198,6 +254,41 @@ fn widgets_paragraph_can_wrap_its_content() {
);
}
#[test]
fn widgets_paragraph_can_trim_its_content() {
let space_text = "This is some text with an excessive amount of whitespace between words.";
let text = vec![Line::from(space_text)];
let paragraph = Paragraph::new(text)
.block(Block::default().borders(Borders::ALL))
.wrap(Wrap::CharBoundary);
test_case(
paragraph.clone().alignment(Alignment::Left).trim(true),
Buffer::with_lines(vec![
"┌──────────────────┐",
"│This is some │",
"│text with an exces│",
"│sive amount │",
"│of whitespace │",
"│between words. │",
"└──────────────────┘",
]),
);
test_case(
paragraph.clone().alignment(Alignment::Left).trim(false),
Buffer::with_lines(vec![
"┌──────────────────┐",
"│This is some │",
"│ text with an ex│",
"│cessive amou│",
"│nt of whitespace │",
"│ be│",
"│tween words. │",
"└──────────────────┘",
]),
);
}
#[test]
fn widgets_paragraph_works_with_padding() {
let text = vec![Line::from(SAMPLE_STRING)];