Compare commits
4 Commits
v0.26.2-al
...
feat-wrapp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58ccaf9af9 | ||
|
|
f61d4fa034 | ||
|
|
7908e944f6 | ||
|
|
b0b1ac85b5 |
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user