Compare commits

...

4 Commits

6 changed files with 626 additions and 89 deletions

View File

@@ -293,7 +293,10 @@ where
.fg(Color::Magenta)
.add_modifier(Modifier::BOLD),
));
let paragraph = Paragraph::new(text).block(block).wrap(Wrap { trim: true });
let paragraph = Paragraph::new(text)
.block(block)
.wrap(Wrap::WordBoundary)
.trim(true);
f.render_widget(paragraph, area);
}

View File

@@ -160,21 +160,24 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
let paragraph = Paragraph::new(text.clone())
.style(Style::default().fg(Color::Gray))
.block(create_block("Default alignment (Left), with wrap"))
.wrap(Wrap { trim: true });
.wrap(Wrap::WordBoundary)
.trim(true);
f.render_widget(paragraph, chunks[1]);
let paragraph = Paragraph::new(text.clone())
.style(Style::default().fg(Color::Gray))
.block(create_block("Right alignment, with wrap"))
.alignment(Alignment::Right)
.wrap(Wrap { trim: true });
.wrap(Wrap::WordBoundary)
.trim(true);
f.render_widget(paragraph, chunks[2]);
let paragraph = Paragraph::new(text)
.style(Style::default().fg(Color::Gray))
.block(create_block("Center alignment, with wrap, with scroll"))
.alignment(Alignment::Center)
.wrap(Wrap { trim: true })
.wrap(Wrap::WordBoundary)
.trim(true)
.scroll((app.scroll, 0));
f.render_widget(paragraph, chunks[3]);
}

View File

@@ -85,7 +85,8 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
Style::default().add_modifier(Modifier::SLOW_BLINK),
))
.alignment(Alignment::Center)
.wrap(Wrap { trim: true });
.wrap(Wrap::WordBoundary)
.trim(true);
f.render_widget(paragraph, chunks[0]);
let block = Block::default()

View File

@@ -6,7 +6,7 @@ use crate::{
style::Style,
text::{StyledGrapheme, Text},
widgets::{
reflow::{LineComposer, LineTruncator, WordWrapper},
reflow::{CharWrapper, LineComposer, LineTruncator, WordWrapper},
Block, Widget,
},
};
@@ -40,7 +40,7 @@ fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment)
/// .block(Block::default().title("Paragraph").borders(Borders::ALL))
/// .style(Style::default().fg(Color::White).bg(Color::Black))
/// .alignment(Alignment::Center)
/// .wrap(Wrap { trim: true });
/// .wrap(Wrap::WordBoundary).trim(true);
/// ```
#[derive(Debug, Clone)]
pub struct Paragraph<'a> {
@@ -50,6 +50,8 @@ pub struct Paragraph<'a> {
style: Style,
/// How to wrap the text
wrap: Option<Wrap>,
/// Whether leading whitespace should be trimmed
trim: bool,
/// The text to display
text: Text<'a>,
/// Scroll
@@ -69,16 +71,16 @@ pub struct Paragraph<'a> {
/// - First thing goes here and is long so that it wraps
/// - Here is another point that is long enough to wrap"#);
///
/// // With leading spaces trimmed (window width of 30 chars):
/// Paragraph::new(bullet_points.clone()).wrap(Wrap { trim: true });
/// // Wrapping on char boundaries (window width of 30 chars):
/// Paragraph::new(bullet_points.clone()).wrap(Wrap::CharBoundary);
/// // Some indented points:
/// // - First thing goes here and is
/// // long so that it wraps
/// // - Here is another point that
/// // is long enough to wrap
/// // - First thing goes here an
/// // d is long so that it wraps
/// // - Here is another point th
/// // at is long enough to wrap
///
/// // But without trimming, indentation is preserved:
/// Paragraph::new(bullet_points).wrap(Wrap { trim: false });
/// // Wrapping on word boundaries
/// Paragraph::new(bullet_points).wrap(Wrap::WordBoundary);
/// // Some indented points:
/// // - First thing goes here
/// // and is long so that it wraps
@@ -86,9 +88,9 @@ pub struct Paragraph<'a> {
/// // that is long enough to wrap
/// ```
#[derive(Debug, Clone, Copy)]
pub struct Wrap {
/// Should leading whitespace be trimmed
pub trim: bool,
pub enum Wrap {
WordBoundary,
CharBoundary,
}
impl<'a> Paragraph<'a> {
@@ -100,6 +102,7 @@ impl<'a> Paragraph<'a> {
block: None,
style: Style::default(),
wrap: None,
trim: false,
text: text.into(),
scroll: (0, 0),
alignment: Alignment::Left,
@@ -121,6 +124,11 @@ impl<'a> Paragraph<'a> {
self
}
pub fn trim(mut self, trim: bool) -> Paragraph<'a> {
self.trim = trim;
self
}
pub fn scroll(mut self, offset: (u16, u16)) -> Paragraph<'a> {
self.scroll = offset;
self
@@ -158,13 +166,20 @@ impl<'a> Widget for Paragraph<'a> {
)
});
let mut line_composer: Box<dyn LineComposer> = if let Some(Wrap { trim }) = self.wrap {
Box::new(WordWrapper::new(styled, text_area.width, trim))
} else {
let mut line_composer = Box::new(LineTruncator::new(styled, text_area.width));
line_composer.set_horizontal_offset(self.scroll.1);
line_composer
let mut line_composer: Box<dyn LineComposer> = match self.wrap {
Some(Wrap::CharBoundary) => {
Box::new(CharWrapper::new(styled, text_area.width, self.trim))
}
Some(Wrap::WordBoundary) => {
Box::new(WordWrapper::new(styled, text_area.width, self.trim))
}
None => {
let mut line_composer = Box::new(LineTruncator::new(styled, text_area.width));
line_composer.set_horizontal_offset(self.scroll.1);
line_composer
}
};
let mut y = 0;
while let Some((current_line, current_line_width, current_line_alignment)) =
line_composer.next_line()
@@ -231,8 +246,8 @@ mod test {
let line = "foo\0";
let paragraphs = vec![
Paragraph::new(line),
Paragraph::new(line).wrap(Wrap { trim: false }),
Paragraph::new(line).wrap(Wrap { trim: true }),
Paragraph::new(line).wrap(Wrap::WordBoundary).trim(false),
Paragraph::new(line).wrap(Wrap::WordBoundary).trim(true),
];
for paragraph in paragraphs {
@@ -247,8 +262,8 @@ mod test {
fn test_render_empty_paragraph() {
let paragraphs = vec![
Paragraph::new(""),
Paragraph::new("").wrap(Wrap { trim: false }),
Paragraph::new("").wrap(Wrap { trim: true }),
Paragraph::new("").wrap(Wrap::WordBoundary).trim(false),
Paragraph::new("").wrap(Wrap::WordBoundary).trim(true),
];
for paragraph in paragraphs {
@@ -263,8 +278,8 @@ mod test {
fn test_render_single_line_paragraph() {
let text = "Hello, world!";
let truncated_paragraph = Paragraph::new(text);
let wrapped_paragraph = Paragraph::new(text).wrap(Wrap { trim: false });
let trimmed_paragraph = Paragraph::new(text).wrap(Wrap { trim: true });
let wrapped_paragraph = Paragraph::new(text).wrap(Wrap::WordBoundary).trim(false);
let trimmed_paragraph = Paragraph::new(text).wrap(Wrap::WordBoundary).trim(true);
let paragraphs = vec![&truncated_paragraph, &wrapped_paragraph, &trimmed_paragraph];
@@ -288,8 +303,8 @@ mod test {
let paragraphs = vec![
Paragraph::new(text),
Paragraph::new(text).wrap(Wrap { trim: false }),
Paragraph::new(text).wrap(Wrap { trim: true }),
Paragraph::new(text).wrap(Wrap::WordBoundary).trim(false),
Paragraph::new(text).wrap(Wrap::WordBoundary).trim(true),
];
for paragraph in paragraphs {
@@ -323,10 +338,24 @@ mod test {
let text = "Hello, world!";
let truncated_paragraph =
Paragraph::new(text).block(Block::default().title("Title").borders(Borders::ALL));
let wrapped_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: false });
let trimmed_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: true });
let char_wrapped_paragraph = truncated_paragraph
.clone()
.wrap(Wrap::CharBoundary)
.trim(false);
let word_wrapped_paragraph = truncated_paragraph
.clone()
.wrap(Wrap::WordBoundary)
.trim(false);
let trimmed_paragraph = truncated_paragraph
.clone()
.wrap(Wrap::WordBoundary)
.trim(true);
let paragraphs = vec![&truncated_paragraph, &wrapped_paragraph, &trimmed_paragraph];
let paragraphs = vec![
&truncated_paragraph,
&word_wrapped_paragraph,
&trimmed_paragraph,
];
for paragraph in paragraphs {
test_case(
@@ -366,7 +395,16 @@ mod test {
]),
);
test_case(
&wrapped_paragraph,
&char_wrapped_paragraph,
Buffer::with_lines(vec![
"┌Title──────┐",
"│Hello, worl│",
"│d! │",
"└───────────┘",
]),
);
test_case(
&word_wrapped_paragraph,
Buffer::with_lines(vec![
"┌Title──────┐",
"│Hello, │",
@@ -388,8 +426,8 @@ mod test {
#[test]
fn test_render_paragraph_with_word_wrap() {
let text = "This is a long line of text that should wrap and contains a superultramegagigalong word.";
let wrapped_paragraph = Paragraph::new(text).wrap(Wrap { trim: false });
let trimmed_paragraph = Paragraph::new(text).wrap(Wrap { trim: true });
let wrapped_paragraph = Paragraph::new(text).wrap(Wrap::WordBoundary).trim(false);
let trimmed_paragraph = Paragraph::new(text).wrap(Wrap::WordBoundary).trim(true);
test_case(
&wrapped_paragraph,
@@ -471,8 +509,14 @@ mod test {
fn test_render_paragraph_with_left_alignment() {
let text = "Hello, world!";
let truncated_paragraph = Paragraph::new(text).alignment(Alignment::Left);
let wrapped_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: false });
let trimmed_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: true });
let wrapped_paragraph = truncated_paragraph
.clone()
.wrap(Wrap::WordBoundary)
.trim(false);
let trimmed_paragraph = truncated_paragraph
.clone()
.wrap(Wrap::WordBoundary)
.trim(true);
let paragraphs = vec![&truncated_paragraph, &wrapped_paragraph, &trimmed_paragraph];
@@ -496,8 +540,14 @@ mod test {
fn test_render_paragraph_with_center_alignment() {
let text = "Hello, world!";
let truncated_paragraph = Paragraph::new(text).alignment(Alignment::Center);
let wrapped_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: false });
let trimmed_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: true });
let wrapped_paragraph = truncated_paragraph
.clone()
.wrap(Wrap::WordBoundary)
.trim(false);
let trimmed_paragraph = truncated_paragraph
.clone()
.wrap(Wrap::WordBoundary)
.trim(true);
let paragraphs = vec![&truncated_paragraph, &wrapped_paragraph, &trimmed_paragraph];
@@ -523,8 +573,14 @@ mod test {
fn test_render_paragraph_with_right_alignment() {
let text = "Hello, world!";
let truncated_paragraph = Paragraph::new(text).alignment(Alignment::Right);
let wrapped_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: false });
let trimmed_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: true });
let wrapped_paragraph = truncated_paragraph
.clone()
.wrap(Wrap::WordBoundary)
.trim(false);
let trimmed_paragraph = truncated_paragraph
.clone()
.wrap(Wrap::WordBoundary)
.trim(true);
let paragraphs = vec![&truncated_paragraph, &wrapped_paragraph, &trimmed_paragraph];
@@ -548,8 +604,14 @@ mod test {
fn test_render_paragraph_with_scroll_offset() {
let text = "This is a\ncool\nmultiline\nparagraph.";
let truncated_paragraph = Paragraph::new(text).scroll((2, 0));
let wrapped_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: false });
let trimmed_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: true });
let wrapped_paragraph = truncated_paragraph
.clone()
.wrap(Wrap::WordBoundary)
.trim(false);
let trimmed_paragraph = truncated_paragraph
.clone()
.wrap(Wrap::WordBoundary)
.trim(true);
let paragraphs = vec![&truncated_paragraph, &wrapped_paragraph, &trimmed_paragraph];
@@ -577,8 +639,8 @@ mod test {
let paragraphs = vec![
Paragraph::new(text),
Paragraph::new(text).wrap(Wrap { trim: false }),
Paragraph::new(text).wrap(Wrap { trim: true }),
Paragraph::new(text).wrap(Wrap::WordBoundary).trim(false),
Paragraph::new(text).wrap(Wrap::WordBoundary).trim(true),
];
let area = Rect::new(0, 0, 0, 3);
@@ -594,8 +656,8 @@ mod test {
let paragraphs = vec![
Paragraph::new(text),
Paragraph::new(text).wrap(Wrap { trim: false }),
Paragraph::new(text).wrap(Wrap { trim: true }),
Paragraph::new(text).wrap(Wrap::WordBoundary).trim(false),
Paragraph::new(text).wrap(Wrap::WordBoundary).trim(true),
];
let area = Rect::new(0, 0, 10, 0);
@@ -614,8 +676,12 @@ mod test {
let paragraphs = vec![
Paragraph::new(text.clone()),
Paragraph::new(text.clone()).wrap(Wrap { trim: false }),
Paragraph::new(text.clone()).wrap(Wrap { trim: true }),
Paragraph::new(text.clone())
.wrap(Wrap::WordBoundary)
.trim(false),
Paragraph::new(text.clone())
.wrap(Wrap::WordBoundary)
.trim(true),
];
let mut expected_buffer = Buffer::with_lines(vec!["Hello, world!"]);
@@ -640,8 +706,8 @@ mod test {
let text = "Hello, <world>!";
let paragraphs = vec![
Paragraph::new(text),
Paragraph::new(text).wrap(Wrap { trim: false }),
Paragraph::new(text).wrap(Wrap { trim: true }),
Paragraph::new(text).wrap(Wrap::WordBoundary).trim(false),
Paragraph::new(text).wrap(Wrap::WordBoundary).trim(true),
];
for paragraph in paragraphs {
@@ -662,8 +728,8 @@ mod test {
fn test_render_paragraph_with_unicode_characters() {
let text = "こんにちは, 世界! 😃";
let truncated_paragraph = Paragraph::new(text);
let wrapped_paragraph = Paragraph::new(text).wrap(Wrap { trim: false });
let trimmed_paragraph = Paragraph::new(text).wrap(Wrap { trim: true });
let wrapped_paragraph = Paragraph::new(text).wrap(Wrap::WordBoundary).trim(false);
let trimmed_paragraph = Paragraph::new(text).wrap(Wrap::WordBoundary).trim(true);
let paragraphs = vec![&truncated_paragraph, &wrapped_paragraph, &trimmed_paragraph];

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).
@@ -16,6 +22,175 @@ pub trait LineComposer<'a> {
fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16, Alignment)>;
}
/// A state machine that wraps lines on char boundaries.
pub struct CharWrapper<'a, O, I>
where
O: Iterator<Item = (I, Alignment)>, // Outer iterator providing the individual lines
I: Iterator<Item = StyledGrapheme<'a>>, // Inner iterator providing the styled symbols of a line
// Each line consists of an alignment and a series of symbols
{
/// The given, unprocessed lines
input_lines: O,
max_line_width: u16,
wrapped_lines_buffer: Option<IntoIter<Vec<StyledGrapheme<'a>>>>,
current_alignment: Alignment,
current_line: Vec<StyledGrapheme<'a>>,
/// Removes the leading whitespace from lines
trim: bool,
}
impl<'a, O, I> CharWrapper<'a, O, I>
where
O: Iterator<Item = (I, Alignment)>,
I: Iterator<Item = StyledGrapheme<'a>>,
{
pub fn new(lines: O, max_line_width: u16, trim: bool) -> CharWrapper<'a, O, I> {
CharWrapper {
input_lines: lines,
max_line_width,
wrapped_lines_buffer: None,
current_alignment: Alignment::Left,
current_line: vec![],
trim,
}
}
/// Wraps a given line (which is represented as an iterator over characters) into multiple
/// parts according to this `CharWrapper`'s configuration.
///
/// The parts are represented as a list.
///
fn wrap_line(&self, line: &mut I) -> Vec<Vec<StyledGrapheme<'a>>> {
let mut wrapped_lines = vec![];
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;
// Ignore characters wider than the total max width
if symbol_width > self.max_line_width {
continue;
}
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 });
current_line_width += symbol_width;
} else {
// If it doesn't fit, wrap the current line and start a new one
wrapped_lines.push(current_line);
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;
}
}
if !current_line.is_empty() {
// Append the rest of current line to the wrapped lines
wrapped_lines.push(current_line);
}
if wrapped_lines.is_empty() {
// Append empty line if there was nothing to wrap in the first place
wrapped_lines.push(vec![]);
}
wrapped_lines
}
/// Returns the next wrapped line and its length currently in the wrapped lines buffer
fn next_wrapped_line(&mut self) -> Option<(Vec<StyledGrapheme<'a>>, u16)> {
if let Some(line_iterator) = &mut self.wrapped_lines_buffer {
if let Some(line) = line_iterator.next() {
let line_width = line
.iter()
.map(|grapheme| grapheme.symbol.width())
.sum::<usize>() as u16;
return Some((line, line_width));
}
}
None
}
}
impl<'a, O, I> LineComposer<'a> for CharWrapper<'a, O, I>
where
O: Iterator<Item = (I, Alignment)>,
I: Iterator<Item = StyledGrapheme<'a>>,
{
/// This function returns the next line based on its input lines by wrapping them so that
/// words get wrapped at the point where they intersect with the border of the widget.
///
/// ### Implementation details:
/// The `CharWrapper` uses an internal buffer (`wrapped_lines_buffer`) that holds all the
/// wrapped parts of a single line from the input (`input_lines`). The individual parts are
/// returned by invoking this method.
///
/// Once the buffer is empty, the next line from the input is split into wrapped parts and
/// stored in the buffer. Rinse and repeat until all lines from the input are exhausted and have
/// already been processed.
///
fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16, Alignment)> {
if self.max_line_width == 0 {
return None;
}
// If `next_line` has already been invoked, try to retrieve the next line from the buffer containing the wrapped parts
let mut next_buffered_wrapped_line = self.next_wrapped_line();
// If there is non/the buffer is exhausted
if next_buffered_wrapped_line.is_none() {
// Get the next pending input line
if let Some((line_symbols, line_alignment)) = &mut self.input_lines.next() {
// Wrap it, save the result to a buffer
self.current_alignment = *line_alignment;
let wrapped_lines = self.wrap_line(line_symbols);
self.wrapped_lines_buffer = Some(wrapped_lines.into_iter());
// Get the first newly wrapped line from the buffer
next_buffered_wrapped_line = self.next_wrapped_line();
}
}
// Return the next wrapped line if nothing has been fully exhausted
if let Some((wrapped_line, wrapped_line_width)) = next_buffered_wrapped_line {
self.current_line = wrapped_line;
Some((
&self.current_line[..],
wrapped_line_width,
self.current_alignment,
))
} else {
None
}
}
}
/// A state machine that wraps lines on word boundaries.
pub struct WordWrapper<'a, O, I>
where
@@ -90,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 {
@@ -319,6 +493,7 @@ mod test {
use unicode_segmentation::UnicodeSegmentation;
enum Composer {
CharWrapper { trim: bool },
WordWrapper { trim: bool },
LineTruncator,
}
@@ -339,6 +514,9 @@ mod test {
});
let mut composer: Box<dyn LineComposer> = match which {
Composer::CharWrapper { trim } => {
Box::new(CharWrapper::new(styled_lines, text_area_width, trim))
}
Composer::WordWrapper { trim } => {
Box::new(WordWrapper::new(styled_lines, text_area_width, trim))
}
@@ -370,9 +548,15 @@ mod test {
&text[..],
width as u16,
);
let (char_wrapper, _, _) = run_composer(
Composer::CharWrapper { trim: true },
&text[..],
width as u16,
);
let (line_truncator, _, _) =
run_composer(Composer::LineTruncator, &text[..], width as u16);
let expected = vec![text];
assert_eq!(char_wrapper, expected);
assert_eq!(word_wrapper, expected);
assert_eq!(line_truncator, expected);
}
@@ -383,10 +567,12 @@ mod test {
let width = 20;
let text =
"abcdefg\nhijklmno\npabcdefg\nhijklmn\nopabcdefghijk\nlmnopabcd\n\n\nefghijklmno";
let (char_wrapper, _, _) = run_composer(Composer::CharWrapper { trim: true }, text, width);
let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width);
let wrapped: Vec<&str> = text.split('\n').collect();
assert_eq!(char_wrapper, wrapped);
assert_eq!(word_wrapper, wrapped);
assert_eq!(line_truncator, wrapped);
}
@@ -395,6 +581,8 @@ mod test {
fn line_composer_long_word() {
let width = 20;
let text = "abcdefghijklmnopabcdefghijklmnopabcdefghijklmnopabcdefghijklmno";
let (char_wrapper, _, _) =
run_composer(Composer::CharWrapper { trim: true }, text, width as u16);
let (word_wrapper, _, _) =
run_composer(Composer::WordWrapper { trim: true }, text, width as u16);
let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width as u16);
@@ -407,6 +595,10 @@ mod test {
];
assert_eq!(
word_wrapper, wrapped,
"CharWrapper should break the word at the line width limit."
);
assert_eq!(
char_wrapper, wrapped,
"WordWrapper should detect the line cannot be broken on word boundary and \
break it at line width limit."
);
@@ -421,6 +613,13 @@ mod test {
let text_multi_space =
"abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab c d e f g h i j k l \
m n o";
let (char_wrapper_single_space, _, _) =
run_composer(Composer::CharWrapper { trim: true }, text, width as u16);
let (char_wrapper_multi_space, _, _) = run_composer(
Composer::CharWrapper { trim: true },
text_multi_space,
width as u16,
);
let (word_wrapper_single_space, _, _) =
run_composer(Composer::WordWrapper { trim: true }, text, width as u16);
let (word_wrapper_multi_space, _, _) = run_composer(
@@ -430,6 +629,21 @@ mod test {
);
let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width as u16);
let char_wrapped_single_space = vec![
"abcd efghij klmnopab",
"cd efgh ijklmnopabcd",
"efg hijkl mnopab c d",
"e f g h i j k l m n ",
"o",
];
let char_wrapped_multi_space = vec![
"abcd efghij klmno",
"pabcd efgh ijklm",
"nopabcdefg hijkl mno",
"pab c d e f g h i j ",
"k l m n o",
];
// Word wrapping should give the same result for multiple or single space due to trimming.
let word_wrapped = vec![
"abcd efghij",
"klmnopabcd efgh",
@@ -437,6 +651,8 @@ mod test {
"hijkl mnopab c d e f",
"g h i j k l m n o",
];
assert_eq!(char_wrapper_single_space, char_wrapped_single_space);
assert_eq!(char_wrapper_multi_space, char_wrapped_multi_space);
assert_eq!(word_wrapper_single_space, word_wrapped);
assert_eq!(word_wrapper_multi_space, word_wrapped);
@@ -447,10 +663,12 @@ mod test {
fn line_composer_zero_width() {
let width = 0;
let text = "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab ";
let (char_wrapper, _, _) = run_composer(Composer::CharWrapper { trim: true }, text, width);
let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width);
let expected: Vec<&str> = Vec::new();
assert_eq!(char_wrapper, expected);
assert_eq!(word_wrapper, expected);
assert_eq!(line_truncator, expected);
}
@@ -459,12 +677,14 @@ mod test {
fn line_composer_max_line_width_of_1() {
let width = 1;
let text = "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab ";
let (char_wrapper, _, _) = run_composer(Composer::CharWrapper { trim: true }, text, width);
let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width);
let expected: Vec<&str> = UnicodeSegmentation::graphemes(text, true)
.filter(|g| g.chars().any(|c| !c.is_whitespace()))
.collect();
assert_eq!(char_wrapper, expected);
assert_eq!(word_wrapper, expected);
assert_eq!(line_truncator, vec!["a"]);
}
@@ -475,12 +695,31 @@ mod test {
let text =
"コンピュータ上で文字を扱う場合、典型的には文字\naaa\naによる通信を行う場合にその\
両端点では、";
let (char_wrapper, _, _) = run_composer(Composer::CharWrapper { trim: true }, text, width);
let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width);
assert_eq!(char_wrapper, vec!["", "a", "a", "a", "a"]);
assert_eq!(word_wrapper, vec!["", "a", "a", "a", "a"]);
assert_eq!(line_truncator, vec!["", "a", "a"]);
}
/// Tests `CharWrapper` with words some of which exceed line length and some not.
#[test]
fn line_composer_char_wrapper_mixed_length() {
let width = 20;
let text = "abcd efghij klmnopabcdefghijklmnopabcdefghijkl mnopab cdefghi j klmno";
let (char_wrapper, _, _) = run_composer(Composer::CharWrapper { trim: true }, text, width);
assert_eq!(
char_wrapper,
vec![
"abcd efghij klmnopab",
"cdefghijklmnopabcdef",
"ghijkl mnopab cdefgh",
"i j klmno",
]
)
}
/// Tests `WordWrapper` with words some of which exceed line length and some not.
#[test]
fn line_composer_word_wrapper_mixed_length() {
@@ -504,6 +743,8 @@ mod test {
let width = 20;
let text = "コンピュータ上で文字を扱う場合、典型的には文字による通信を行う場合にその両端点\
では、";
let (char_wrapper, char_wrapper_width, _) =
run_composer(Composer::CharWrapper { trim: true }, text, width);
let (word_wrapper, word_wrapper_width, _) =
run_composer(Composer::WordWrapper { trim: true }, text, width);
let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width);
@@ -515,7 +756,9 @@ mod test {
"う場合にその両端点で",
"は、",
];
assert_eq!(char_wrapper, wrapped);
assert_eq!(word_wrapper, wrapped);
assert_eq!(char_wrapper_width, vec![width, width, width, width, 4]);
assert_eq!(word_wrapper_width, vec![width, width, width, width, 4]);
}
@@ -523,8 +766,10 @@ mod test {
fn line_composer_leading_whitespace_removal() {
let width = 20;
let text = "AAAAAAAAAAAAAAAAAAAA AAA";
let (char_wrapper, _, _) = run_composer(Composer::CharWrapper { trim: true }, text, width);
let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width);
assert_eq!(char_wrapper, vec!["AAAAAAAAAAAAAAAAAAAA", "AAA",]);
assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAAAAAAA", "AAA",]);
assert_eq!(line_truncator, vec!["AAAAAAAAAAAAAAAAAAAA"]);
}
@@ -534,8 +779,10 @@ mod test {
fn line_composer_lots_of_spaces() {
let width = 20;
let text = " ";
let (char_wrapper, _, _) = run_composer(Composer::CharWrapper { trim: true }, text, width);
let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width);
assert_eq!(char_wrapper, vec![""]);
assert_eq!(word_wrapper, vec![""]);
assert_eq!(line_truncator, vec![" "]);
}
@@ -546,12 +793,14 @@ mod test {
fn line_composer_char_plus_lots_of_spaces() {
let width = 20;
let text = "a ";
let (char_wrapper, _, _) = run_composer(Composer::CharWrapper { trim: true }, text, width);
let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
let (line_truncator, _, _) = run_composer(Composer::LineTruncator, text, width);
// What's happening below is: the first line gets consumed, trailing spaces discarded,
// after 20 of which a word break occurs (probably shouldn't). The second line break
// discards all whitespace. The result should probably be vec!["a"] but it doesn't matter
// that much.
assert_eq!(char_wrapper, vec!["a "]);
assert_eq!(word_wrapper, vec!["a", ""]);
assert_eq!(line_truncator, vec!["a "]);
}
@@ -565,8 +814,20 @@ mod test {
// hiragana and katakana...
// This happens to also be a test case for mixed width because regular spaces are single width.
let text = "コンピュ ータ上で文字を扱う場合、 典型的には文 字による 通信を行 う場合にその両端点では、";
let (char_wrapper, char_wrapper_width, _) =
run_composer(Composer::CharWrapper { trim: true }, text, width);
let (word_wrapper, word_wrapper_width, _) =
run_composer(Composer::WordWrapper { trim: true }, text, width);
assert_eq!(
char_wrapper,
vec![
"コンピュ ータ上で文",
"字を扱う場合、 典型",
"的には文 字による 通",
"信を行 う場合にその",
"両端点では、",
]
);
assert_eq!(
word_wrapper,
vec![
@@ -579,9 +840,47 @@ mod test {
]
);
// Odd-sized lines have a space in them.
assert_eq!(char_wrapper_width, vec![19, 19, 20, 19, 12]);
assert_eq!(word_wrapper_width, vec![8, 20, 17, 17, 20, 4]);
}
#[test]
fn line_composer_char_wrapper_preserve_indentation() {
let width = 20;
let text = "AAAAAAAAAAAAAAAAAAAA AAA";
let (char_wrapper, _, _) = run_composer(Composer::CharWrapper { trim: false }, text, width);
assert_eq!(char_wrapper, vec!["AAAAAAAAAAAAAAAAAAAA", " AAA",]);
}
#[test]
fn line_composer_char_wrapper_preserve_indentation_with_wrap() {
let width = 10;
let text = "AAA AAA AAAAA AA AAAAAA\n B\n C\n D";
let (char_wrapper, _, _) = run_composer(Composer::CharWrapper { trim: false }, text, width);
assert_eq!(
char_wrapper,
vec!["AAA AAA AA", "AAA AA AAA", "AAA", " B", " C", " D"]
);
}
#[test]
fn line_composer_char_wrapper_preserve_indentation_lots_of_whitespace() {
let width = 10;
let text = " 4 Indent\n must wrap!";
let (char_wrapper, _, _) = run_composer(Composer::CharWrapper { trim: false }, text, width);
assert_eq!(
char_wrapper,
vec![
" ",
" 4 Ind",
"ent",
" ",
" mus",
"t wrap!"
]
);
}
/// Ensure words separated by nbsp are wrapped as if they were a single one.
#[test]
fn line_composer_word_wrapper_nbsp() {
@@ -641,8 +940,10 @@ mod test {
fn line_composer_zero_width_at_end() {
let width = 3;
let line = "foo\0";
let (char_wrapper, _, _) = run_composer(Composer::CharWrapper { trim: true }, line, width);
let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, line, width);
let (line_truncator, _, _) = run_composer(Composer::LineTruncator, line, width);
assert_eq!(char_wrapper, vec!["foo\0"]);
assert_eq!(word_wrapper, vec!["foo\0"]);
assert_eq!(line_truncator, vec!["foo\0"]);
}
@@ -655,11 +956,24 @@ mod test {
Line::from("This is right aligned and half short.").alignment(Alignment::Right),
Line::from("This should sit in the center.").alignment(Alignment::Center),
];
let (_, _, wrapped_alignments) =
let (_, _, char_wrapped_alignments) =
run_composer(Composer::CharWrapper { trim: true }, lines.clone(), width);
let (_, _, word_wrapped_alignments) =
run_composer(Composer::WordWrapper { trim: true }, lines.clone(), width);
let (_, _, truncated_alignments) = run_composer(Composer::LineTruncator, lines, width);
assert_eq!(
wrapped_alignments,
char_wrapped_alignments,
vec![
Alignment::Left,
Alignment::Left,
Alignment::Right,
Alignment::Right,
Alignment::Center,
Alignment::Center
]
);
assert_eq!(
word_wrapped_alignments,
vec![
Alignment::Left,
Alignment::Left,

View File

@@ -32,7 +32,8 @@ fn widgets_paragraph_renders_double_width_graphemes() {
let text = vec![Line::from(s)];
let paragraph = Paragraph::new(text)
.block(Block::default().borders(Borders::ALL))
.wrap(Wrap { trim: true });
.wrap(Wrap::WordBoundary)
.trim(true);
test_case(
paragraph,
@@ -63,7 +64,8 @@ fn widgets_paragraph_renders_mixed_width_graphemes() {
let text = vec![Line::from(s)];
let paragraph = Paragraph::new(text)
.block(Block::default().borders(Borders::ALL))
.wrap(Wrap { trim: true });
.wrap(Wrap::WordBoundary)
.trim(true);
f.render_widget(paragraph, size);
})
.unwrap();
@@ -142,11 +144,68 @@ 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 { trim: true });
.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))
.wrap(Wrap::WordBoundary)
.trim(true);
test_case(
paragraph.clone().alignment(Alignment::Left),
@@ -195,9 +254,74 @@ 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.clone())
.block(Block::default().borders(Borders::ALL))
.alignment(Alignment::Left);
test_case(
paragraph.clone().wrap(Wrap::CharBoundary).trim(true),
Buffer::with_lines(vec![
"┌──────────────────┐",
"│This is some │",
"│text with an exces│",
"│sive amount │",
"│of whitespace │",
"│between words. │",
"└──────────────────┘",
]),
);
test_case(
paragraph.clone().wrap(Wrap::CharBoundary).trim(false),
Buffer::with_lines(vec![
"┌──────────────────┐",
"│This is some │",
"│ text with an ex│",
"│cessive amou│",
"│nt of whitespace │",
"│ be│",
"│tween words. │",
"└──────────────────┘",
]),
);
test_case(
paragraph.clone().wrap(Wrap::WordBoundary).trim(true),
Buffer::with_lines(vec![
"┌──────────────────┐",
"│This is some │",
"│text with an │",
"│excessive │",
"│amount of │",
"│whitespace │",
"│between words. │",
"└──────────────────┘",
]),
);
// TODO: This test case is currently failing, will be reenabled upon being fixed.
// test_case(
// paragraph.clone().wrap(Wrap::WordBoundary).trim(false),
// Buffer::with_lines(vec![
// "┌──────────────────┐",
// "│This is some │",
// "│ text with an │",
// "│excessive │",
// "│amount of │",
// "│whitespace │",
// "│ between │",
// "│words. │",
// "└──────────────────┘",
// ]),
// );
}
#[test]
fn widgets_paragraph_works_with_padding() {
let text = vec![Line::from(SAMPLE_STRING)];
let mut text = vec![Line::from("This is always centered.").alignment(Alignment::Center)];
text.push(Line::from(SAMPLE_STRING));
let paragraph = Paragraph::new(text)
.block(Block::default().borders(Borders::ALL).padding(Padding {
left: 2,
@@ -205,13 +329,40 @@ fn widgets_paragraph_works_with_padding() {
top: 1,
bottom: 1,
}))
.wrap(Wrap { trim: true });
.trim(true);
test_case(
paragraph.clone().alignment(Alignment::Left),
paragraph
.clone()
.alignment(Alignment::Left)
.wrap(Wrap::CharBoundary),
Buffer::with_lines(vec![
"┌────────────────────┐",
"│ │",
"│ This is always c │",
"│ entered. │",
"│ The library is b │",
"│ ased on the prin │",
"│ ciple of immedia │",
"│ te rendering wit │",
"│ h intermediate b │",
"│ uffers. This mea │",
"│ ns that at each │",
"│ new frame you sh │",
"│ │",
"└────────────────────┘",
]),
);
test_case(
paragraph
.clone()
.alignment(Alignment::Left)
.wrap(Wrap::WordBoundary),
Buffer::with_lines(vec![
"┌────────────────────┐",
"│ │",
"│ This is always │",
"│ centered. │",
"│ The library is │",
"│ based on the │",
"│ principle of │",
@@ -224,37 +375,35 @@ fn widgets_paragraph_works_with_padding() {
"└────────────────────┘",
]),
);
test_case(
paragraph.clone().alignment(Alignment::Right),
paragraph
.clone()
.alignment(Alignment::Right)
.wrap(Wrap::CharBoundary),
Buffer::with_lines(vec![
"┌────────────────────┐",
"│ │",
" The library is",
"based on the",
" principle of",
" immediate",
" rendering with",
" intermediate",
" buffers. This",
" means that at",
"This is always c",
"entered. ",
"The library is b",
"ased on the prin",
"ciple of immedia",
"te rendering wit",
"h intermediate b",
"uffers. This mea",
"│ ns that at each │",
"│ new frame you sh │",
"│ │",
"└────────────────────┘",
]),
);
let mut text = vec![Line::from("This is always centered.").alignment(Alignment::Center)];
text.push(Line::from(SAMPLE_STRING));
let paragraph = Paragraph::new(text)
.block(Block::default().borders(Borders::ALL).padding(Padding {
left: 2,
right: 2,
top: 1,
bottom: 1,
}))
.wrap(Wrap { trim: true });
test_case(
paragraph.alignment(Alignment::Right),
paragraph
.clone()
.alignment(Alignment::Right)
.wrap(Wrap::WordBoundary),
Buffer::with_lines(vec![
"┌────────────────────┐",
"│ │",
@@ -285,7 +434,8 @@ fn widgets_paragraph_can_align_spans() {
];
let paragraph = Paragraph::new(text)
.block(Block::default().borders(Borders::ALL))
.wrap(Wrap { trim: true });
.wrap(Wrap::WordBoundary)
.trim(true);
test_case(
paragraph.clone().alignment(Alignment::Left),