Compare commits

...

4 Commits

Author SHA1 Message Date
Leon Sautour
f32fb022e0 docs(example): add break word behaviour to paragraph example 2023-04-26 21:14:41 +02:00
charliedu2000
12e4f0d921 Feature: allow word-break in wrapped paragraphs, add it to document. 2022-08-13 00:07:29 +08:00
charliedu2000
727e13d876 Feature: allow word-break in wrapped paragraphs. 2022-08-12 23:49:24 +08:00
charliedu2000
71835a0a52 Feature: allow word-break in wrapped paragraphs. 2022-08-12 23:40:08 +08:00
6 changed files with 287 additions and 55 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 {
trim: true,
break_words: false,
});
f.render_widget(paragraph, area);
}

View File

@@ -153,19 +153,28 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
.style(Style::default().bg(Color::White).fg(Color::Black))
.block(create_block("Left, wrap"))
.alignment(Alignment::Left)
.wrap(Wrap { trim: true });
.wrap(Wrap {
trim: true,
break_words: false,
});
f.render_widget(paragraph, chunks[1]);
let paragraph = Paragraph::new(text.clone())
.style(Style::default().bg(Color::White).fg(Color::Black))
.block(create_block("Center, wrap"))
.alignment(Alignment::Center)
.wrap(Wrap { trim: true })
.wrap(Wrap {
trim: true,
break_words: false,
})
.scroll((app.scroll, 0));
f.render_widget(paragraph, chunks[2]);
let paragraph = Paragraph::new(text)
.style(Style::default().bg(Color::White).fg(Color::Black))
.block(create_block("Right, wrap"))
.block(create_block("Right, wrap (break words)"))
.alignment(Alignment::Right)
.wrap(Wrap { trim: true });
.wrap(Wrap {
trim: true,
break_words: true,
});
f.render_widget(paragraph, chunks[3]);
}

View File

@@ -83,7 +83,10 @@ 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 {
trim: true,
break_words: false,
});
f.render_widget(paragraph, chunks[0]);
let block = Block::default()

View File

@@ -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 { trim: true, break_words: false });
/// ```
#[derive(Debug, Clone)]
pub struct Paragraph<'a> {
@@ -70,7 +70,7 @@ pub struct Paragraph<'a> {
/// - 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 });
/// Paragraph::new(bullet_points.clone()).wrap(Wrap { trim: true, break_words: false });
/// // Some indented points:
/// // - First thing goes here and is
/// // long so that it wraps
@@ -78,7 +78,7 @@ pub struct Paragraph<'a> {
/// // is long enough to wrap
///
/// // But without trimming, indentation is preserved:
/// Paragraph::new(bullet_points).wrap(Wrap { trim: false });
/// Paragraph::new(bullet_points).wrap(Wrap { trim: false, break_words: false });
/// // Some indented points:
/// // - First thing goes here
/// // and is long so that it wraps
@@ -89,6 +89,8 @@ pub struct Paragraph<'a> {
pub struct Wrap {
/// Should leading whitespace be trimmed
pub trim: bool,
/// Should words at the end of lines be broken
pub break_words: bool,
}
impl<'a> Paragraph<'a> {
@@ -162,15 +164,21 @@ impl<'a> Widget for Paragraph<'a> {
}))
});
let mut line_composer: Box<dyn LineComposer> = if let Some(Wrap { trim }) = self.wrap {
Box::new(WordWrapper::new(&mut styled, text_area.width, trim))
} else {
let mut line_composer = Box::new(LineTruncator::new(&mut styled, text_area.width));
if let Alignment::Left = self.alignment {
line_composer.set_horizontal_offset(self.scroll.1);
}
line_composer
};
let mut line_composer: Box<dyn LineComposer> =
if let Some(Wrap { trim, break_words }) = self.wrap {
Box::new(WordWrapper::new(
&mut styled,
text_area.width,
trim,
break_words,
))
} else {
let mut line_composer = Box::new(LineTruncator::new(&mut styled, text_area.width));
if let Alignment::Left = self.alignment {
line_composer.set_horizontal_offset(self.scroll.1);
}
line_composer
};
let mut y = 0;
while let Some((current_line, current_line_width)) = line_composer.next_line() {
if y >= self.scroll.0 {

View File

@@ -19,6 +19,7 @@ pub struct WordWrapper<'a, 'b> {
next_line: Vec<StyledGrapheme<'a>>,
/// Removes the leading whitespace from lines
trim: bool,
break_words: bool,
}
impl<'a, 'b> WordWrapper<'a, 'b> {
@@ -26,6 +27,7 @@ impl<'a, 'b> WordWrapper<'a, 'b> {
symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,
max_line_width: u16,
trim: bool,
break_words: bool,
) -> WordWrapper<'a, 'b> {
WordWrapper {
symbols,
@@ -33,6 +35,7 @@ impl<'a, 'b> WordWrapper<'a, 'b> {
current_line: vec![],
next_line: vec![],
trim,
break_words,
}
}
}
@@ -87,11 +90,12 @@ impl<'a, 'b> LineComposer<'a> for WordWrapper<'a, 'b> {
if current_line_width > self.max_line_width {
// If there was no word break in the text, wrap at the end of the line.
let (truncate_at, truncated_width) = if symbols_to_last_word_end != 0 {
(symbols_to_last_word_end, width_to_last_word_end)
} else {
(self.current_line.len() - 1, self.max_line_width)
};
let (truncate_at, truncated_width) =
if symbols_to_last_word_end != 0 && !self.break_words {
(symbols_to_last_word_end, width_to_last_word_end)
} else {
(self.current_line.len() - 1, self.max_line_width)
};
// Push the remainder to the next line but strip leading whitespace:
{
@@ -235,7 +239,7 @@ mod test {
use unicode_segmentation::UnicodeSegmentation;
enum Composer {
WordWrapper { trim: bool },
WordWrapper { trim: bool, break_words: bool },
LineTruncator,
}
@@ -244,9 +248,12 @@ mod test {
let mut styled =
UnicodeSegmentation::graphemes(text, true).map(|g| StyledGrapheme { symbol: g, style });
let mut composer: Box<dyn LineComposer> = match which {
Composer::WordWrapper { trim } => {
Box::new(WordWrapper::new(&mut styled, text_area_width, trim))
}
Composer::WordWrapper { trim, break_words } => Box::new(WordWrapper::new(
&mut styled,
text_area_width,
trim,
break_words,
)),
Composer::LineTruncator => Box::new(LineTruncator::new(&mut styled, text_area_width)),
};
let mut lines = vec![];
@@ -268,8 +275,14 @@ mod test {
let width = 40;
for i in 1..width {
let text = "a".repeat(i);
let (word_wrapper, _) =
run_composer(Composer::WordWrapper { trim: true }, &text, width as u16);
let (word_wrapper, _) = run_composer(
Composer::WordWrapper {
trim: true,
break_words: true,
},
&text,
width as u16,
);
let (line_truncator, _) = run_composer(Composer::LineTruncator, &text, width as u16);
let expected = vec![text];
assert_eq!(word_wrapper, expected);
@@ -282,7 +295,14 @@ mod test {
let width = 20;
let text =
"abcdefg\nhijklmno\npabcdefg\nhijklmn\nopabcdefghijk\nlmnopabcd\n\n\nefghijklmno";
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
let (word_wrapper, _) = run_composer(
Composer::WordWrapper {
trim: true,
break_words: true,
},
text,
width,
);
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
let wrapped: Vec<&str> = text.split('\n').collect();
@@ -294,8 +314,14 @@ mod test {
fn line_composer_long_word() {
let width = 20;
let text = "abcdefghijklmnopabcdefghijklmnopabcdefghijklmnopabcdefghijklmno";
let (word_wrapper, _) =
run_composer(Composer::WordWrapper { trim: true }, text, width as u16);
let (word_wrapper, _) = run_composer(
Composer::WordWrapper {
trim: true,
break_words: true,
},
text,
width as u16,
);
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width as u16);
let wrapped = vec![
@@ -320,10 +346,19 @@ 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 (word_wrapper_single_space, _) =
run_composer(Composer::WordWrapper { trim: true }, text, width as u16);
let (word_wrapper_single_space, _) = run_composer(
Composer::WordWrapper {
trim: true,
break_words: true,
},
text,
width as u16,
);
let (word_wrapper_multi_space, _) = run_composer(
Composer::WordWrapper { trim: true },
Composer::WordWrapper {
trim: true,
break_words: true,
},
text_multi_space,
width as u16,
);
@@ -346,7 +381,14 @@ mod test {
fn line_composer_zero_width() {
let width = 0;
let text = "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab ";
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
let (word_wrapper, _) = run_composer(
Composer::WordWrapper {
trim: true,
break_words: true,
},
text,
width,
);
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
let expected: Vec<&str> = Vec::new();
@@ -358,7 +400,14 @@ mod test {
fn line_composer_max_line_width_of_1() {
let width = 1;
let text = "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab ";
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
let (word_wrapper, _) = run_composer(
Composer::WordWrapper {
trim: true,
break_words: true,
},
text,
width,
);
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
let expected: Vec<&str> = UnicodeSegmentation::graphemes(text, true)
@@ -373,7 +422,14 @@ mod test {
let width = 1;
let text = "コンピュータ上で文字を扱う場合、典型的には文字\naaaによる通信を行う場合にその\
両端点では、";
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
let (word_wrapper, _) = run_composer(
Composer::WordWrapper {
trim: true,
break_words: true,
},
text,
width,
);
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
assert_eq!(word_wrapper, vec!["", "a", "a", "a"]);
assert_eq!(line_truncator, vec!["", "a"]);
@@ -384,7 +440,14 @@ mod test {
fn line_composer_word_wrapper_mixed_length() {
let width = 20;
let text = "abcd efghij klmnopabcdefghijklmnopabcdefghijkl mnopab cdefghi j klmno";
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
let (word_wrapper, _) = run_composer(
Composer::WordWrapper {
trim: true,
break_words: true,
},
text,
width,
);
assert_eq!(
word_wrapper,
vec![
@@ -402,8 +465,14 @@ mod test {
let width = 20;
let text = "コンピュータ上で文字を扱う場合、典型的には文字による通信を行う場合にその両端点\
では、";
let (word_wrapper, word_wrapper_width) =
run_composer(Composer::WordWrapper { trim: true }, text, width);
let (word_wrapper, word_wrapper_width) = run_composer(
Composer::WordWrapper {
trim: true,
break_words: true,
},
text,
width,
);
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
assert_eq!(line_truncator, vec!["コンピュータ上で文字"]);
let wrapped = vec![
@@ -421,7 +490,14 @@ mod test {
fn line_composer_leading_whitespace_removal() {
let width = 20;
let text = "AAAAAAAAAAAAAAAAAAAA AAA";
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
let (word_wrapper, _) = run_composer(
Composer::WordWrapper {
trim: true,
break_words: true,
},
text,
width,
);
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAAAAAAA", "AAA",]);
assert_eq!(line_truncator, vec!["AAAAAAAAAAAAAAAAAAAA"]);
@@ -432,7 +508,14 @@ mod test {
fn line_composer_lots_of_spaces() {
let width = 20;
let text = " ";
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
let (word_wrapper, _) = run_composer(
Composer::WordWrapper {
trim: true,
break_words: true,
},
text,
width,
);
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
assert_eq!(word_wrapper, vec![""]);
assert_eq!(line_truncator, vec![" "]);
@@ -444,7 +527,14 @@ mod test {
fn line_composer_char_plus_lots_of_spaces() {
let width = 20;
let text = "a ";
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
let (word_wrapper, _) = run_composer(
Composer::WordWrapper {
trim: true,
break_words: 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
@@ -463,8 +553,14 @@ 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 (word_wrapper, word_wrapper_width) =
run_composer(Composer::WordWrapper { trim: true }, text, width);
let (word_wrapper, word_wrapper_width) = run_composer(
Composer::WordWrapper {
trim: true,
break_words: true,
},
text,
width,
);
assert_eq!(
word_wrapper,
vec![
@@ -485,13 +581,26 @@ mod test {
fn line_composer_word_wrapper_nbsp() {
let width = 20;
let text = "AAAAAAAAAAAAAAA AAAA\u{00a0}AAA";
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
let (word_wrapper, _) = run_composer(
Composer::WordWrapper {
trim: true,
break_words: true,
},
text,
width,
);
assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAA", "AAAA\u{00a0}AAA",]);
// Ensure that if the character was a regular space, it would be wrapped differently.
let text_space = text.replace('\u{00a0}', " ");
let (word_wrapper_space, _) =
run_composer(Composer::WordWrapper { trim: true }, &text_space, width);
let (word_wrapper_space, _) = run_composer(
Composer::WordWrapper {
trim: true,
break_words: true,
},
&text_space,
width,
);
assert_eq!(word_wrapper_space, vec!["AAAAAAAAAAAAAAA AAAA", "AAA",]);
}
@@ -499,7 +608,14 @@ mod test {
fn line_composer_word_wrapper_preserve_indentation() {
let width = 20;
let text = "AAAAAAAAAAAAAAAAAAAA AAA";
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: false }, text, width);
let (word_wrapper, _) = run_composer(
Composer::WordWrapper {
trim: false,
break_words: true,
},
text,
width,
);
assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAAAAAAA", " AAA",]);
}
@@ -507,7 +623,14 @@ mod test {
fn line_composer_word_wrapper_preserve_indentation_with_wrap() {
let width = 10;
let text = "AAA AAA AAAAA AA AAAAAA\n B\n C\n D";
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: false }, text, width);
let (word_wrapper, _) = run_composer(
Composer::WordWrapper {
trim: false,
break_words: true,
},
text,
width,
);
assert_eq!(
word_wrapper,
vec!["AAA AAA", "AAAAA AA", "AAAAAA", " B", " C", " D"]
@@ -518,7 +641,14 @@ mod test {
fn line_composer_word_wrapper_preserve_indentation_lots_of_whitespace() {
let width = 10;
let text = " 4 Indent\n must wrap!";
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: false }, text, width);
let (word_wrapper, _) = run_composer(
Composer::WordWrapper {
trim: false,
break_words: true,
},
text,
width,
);
assert_eq!(
word_wrapper,
vec![

View File

@@ -25,7 +25,10 @@ fn widgets_paragraph_can_wrap_its_content() {
let paragraph = Paragraph::new(text)
.block(Block::default().borders(Borders::ALL))
.alignment(alignment)
.wrap(Wrap { trim: true });
.wrap(Wrap {
trim: true,
break_words: false,
});
f.render_widget(paragraph, size);
})
.unwrap();
@@ -79,6 +82,76 @@ fn widgets_paragraph_can_wrap_its_content() {
);
}
#[test]
fn widgets_paragraph_can_wrap_its_content_with_word_break_enabled() {
let test_case = |alignment, expected| {
let backend = TestBackend::new(20, 10);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let size = f.size();
let text = vec![Spans::from(SAMPLE_STRING)];
let paragraph = Paragraph::new(text)
.block(Block::default().borders(Borders::ALL))
.alignment(alignment)
.wrap(Wrap {
trim: true,
break_words: true,
});
f.render_widget(paragraph, size);
})
.unwrap();
terminal.backend().assert_buffer(&expected);
};
test_case(
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(
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_case(
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]
fn widgets_paragraph_renders_double_width_graphemes() {
let backend = TestBackend::new(10, 10);
@@ -91,7 +164,10 @@ fn widgets_paragraph_renders_double_width_graphemes() {
let text = vec![Spans::from(s)];
let paragraph = Paragraph::new(text)
.block(Block::default().borders(Borders::ALL))
.wrap(Wrap { trim: true });
.wrap(Wrap {
trim: true,
break_words: false,
});
f.render_widget(paragraph, size);
})
.unwrap();
@@ -123,7 +199,10 @@ fn widgets_paragraph_renders_mixed_width_graphemes() {
let text = vec![Spans::from(s)];
let paragraph = Paragraph::new(text)
.block(Block::default().borders(Borders::ALL))
.wrap(Wrap { trim: true });
.wrap(Wrap {
trim: true,
break_words: false,
});
f.render_widget(paragraph, size);
})
.unwrap();