mirror of
https://github.com/ratatui/ratatui.git
synced 2025-09-29 05:51:57 +00:00
Paragraph: word wrapping
This commit is contained in:
parent
063ab2f87d
commit
10642d0e04
@ -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') {
|
||||
|
@ -7,6 +7,7 @@ mod chart;
|
||||
mod gauge;
|
||||
mod list;
|
||||
mod paragraph;
|
||||
mod reflow;
|
||||
mod sparkline;
|
||||
mod table;
|
||||
mod tabs;
|
||||
|
@ -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<Item = &'t Text<'t>>,
|
||||
{
|
||||
@ -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<Item = (&'a str, Style)>>(
|
||||
styled: &mut MultiPeek<I>,
|
||||
) -> 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<dyn LineComposer> = 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
446
src/widgets/reflow.rs
Normal file
446
src/widgets/reflow.rs
Normal file
@ -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<Item = Styled<'a>>,
|
||||
max_line_width: u16,
|
||||
current_line: Vec<Styled<'a>>,
|
||||
next_line: Vec<Styled<'a>>,
|
||||
}
|
||||
|
||||
impl<'a, 'b> WordWrapper<'a, 'b> {
|
||||
pub fn new(
|
||||
symbols: &'b mut Iterator<Item = Styled<'a>>,
|
||||
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<Item = Styled<'a>>,
|
||||
max_line_width: u16,
|
||||
current_line: Vec<Styled<'a>>,
|
||||
}
|
||||
|
||||
impl<'a, 'b> LineTruncator<'a, 'b> {
|
||||
pub fn new(
|
||||
symbols: &'b mut Iterator<Item = Styled<'a>>,
|
||||
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<String>, Vec<u16>) {
|
||||
let style = Default::default();
|
||||
let mut styled = UnicodeSegmentation::graphemes(text, true).map(|g| Styled(g, style));
|
||||
let mut composer: Box<dyn LineComposer> = 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::<String>();
|
||||
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",]);
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
Loading…
x
Reference in New Issue
Block a user