diff --git a/examples/demo/ui.rs b/examples/demo/ui.rs index 837bd42..3eda0b1 100644 --- a/examples/demo/ui.rs +++ b/examples/demo/ui.rs @@ -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); } diff --git a/examples/paragraph.rs b/examples/paragraph.rs index 1ec8b86..7a8144a 100644 --- a/examples/paragraph.rs +++ b/examples/paragraph.rs @@ -153,19 +153,28 @@ fn ui(f: &mut Frame, 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]); } diff --git a/examples/popup.rs b/examples/popup.rs index 3ae8cc4..66bdad7 100644 --- a/examples/popup.rs +++ b/examples/popup.rs @@ -83,7 +83,10 @@ fn ui(f: &mut Frame, 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() diff --git a/src/widgets/paragraph.rs b/src/widgets/paragraph.rs index f4ebd8d..fce2bdc 100644 --- a/src/widgets/paragraph.rs +++ b/src/widgets/paragraph.rs @@ -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 = 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 = + 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 { diff --git a/src/widgets/reflow.rs b/src/widgets/reflow.rs index c611299..0cac380 100644 --- a/src/widgets/reflow.rs +++ b/src/widgets/reflow.rs @@ -19,6 +19,7 @@ pub struct WordWrapper<'a, 'b> { next_line: Vec>, /// 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>, 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 = 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![ diff --git a/tests/widgets_paragraph.rs b/tests/widgets_paragraph.rs index a4cd465..13b506d 100644 --- a/tests/widgets_paragraph.rs +++ b/tests/widgets_paragraph.rs @@ -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();