From 10642d0e04cfde214b93de1ef4f427c1fa35440d Mon Sep 17 00:00:00 2001 From: Karoline Pauls Date: Sat, 8 Dec 2018 17:28:25 +0000 Subject: [PATCH] Paragraph: word wrapping --- examples/paragraph.rs | 19 +- src/widgets/mod.rs | 1 + src/widgets/paragraph.rs | 103 +++------ src/widgets/reflow.rs | 446 +++++++++++++++++++++++++++++++++++++++ tests/paragraph.rs | 102 ++++++--- 5 files changed, 555 insertions(+), 116 deletions(-) create mode 100644 src/widgets/reflow.rs diff --git a/examples/paragraph.rs b/examples/paragraph.rs index ce79bbf3..b4bbc7f2 100644 --- a/examples/paragraph.rs +++ b/examples/paragraph.rs @@ -30,12 +30,13 @@ fn main() -> Result<(), failure::Error> { let events = Events::new(); + let mut scroll: u16 = 0; loop { terminal.draw(|mut f| { let size = f.size(); // Words made "loooong" to demonstrate line breaking. - let s = "Veeeeeeeeeeeeeeeery loooooooooooooooooong striiiiiiiiiiiiiiiiiiiiiiiiiing. "; + let s = "Veeeeeeeeeeeeeeeery loooooooooooooooooong striiiiiiiiiiiiiiiiiiiiiiiiiing. "; let mut long_line = s.repeat(usize::from(size.width) / s.len() + 4); long_line.push('\n'); @@ -58,16 +59,16 @@ fn main() -> Result<(), failure::Error> { .split(size); let text = [ - Text::raw("This a line\n"), - Text::styled("This a line\n", Style::default().fg(Color::Red)), - Text::styled("This a line\n", Style::default().bg(Color::Blue)), + Text::raw("This is a line \n"), + Text::styled("This is a line \n", Style::default().fg(Color::Red)), + Text::styled("This is a line\n", Style::default().bg(Color::Blue)), Text::styled( - "This a longer line\n", + "This is a longer line\n", Style::default().modifier(Modifier::CrossedOut), ), - Text::raw(&long_line), + Text::styled(&long_line, Style::default().bg(Color::Green)), Text::styled( - "This a line\n", + "This is a line\n", Style::default().fg(Color::Green).modifier(Modifier::Italic), ), ]; @@ -88,6 +89,7 @@ fn main() -> Result<(), failure::Error> { .block(block.clone().title("Center, wrap")) .alignment(Alignment::Center) .wrap(true) + .scroll(scroll) .render(&mut f, chunks[2]); Paragraph::new(text.iter()) .block(block.clone().title("Right, wrap")) @@ -96,6 +98,9 @@ fn main() -> Result<(), failure::Error> { .render(&mut f, chunks[3]); })?; + scroll += 1; + scroll %= 10; + match events.next()? { Event::Input(key) => { if key == Key::Char('q') { diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 63358ac9..91f1a0af 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -7,6 +7,7 @@ mod chart; mod gauge; mod list; mod paragraph; +mod reflow; mod sparkline; mod table; mod tabs; diff --git a/src/widgets/paragraph.rs b/src/widgets/paragraph.rs index 32d407f2..4ae1d8cb 100644 --- a/src/widgets/paragraph.rs +++ b/src/widgets/paragraph.rs @@ -1,13 +1,21 @@ use either::Either; -use itertools::{multipeek, MultiPeek}; use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthStr; use buffer::Buffer; use layout::{Alignment, Rect}; use style::Style; +use widgets::reflow::{LineComposer, LineTruncator, Styled, WordWrapper}; use widgets::{Block, Text, Widget}; +fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment) -> u16 { + match alignment { + Alignment::Center => (text_area_width / 2).saturating_sub(line_width / 2), + Alignment::Right => text_area_width.saturating_sub(line_width), + Alignment::Left => 0, + } +} + /// A widget to display some text. /// /// # Examples @@ -96,7 +104,7 @@ where } } -impl<'a, 't, T> Widget for Paragraph<'a, 't, T> +impl<'a, 't, 'b, T> Widget for Paragraph<'a, 't, T> where T: Iterator>, { @@ -116,94 +124,37 @@ where self.background(&text_area, buf, self.style.bg); let style = self.style; - let styled = self.text.by_ref().flat_map(|t| match *t { + let mut styled = self.text.by_ref().flat_map(|t| match *t { Text::Raw(ref d) => { let data: &'t str = d; // coerce to &str - Either::Left(UnicodeSegmentation::graphemes(data, true).map(|g| (g, style))) + Either::Left(UnicodeSegmentation::graphemes(data, true).map(|g| Styled(g, style))) } Text::Styled(ref d, s) => { let data: &'t str = d; // coerce to &str - Either::Right(UnicodeSegmentation::graphemes(data, true).map(move |g| (g, s))) + Either::Right(UnicodeSegmentation::graphemes(data, true).map(move |g| Styled(g, s))) } }); - let mut styled = multipeek(styled); - fn get_cur_line_len<'a, I: Iterator>( - styled: &mut MultiPeek, - ) -> u16 { - let mut line_len = 0; - while match &styled.peek() { - Some(&(x, _)) => x != "\n", - None => false, - } { - line_len += 1; - } - line_len - }; - - let mut x = match self.alignment { - Alignment::Center => { - (text_area.width / 2).saturating_sub(get_cur_line_len(&mut styled) / 2) - } - Alignment::Right => (text_area.width).saturating_sub(get_cur_line_len(&mut styled)), - Alignment::Left => 0, + let mut line_composer: Box = if self.wrapping { + Box::new(WordWrapper::new(&mut styled, text_area.width)) + } else { + Box::new(LineTruncator::new(&mut styled, text_area.width)) }; let mut y = 0; - - let mut remove_leading_whitespaces = false; - while let Some((string, style)) = styled.next() { - if string == "\n" { - x = match self.alignment { - Alignment::Center => { - (text_area.width / 2).saturating_sub(get_cur_line_len(&mut styled) / 2) - } - - Alignment::Right => { - (text_area.width).saturating_sub(get_cur_line_len(&mut styled)) - } - Alignment::Left => 0, - }; - y += 1; - continue; - } - let token_end_index = x + string.width() as u16 - 1; - let last_column_index = text_area.width - 1; - if token_end_index > last_column_index { - if !self.wrapping { - continue; // Truncate the remainder of the line. - } else { - x = match self.alignment { - Alignment::Center => { - (text_area.width / 2).saturating_sub(get_cur_line_len(&mut styled) / 2) - } - - Alignment::Right => { - (text_area.width).saturating_sub(get_cur_line_len(&mut styled) + 1) - } - Alignment::Left => 0, - }; - y += 1; - remove_leading_whitespaces = true + while let Some((current_line, current_line_width)) = line_composer.next_line() { + if y >= self.scroll { + let mut x = get_line_offset(current_line_width, text_area.width, self.alignment); + for Styled(symbol, style) in current_line { + buf.get_mut(text_area.left() + x, text_area.top() + y - self.scroll) + .set_symbol(symbol) + .set_style(*style); + x += symbol.width() as u16; } } - - if remove_leading_whitespaces && string == " " { - continue; - } - remove_leading_whitespaces = false; - - if y > text_area.height + self.scroll - 1 { + y += 1; + if y >= text_area.height + self.scroll { break; } - - if y < self.scroll { - continue; - } - - buf.get_mut(text_area.left() + x, text_area.top() + y - self.scroll) - .set_symbol(string) - .set_style(style); - x += string.width() as u16; } } } diff --git a/src/widgets/reflow.rs b/src/widgets/reflow.rs new file mode 100644 index 00000000..d46d490b --- /dev/null +++ b/src/widgets/reflow.rs @@ -0,0 +1,446 @@ +use style::Style; +use unicode_width::UnicodeWidthStr; + +const NBSP: &str = "\u{00a0}"; + +#[derive(Copy, Clone, Debug)] +pub struct Styled<'a>(pub &'a str, pub Style); + +/// 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). +pub trait LineComposer<'a> { + fn next_line(&mut self) -> Option<(&[Styled<'a>], u16)>; +} + +/// A state machine that wraps lines on word boundaries. +pub struct WordWrapper<'a, 'b> { + symbols: &'b mut Iterator>, + max_line_width: u16, + current_line: Vec>, + next_line: Vec>, +} + +impl<'a, 'b> WordWrapper<'a, 'b> { + pub fn new( + symbols: &'b mut Iterator>, + max_line_width: u16, + ) -> WordWrapper<'a, 'b> { + WordWrapper { + symbols, + max_line_width, + current_line: vec![], + next_line: vec![], + } + } +} + +impl<'a, 'b> LineComposer<'a> for WordWrapper<'a, 'b> { + fn next_line(&mut self) -> Option<(&[Styled<'a>], u16)> { + if self.max_line_width == 0 { + return None; + } + std::mem::swap(&mut self.current_line, &mut self.next_line); + self.next_line.truncate(0); + + let mut current_line_width = self + .current_line + .iter() + .map(|Styled(c, _)| c.width() as u16) + .sum(); + + let mut symbols_to_last_word_end: usize = 0; + let mut width_to_last_word_end: u16 = 0; + let mut prev_whitespace = false; + let mut symbols_exhausted = true; + for Styled(symbol, style) in &mut self.symbols { + symbols_exhausted = false; + let symbol_whitespace = symbol.chars().all(&char::is_whitespace); + + // Ignore characters wider that the total max width. + if symbol.width() as u16 > self.max_line_width + // Skip leading whitespace. + || symbol_whitespace && symbol != "\n" && current_line_width == 0 + { + continue; + } + + // Break on newline and discard it. + if symbol == "\n" { + if prev_whitespace { + current_line_width = width_to_last_word_end; + self.current_line.truncate(symbols_to_last_word_end); + } + break; + } + + // Mark the previous symbol as word end. + if symbol_whitespace && !prev_whitespace && symbol != NBSP { + symbols_to_last_word_end = self.current_line.len(); + width_to_last_word_end = current_line_width; + } + + self.current_line.push(Styled(symbol, style)); + current_line_width += symbol.width() as u16; + + 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) + }; + + // Push the remainder to the next line but strip leading whitespace: + { + let remainder = &self.current_line[truncate_at..]; + if let Some(remainder_nonwhite) = remainder + .iter() + .position(|Styled(c, _)| !c.chars().all(&char::is_whitespace)) + { + self.next_line + .extend_from_slice(&remainder[remainder_nonwhite..]); + } + } + self.current_line.truncate(truncate_at); + current_line_width = truncated_width; + break; + } + + prev_whitespace = symbol_whitespace; + } + + // Even if the iterator is exhausted, pass the previous remainder. + if symbols_exhausted && self.current_line.is_empty() { + None + } else { + Some((&self.current_line[..], current_line_width)) + } + } +} + +/// A state machine that truncates overhanging lines. +pub struct LineTruncator<'a, 'b> { + symbols: &'b mut Iterator>, + max_line_width: u16, + current_line: Vec>, +} + +impl<'a, 'b> LineTruncator<'a, 'b> { + pub fn new( + symbols: &'b mut Iterator>, + max_line_width: u16, + ) -> LineTruncator<'a, 'b> { + LineTruncator { + symbols, + max_line_width, + current_line: vec![], + } + } +} + +impl<'a, 'b> LineComposer<'a> for LineTruncator<'a, 'b> { + fn next_line(&mut self) -> Option<(&[Styled<'a>], u16)> { + if self.max_line_width == 0 { + return None; + } + + self.current_line.truncate(0); + let mut current_line_width = 0; + + let mut skip_rest = false; + let mut symbols_exhausted = true; + for Styled(symbol, style) in &mut self.symbols { + symbols_exhausted = false; + + // Ignore characters wider that the total max width. + if symbol.width() as u16 > self.max_line_width { + continue; + } + + // Break on newline and discard it. + if symbol == "\n" { + break; + } + + if current_line_width + symbol.width() as u16 > self.max_line_width { + // Exhaust the remainder of the line. + skip_rest = true; + break; + } + + current_line_width += symbol.width() as u16; + self.current_line.push(Styled(symbol, style)); + } + + if skip_rest { + for Styled(symbol, _) in &mut self.symbols { + if symbol == "\n" { + break; + } + } + } + + if symbols_exhausted && self.current_line.is_empty() { + None + } else { + Some((&self.current_line[..], current_line_width)) + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use unicode_segmentation::UnicodeSegmentation; + + enum Composer { + WordWrapper, + LineTruncator, + } + + fn run_composer(which: Composer, text: &str, text_area_width: u16) -> (Vec, Vec) { + let style = Default::default(); + let mut styled = UnicodeSegmentation::graphemes(text, true).map(|g| Styled(g, style)); + let mut composer: Box = match which { + Composer::WordWrapper => Box::new(WordWrapper::new(&mut styled, text_area_width)), + Composer::LineTruncator => Box::new(LineTruncator::new(&mut styled, text_area_width)), + }; + let mut lines = vec![]; + let mut widths = vec![]; + while let Some((styled, width)) = composer.next_line() { + let line = styled + .iter() + .map(|Styled(g, _style)| *g) + .collect::(); + assert!(width <= text_area_width); + lines.push(line); + widths.push(width); + } + (lines, widths) + } + + #[test] + fn line_composer_one_line() { + let width = 40; + for i in 1..width { + let text = "a".repeat(i); + let (word_wrapper, _) = run_composer(Composer::WordWrapper, &text, width as u16); + let (line_truncator, _) = run_composer(Composer::LineTruncator, &text, width as u16); + let expected = vec![text]; + assert_eq!(word_wrapper, expected); + assert_eq!(line_truncator, expected); + } + } + + #[test] + fn line_composer_short_lines() { + let width = 20; + let text = + "abcdefg\nhijklmno\npabcdefg\nhijklmn\nopabcdefghijk\nlmnopabcd\n\n\nefghijklmno"; + let (word_wrapper, _) = run_composer(Composer::WordWrapper, text, width); + let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width); + + let wrapped: Vec<&str> = text.split('\n').collect(); + assert_eq!(word_wrapper, wrapped); + assert_eq!(line_truncator, wrapped); + } + + #[test] + fn line_composer_long_word() { + let width = 20; + let text = "abcdefghijklmnopabcdefghijklmnopabcdefghijklmnopabcdefghijklmno"; + let (word_wrapper, _) = run_composer(Composer::WordWrapper, text, width as u16); + let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width as u16); + + let wrapped = vec![ + &text[..width], + &text[width..width * 2], + &text[width * 2..width * 3], + &text[width * 3..], + ]; + assert_eq!( + word_wrapper, wrapped, + "WordWrapper should deect the line cannot be broken on word boundary and \ + break it at line width limit." + ); + assert_eq!(line_truncator, vec![&text[..width]]); + } + + #[test] + fn line_composer_long_sentence() { + let width = 20; + let text = + "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab c d e f g h i j k l m n o"; + 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, text, width as u16); + let (word_wrapper_multi_space, _) = + run_composer(Composer::WordWrapper, text_multi_space, width as u16); + let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width as u16); + + let word_wrapped = vec![ + "abcd efghij", + "klmnopabcd efgh", + "ijklmnopabcdefg", + "hijkl mnopab c d e f", + "g h i j k l m n o", + ]; + assert_eq!(word_wrapper_single_space, word_wrapped); + assert_eq!(word_wrapper_multi_space, word_wrapped); + + assert_eq!(line_truncator, vec![&text[..width]]); + } + + #[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, text, width); + let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width); + + let expected: Vec<&str> = Vec::new(); + assert_eq!(word_wrapper, expected); + assert_eq!(line_truncator, expected); + } + + #[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, 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!(word_wrapper, expected); + assert_eq!(line_truncator, vec!["a"]); + } + + #[test] + fn line_composer_max_line_width_of_1_double_width_characters() { + let width = 1; + let text = "コンピュータ上で文字を扱う場合、典型的には文字\naaaによる通信を行う場合にその\ + 両端点では、"; + let (word_wrapper, _) = run_composer(Composer::WordWrapper, 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"]); + } + + /// Tests WordWrapper with words some of which exceed line length and some not. + #[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, text, width); + assert_eq!( + word_wrapper, + vec![ + "abcd efghij", + "klmnopabcdefghijklmn", + "opabcdefghijkl", + "mnopab cdefghi j", + "klmno", + ] + ) + } + + #[test] + fn line_composer_double_width_chars() { + let width = 20; + let text = "コンピュータ上で文字を扱う場合、典型的には文字による通信を行う場合にその両端点\ + では、"; + let (word_wrapper, word_wrapper_width) = run_composer(Composer::WordWrapper, &text, width); + let (line_truncator, _) = run_composer(Composer::LineTruncator, &text, width); + assert_eq!(line_truncator, vec!["コンピュータ上で文字"]); + let wrapped = vec![ + "コンピュータ上で文字", + "を扱う場合、典型的に", + "は文字による通信を行", + "う場合にその両端点で", + "は、", + ]; + assert_eq!(word_wrapper, wrapped); + assert_eq!(word_wrapper_width, vec![width, width, width, width, 4]); + } + + #[test] + fn line_composer_leading_whitespace_removal() { + let width = 20; + let text = "AAAAAAAAAAAAAAAAAAAA AAA"; + let (word_wrapper, _) = run_composer(Composer::WordWrapper, text, width); + let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width); + assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAAAAAAA", "AAA",]); + assert_eq!(line_truncator, vec!["AAAAAAAAAAAAAAAAAAAA"]); + } + + /// Tests truncation of leading whitespace. + #[test] + fn line_composer_lots_of_spaces() { + let width = 20; + let text = " "; + let (word_wrapper, _) = run_composer(Composer::WordWrapper, text, width); + let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width); + assert_eq!(word_wrapper, vec![""]); + assert_eq!(line_truncator, vec![" "]); + } + + /// Tests an input starting with a letter, folowed by spaces - some of the behaviour is + /// incidental. + #[test] + fn line_composer_char_plus_lots_of_spaces() { + let width = 20; + let text = "a "; + let (word_wrapper, _) = run_composer(Composer::WordWrapper, 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!(word_wrapper, vec!["a", ""]); + assert_eq!(line_truncator, vec!["a "]); + } + + #[test] + fn line_composer_word_wrapper_double_width_chars_mixed_with_spaces() { + let width = 20; + // Japanese seems not to use spaces but we should break on spaces anyway... We're using it + // to test double-width chars. + // You are more than welcome to add word boundary detection based of alterations of + // 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, text, width); + assert_eq!( + word_wrapper, + vec![ + "コンピュ", + "ータ上で文字を扱う場", + "合、 典型的には文", + "字による 通信を行", + "う場合にその両端点で", + "は、", + ] + ); + // Odd-sized lines have a space in them. + assert_eq!(word_wrapper_width, vec![8, 20, 17, 17, 20, 4]); + } + + /// Ensure words separated by nbsp are wrapped as if they were a single one. + #[test] + fn line_composer_word_wrapper_nbsp() { + let width = 20; + let text = "AAAAAAAAAAAAAAA AAAA\u{00a0}AAA"; + let (word_wrapper, _) = run_composer(Composer::WordWrapper, 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, &text_space, width); + assert_eq!(word_wrapper_space, vec!["AAAAAAAAAAAAAAA AAAA", "AAA",]); + } +} diff --git a/tests/paragraph.rs b/tests/paragraph.rs index bd332a8a..9283f3c6 100644 --- a/tests/paragraph.rs +++ b/tests/paragraph.rs @@ -4,43 +4,81 @@ extern crate tui; use tui::backend::TestBackend; use tui::buffer::Buffer; +use tui::layout::Alignment; use tui::widgets::{Block, Borders, Paragraph, Text, Widget}; use tui::Terminal; +const SAMPLE_STRING: &str = + "The library is based on the principle of immediate rendering with \ + intermediate buffers. This means that at each new frame you should build all widgets that are \ + supposed to be part of the UI. While providing a great flexibility for rich and \ + interactive UI, this may introduce overhead for highly dynamic content."; + #[test] -fn paragraph_render_single_width() { - let backend = TestBackend::new(20, 10); - let mut terminal = Terminal::new(backend).unwrap(); +fn paragraph_render_wrap() { + let render = |alignment| { + let backend = TestBackend::new(20, 10); + let mut terminal = Terminal::new(backend).unwrap(); - let s = "The library is based on the principle of immediate rendering with intermediate \ - buffers. This means that at each new frame you should build all widgets that are \ - supposed to be part of the UI. While providing a great flexibility for rich and \ - interactive UI, this may introduce overhead for highly dynamic content."; + terminal + .draw(|mut f| { + let size = f.size(); + let text = [Text::raw(SAMPLE_STRING)]; + Paragraph::new(text.iter()) + .block(Block::default().borders(Borders::ALL)) + .alignment(alignment) + .wrap(true) + .render(&mut f, size); + }) + .unwrap(); + terminal.backend().buffer().clone() + }; - terminal - .draw(|mut f| { - let size = f.size(); - let text = [Text::raw(s)]; - Paragraph::new(text.iter()) - .block(Block::default().borders(Borders::ALL)) - .wrap(true) - .render(&mut f, size); - }) - .unwrap(); - - let expected = 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│", - "└──────────────────┘", - ]); - assert_eq!(&expected, terminal.backend().buffer()); + assert_eq!( + render(Alignment::Left), + Buffer::with_lines(vec![ + "┌──────────────────┐", + "│The library is │", + "│based on the │", + "│principle of │", + "│immediate │", + "│rendering with │", + "│intermediate │", + "│buffers. This │", + "│means that at each│", + "└──────────────────┘", + ]) + ); + assert_eq!( + render(Alignment::Right), + Buffer::with_lines(vec![ + "┌──────────────────┐", + "│ The library is│", + "│ based on the│", + "│ principle of│", + "│ immediate│", + "│ rendering with│", + "│ intermediate│", + "│ buffers. This│", + "│means that at each│", + "└──────────────────┘", + ]) + ); + assert_eq!( + render(Alignment::Center), + Buffer::with_lines(vec![ + "┌──────────────────┐", + "│ The library is │", + "│ based on the │", + "│ principle of │", + "│ immediate │", + "│ rendering with │", + "│ intermediate │", + "│ buffers. This │", + "│means that at each│", + "└──────────────────┘", + ]) + ); } #[test] @@ -49,7 +87,6 @@ fn paragraph_render_double_width() { let mut terminal = Terminal::new(backend).unwrap(); let s = "コンピュータ上で文字を扱う場合、典型的には文字による通信を行う場合にその両端点では、"; - terminal .draw(|mut f| { let size = f.size(); @@ -85,7 +122,6 @@ fn paragraph_render_mixed_width() { let mut terminal = Terminal::new(backend).unwrap(); let s = "aコンピュータ上で文字を扱う場合、"; - terminal .draw(|mut f| { let size = f.size();