Feature: allow word-break in wrapped paragraphs.

pull/653/head
charliedu2000 2 years ago
parent a6b25a4877
commit 71835a0a52

@ -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);
}

@ -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"))
.alignment(Alignment::Right)
.wrap(Wrap { trim: true });
.wrap(Wrap {
trim: true,
break_words: false,
});
f.render_widget(paragraph, chunks[3]);
}

@ -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()

@ -89,6 +89,7 @@ pub struct Paragraph<'a> {
pub struct Wrap {
/// Should leading whitespace be trimmed
pub trim: bool,
pub break_words: bool,
}
impl<'a> Paragraph<'a> {
@ -162,15 +163,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 {

@ -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![

@ -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();
@ -91,7 +94,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 +129,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();

Loading…
Cancel
Save