From 35606a3daa7ee273845a12f3e03728e0ae23928e Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Sun, 9 May 2021 17:52:55 +0900 Subject: Inline tui as helix-tui fork. We only rely on some of the rendering primitives and implement our Cursive-style compositor on top. --- helix-tui/tests/terminal.rs | 36 ++ helix-tui/tests/widgets_block.rs | 213 +++++++++++ helix-tui/tests/widgets_list.rs | 88 +++++ helix-tui/tests/widgets_paragraph.rs | 220 +++++++++++ helix-tui/tests/widgets_table.rs | 717 +++++++++++++++++++++++++++++++++++ 5 files changed, 1274 insertions(+) create mode 100644 helix-tui/tests/terminal.rs create mode 100644 helix-tui/tests/widgets_block.rs create mode 100644 helix-tui/tests/widgets_list.rs create mode 100644 helix-tui/tests/widgets_paragraph.rs create mode 100644 helix-tui/tests/widgets_table.rs (limited to 'helix-tui/tests') diff --git a/helix-tui/tests/terminal.rs b/helix-tui/tests/terminal.rs new file mode 100644 index 00000000..4734dd9a --- /dev/null +++ b/helix-tui/tests/terminal.rs @@ -0,0 +1,36 @@ +use helix_tui::{ + backend::{Backend, TestBackend}, + layout::Rect, + widgets::Paragraph, + Terminal, +}; +use std::error::Error; + +#[test] +fn terminal_buffer_size_should_be_limited() { + let backend = TestBackend::new(400, 400); + let terminal = Terminal::new(backend).unwrap(); + let size = terminal.backend().size().unwrap(); + assert_eq!(size.width, 255); + assert_eq!(size.height, 255); +} + +// #[test] +// fn terminal_draw_returns_the_completed_frame() -> Result<(), Box> { +// let backend = TestBackend::new(10, 10); +// let mut terminal = Terminal::new(backend)?; +// let frame = terminal.draw(|f| { +// let paragrah = Paragraph::new("Test"); +// f.render_widget(paragrah, f.size()); +// })?; +// assert_eq!(frame.buffer.get(0, 0).symbol, "T"); +// assert_eq!(frame.area, Rect::new(0, 0, 10, 10)); +// terminal.backend_mut().resize(8, 8); +// let frame = terminal.draw(|f| { +// let paragrah = Paragraph::new("test"); +// f.render_widget(paragrah, f.size()); +// })?; +// assert_eq!(frame.buffer.get(0, 0).symbol, "t"); +// assert_eq!(frame.area, Rect::new(0, 0, 8, 8)); +// Ok(()) +// } diff --git a/helix-tui/tests/widgets_block.rs b/helix-tui/tests/widgets_block.rs new file mode 100644 index 00000000..8aaf905b --- /dev/null +++ b/helix-tui/tests/widgets_block.rs @@ -0,0 +1,213 @@ +use helix_tui::{ + backend::TestBackend, + buffer::Buffer, + layout::Rect, + style::{Color, Style}, + text::Span, + widgets::{Block, Borders}, + Terminal, +}; + +#[test] +fn widgets_block_renders() { + let backend = TestBackend::new(10, 10); + let mut terminal = Terminal::new(backend).unwrap(); + terminal + .draw(|f| { + let block = Block::default() + .title(Span::styled("Title", Style::default().fg(Color::LightBlue))) + .borders(Borders::ALL); + f.render_widget( + block, + Rect { + x: 0, + y: 0, + width: 8, + height: 8, + }, + ); + }) + .unwrap(); + let mut expected = Buffer::with_lines(vec![ + "┌Title─┐ ", + "│ │ ", + "│ │ ", + "│ │ ", + "│ │ ", + "│ │ ", + "│ │ ", + "└──────┘ ", + " ", + " ", + ]); + for x in 1..=5 { + expected.get_mut(x, 0).set_fg(Color::LightBlue); + } + terminal.backend().assert_buffer(&expected); +} + +#[test] +fn widgets_block_renders_on_small_areas() { + let test_case = |block, area: Rect, expected| { + let backend = TestBackend::new(area.width, area.height); + let mut terminal = Terminal::new(backend).unwrap(); + terminal + .draw(|f| { + f.render_widget(block, area); + }) + .unwrap(); + terminal.backend().assert_buffer(&expected); + }; + + let one_cell_test_cases = [ + (Borders::NONE, "T"), + (Borders::LEFT, "│"), + (Borders::TOP, "T"), + (Borders::RIGHT, "│"), + (Borders::BOTTOM, "T"), + (Borders::ALL, "┌"), + ]; + for (borders, symbol) in one_cell_test_cases.iter().cloned() { + test_case( + Block::default().title("Test").borders(borders), + Rect { + x: 0, + y: 0, + width: 0, + height: 0, + }, + Buffer::empty(Rect { + x: 0, + y: 0, + width: 0, + height: 0, + }), + ); + test_case( + Block::default().title("Test").borders(borders), + Rect { + x: 0, + y: 0, + width: 1, + height: 0, + }, + Buffer::empty(Rect { + x: 0, + y: 0, + width: 1, + height: 0, + }), + ); + test_case( + Block::default().title("Test").borders(borders), + Rect { + x: 0, + y: 0, + width: 0, + height: 1, + }, + Buffer::empty(Rect { + x: 0, + y: 0, + width: 0, + height: 1, + }), + ); + test_case( + Block::default().title("Test").borders(borders), + Rect { + x: 0, + y: 0, + width: 1, + height: 1, + }, + Buffer::with_lines(vec![symbol]), + ); + } + test_case( + Block::default().title("Test").borders(Borders::LEFT), + Rect { + x: 0, + y: 0, + width: 4, + height: 1, + }, + Buffer::with_lines(vec!["│Tes"]), + ); + test_case( + Block::default().title("Test").borders(Borders::RIGHT), + Rect { + x: 0, + y: 0, + width: 4, + height: 1, + }, + Buffer::with_lines(vec!["Tes│"]), + ); + test_case( + Block::default().title("Test").borders(Borders::RIGHT), + Rect { + x: 0, + y: 0, + width: 4, + height: 1, + }, + Buffer::with_lines(vec!["Tes│"]), + ); + test_case( + Block::default() + .title("Test") + .borders(Borders::LEFT | Borders::RIGHT), + Rect { + x: 0, + y: 0, + width: 4, + height: 1, + }, + Buffer::with_lines(vec!["│Te│"]), + ); + test_case( + Block::default().title("Test").borders(Borders::TOP), + Rect { + x: 0, + y: 0, + width: 4, + height: 1, + }, + Buffer::with_lines(vec!["Test"]), + ); + test_case( + Block::default().title("Test").borders(Borders::TOP), + Rect { + x: 0, + y: 0, + width: 5, + height: 1, + }, + Buffer::with_lines(vec!["Test─"]), + ); + test_case( + Block::default() + .title("Test") + .borders(Borders::LEFT | Borders::TOP), + Rect { + x: 0, + y: 0, + width: 5, + height: 1, + }, + Buffer::with_lines(vec!["┌Test"]), + ); + test_case( + Block::default() + .title("Test") + .borders(Borders::LEFT | Borders::TOP), + Rect { + x: 0, + y: 0, + width: 6, + height: 1, + }, + Buffer::with_lines(vec!["┌Test─"]), + ); +} diff --git a/helix-tui/tests/widgets_list.rs b/helix-tui/tests/widgets_list.rs new file mode 100644 index 00000000..e59accd8 --- /dev/null +++ b/helix-tui/tests/widgets_list.rs @@ -0,0 +1,88 @@ +use helix_tui::{ + backend::TestBackend, + buffer::Buffer, + layout::Rect, + style::{Color, Style}, + symbols, + widgets::{Block, Borders, List, ListItem, ListState}, + Terminal, +}; + +#[test] +fn widgets_list_should_highlight_the_selected_item() { + let backend = TestBackend::new(10, 3); + let mut terminal = Terminal::new(backend).unwrap(); + let mut state = ListState::default(); + state.select(Some(1)); + terminal + .draw(|f| { + let size = f.size(); + let items = vec![ + ListItem::new("Item 1"), + ListItem::new("Item 2"), + ListItem::new("Item 3"), + ]; + let list = List::new(items) + .highlight_style(Style::default().bg(Color::Yellow)) + .highlight_symbol(">> "); + f.render_stateful_widget(list, size, &mut state); + }) + .unwrap(); + let mut expected = Buffer::with_lines(vec![" Item 1 ", ">> Item 2 ", " Item 3 "]); + for x in 0..10 { + expected.get_mut(x, 1).set_bg(Color::Yellow); + } + terminal.backend().assert_buffer(&expected); +} + +#[test] +fn widgets_list_should_truncate_items() { + let backend = TestBackend::new(10, 2); + let mut terminal = Terminal::new(backend).unwrap(); + + struct TruncateTestCase<'a> { + selected: Option, + items: Vec>, + expected: Buffer, + } + + let cases = vec![ + // An item is selected + TruncateTestCase { + selected: Some(0), + items: vec![ + ListItem::new("A very long line"), + ListItem::new("A very long line"), + ], + expected: Buffer::with_lines(vec![ + format!(">> A ve{} ", symbols::line::VERTICAL), + format!(" A ve{} ", symbols::line::VERTICAL), + ]), + }, + // No item is selected + TruncateTestCase { + selected: None, + items: vec![ + ListItem::new("A very long line"), + ListItem::new("A very long line"), + ], + expected: Buffer::with_lines(vec![ + format!("A very {} ", symbols::line::VERTICAL), + format!("A very {} ", symbols::line::VERTICAL), + ]), + }, + ]; + for case in cases { + let mut state = ListState::default(); + state.select(case.selected); + terminal + .draw(|f| { + let list = List::new(case.items.clone()) + .block(Block::default().borders(Borders::RIGHT)) + .highlight_symbol(">> "); + f.render_stateful_widget(list, Rect::new(0, 0, 8, 2), &mut state); + }) + .unwrap(); + terminal.backend().assert_buffer(&case.expected); + } +} diff --git a/helix-tui/tests/widgets_paragraph.rs b/helix-tui/tests/widgets_paragraph.rs new file mode 100644 index 00000000..33d693d8 --- /dev/null +++ b/helix-tui/tests/widgets_paragraph.rs @@ -0,0 +1,220 @@ +use helix_tui::{ + backend::TestBackend, + buffer::Buffer, + layout::Alignment, + text::{Span, Spans, Text}, + widgets::{Block, Borders, Paragraph, Wrap}, + 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 widgets_paragraph_can_wrap_its_content() { + 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 }); + f.render_widget(paragraph, size); + }) + .unwrap(); + terminal.backend().assert_buffer(&expected); + }; + + test_case( + Alignment::Left, + Buffer::with_lines(vec![ + "┌──────────────────┐", + "│The library is │", + "│based on the │", + "│principle of │", + "│immediate │", + "│rendering with │", + "│intermediate │", + "│buffers. This │", + "│means that at each│", + "└──────────────────┘", + ]), + ); + test_case( + Alignment::Right, + Buffer::with_lines(vec![ + "┌──────────────────┐", + "│ The library is│", + "│ based on the│", + "│ principle of│", + "│ immediate│", + "│ rendering with│", + "│ intermediate│", + "│ buffers. This│", + "│means that at each│", + "└──────────────────┘", + ]), + ); + test_case( + 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] +fn widgets_paragraph_renders_double_width_graphemes() { + let backend = TestBackend::new(10, 10); + let mut terminal = Terminal::new(backend).unwrap(); + + let s = "コンピュータ上で文字を扱う場合、典型的には文字による通信を行う場合にその両端点では、"; + terminal + .draw(|f| { + let size = f.size(); + let text = vec![Spans::from(s)]; + let paragraph = Paragraph::new(text) + .block(Block::default().borders(Borders::ALL)) + .wrap(Wrap { trim: true }); + f.render_widget(paragraph, size); + }) + .unwrap(); + + let expected = Buffer::with_lines(vec![ + "┌────────┐", + "│コンピュ│", + "│ータ上で│", + "│文字を扱│", + "│う場合、│", + "│典型的に│", + "│は文字に│", + "│よる通信│", + "│を行う場│", + "└────────┘", + ]); + terminal.backend().assert_buffer(&expected); +} + +#[test] +fn widgets_paragraph_renders_mixed_width_graphemes() { + let backend = TestBackend::new(10, 7); + let mut terminal = Terminal::new(backend).unwrap(); + + let s = "aコンピュータ上で文字を扱う場合、"; + terminal + .draw(|f| { + let size = f.size(); + let text = vec![Spans::from(s)]; + let paragraph = Paragraph::new(text) + .block(Block::default().borders(Borders::ALL)) + .wrap(Wrap { trim: true }); + f.render_widget(paragraph, size); + }) + .unwrap(); + + let expected = Buffer::with_lines(vec![ + // The internal width is 8 so only 4 slots for double-width characters. + "┌────────┐", + "│aコンピ │", // Here we have 1 latin character so only 3 double-width ones can fit. + "│ュータ上│", + "│で文字を│", + "│扱う場合│", + "│、 │", + "└────────┘", + ]); + terminal.backend().assert_buffer(&expected); +} + +#[test] +fn widgets_paragraph_can_wrap_with_a_trailing_nbsp() { + let nbsp: &str = "\u{00a0}"; + let line = Spans::from(vec![Span::raw("NBSP"), Span::raw(nbsp)]); + let backend = TestBackend::new(20, 3); + let mut terminal = Terminal::new(backend).unwrap(); + let expected = Buffer::with_lines(vec![ + "┌──────────────────┐", + "│NBSP\u{00a0} │", + "└──────────────────┘", + ]); + terminal + .draw(|f| { + let size = f.size(); + + let paragraph = Paragraph::new(line).block(Block::default().borders(Borders::ALL)); + f.render_widget(paragraph, size); + }) + .unwrap(); + terminal.backend().assert_buffer(&expected); +} +#[test] +fn widgets_paragraph_can_scroll_horizontally() { + let test_case = |alignment, scroll, expected| { + let backend = TestBackend::new(20, 10); + let mut terminal = Terminal::new(backend).unwrap(); + + terminal + .draw(|f| { + let size = f.size(); + let text = Text::from( + "段落现在可以水平滚动了!\nParagraph can scroll horizontally!\nShort line", + ); + let paragraph = Paragraph::new(text) + .block(Block::default().borders(Borders::ALL)) + .alignment(alignment) + .scroll(scroll); + f.render_widget(paragraph, size); + }) + .unwrap(); + terminal.backend().assert_buffer(&expected); + }; + + test_case( + Alignment::Left, + (0, 7), + Buffer::with_lines(vec![ + "┌──────────────────┐", + "│在可以水平滚动了!│", + "│ph can scroll hori│", + "│ine │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "└──────────────────┘", + ]), + ); + // only support Alignment::Left + test_case( + Alignment::Right, + (0, 7), + Buffer::with_lines(vec![ + "┌──────────────────┐", + "│段落现在可以水平滚│", + "│Paragraph can scro│", + "│ Short line│", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "└──────────────────┘", + ]), + ); +} diff --git a/helix-tui/tests/widgets_table.rs b/helix-tui/tests/widgets_table.rs new file mode 100644 index 00000000..0e0b3003 --- /dev/null +++ b/helix-tui/tests/widgets_table.rs @@ -0,0 +1,717 @@ +use helix_tui::{ + backend::TestBackend, + buffer::Buffer, + layout::Constraint, + style::{Color, Modifier, Style}, + text::{Span, Spans}, + widgets::{Block, Borders, Cell, Row, Table, TableState}, + Terminal, +}; + +#[test] +fn widgets_table_column_spacing_can_be_changed() { + let test_case = |column_spacing, expected| { + let backend = TestBackend::new(30, 10); + let mut terminal = Terminal::new(backend).unwrap(); + + terminal + .draw(|f| { + let size = f.size(); + let table = Table::new(vec![ + Row::new(vec!["Row11", "Row12", "Row13"]), + Row::new(vec!["Row21", "Row22", "Row23"]), + Row::new(vec!["Row31", "Row32", "Row33"]), + Row::new(vec!["Row41", "Row42", "Row43"]), + ]) + .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1)) + .block(Block::default().borders(Borders::ALL)) + .widths(&[ + Constraint::Length(5), + Constraint::Length(5), + Constraint::Length(5), + ]) + .column_spacing(column_spacing); + f.render_widget(table, size); + }) + .unwrap(); + terminal.backend().assert_buffer(&expected); + }; + + // no space between columns + test_case( + 0, + Buffer::with_lines(vec![ + "┌────────────────────────────┐", + "│Head1Head2Head3 │", + "│ │", + "│Row11Row12Row13 │", + "│Row21Row22Row23 │", + "│Row31Row32Row33 │", + "│Row41Row42Row43 │", + "│ │", + "│ │", + "└────────────────────────────┘", + ]), + ); + + // one space between columns + test_case( + 1, + Buffer::with_lines(vec![ + "┌────────────────────────────┐", + "│Head1 Head2 Head3 │", + "│ │", + "│Row11 Row12 Row13 │", + "│Row21 Row22 Row23 │", + "│Row31 Row32 Row33 │", + "│Row41 Row42 Row43 │", + "│ │", + "│ │", + "└────────────────────────────┘", + ]), + ); + + // enough space to just not hide the third column + test_case( + 6, + Buffer::with_lines(vec![ + "┌────────────────────────────┐", + "│Head1 Head2 Head3 │", + "│ │", + "│Row11 Row12 Row13 │", + "│Row21 Row22 Row23 │", + "│Row31 Row32 Row33 │", + "│Row41 Row42 Row43 │", + "│ │", + "│ │", + "└────────────────────────────┘", + ]), + ); + + // enough space to hide part of the third column + test_case( + 7, + Buffer::with_lines(vec![ + "┌────────────────────────────┐", + "│Head1 Head2 Head│", + "│ │", + "│Row11 Row12 Row1│", + "│Row21 Row22 Row2│", + "│Row31 Row32 Row3│", + "│Row41 Row42 Row4│", + "│ │", + "│ │", + "└────────────────────────────┘", + ]), + ); +} + +#[test] +fn widgets_table_columns_widths_can_use_fixed_length_constraints() { + let test_case = |widths, expected| { + let backend = TestBackend::new(30, 10); + let mut terminal = Terminal::new(backend).unwrap(); + + terminal + .draw(|f| { + let size = f.size(); + let table = Table::new(vec![ + Row::new(vec!["Row11", "Row12", "Row13"]), + Row::new(vec!["Row21", "Row22", "Row23"]), + Row::new(vec!["Row31", "Row32", "Row33"]), + Row::new(vec!["Row41", "Row42", "Row43"]), + ]) + .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1)) + .block(Block::default().borders(Borders::ALL)) + .widths(widths); + f.render_widget(table, size); + }) + .unwrap(); + terminal.backend().assert_buffer(&expected); + }; + + // columns of zero width show nothing + test_case( + &[ + Constraint::Length(0), + Constraint::Length(0), + Constraint::Length(0), + ], + Buffer::with_lines(vec![ + "┌────────────────────────────┐", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "└────────────────────────────┘", + ]), + ); + + // columns of 1 width trim + test_case( + &[ + Constraint::Length(1), + Constraint::Length(1), + Constraint::Length(1), + ], + Buffer::with_lines(vec![ + "┌────────────────────────────┐", + "│H H H │", + "│ │", + "│R R R │", + "│R R R │", + "│R R R │", + "│R R R │", + "│ │", + "│ │", + "└────────────────────────────┘", + ]), + ); + + // columns of large width just before pushing a column off + test_case( + &[ + Constraint::Length(8), + Constraint::Length(8), + Constraint::Length(8), + ], + Buffer::with_lines(vec![ + "┌────────────────────────────┐", + "│Head1 Head2 Head3 │", + "│ │", + "│Row11 Row12 Row13 │", + "│Row21 Row22 Row23 │", + "│Row31 Row32 Row33 │", + "│Row41 Row42 Row43 │", + "│ │", + "│ │", + "└────────────────────────────┘", + ]), + ); +} + +#[test] +fn widgets_table_columns_widths_can_use_percentage_constraints() { + let test_case = |widths, expected| { + let backend = TestBackend::new(30, 10); + let mut terminal = Terminal::new(backend).unwrap(); + + terminal + .draw(|f| { + let size = f.size(); + let table = Table::new(vec![ + Row::new(vec!["Row11", "Row12", "Row13"]), + Row::new(vec!["Row21", "Row22", "Row23"]), + Row::new(vec!["Row31", "Row32", "Row33"]), + Row::new(vec!["Row41", "Row42", "Row43"]), + ]) + .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1)) + .block(Block::default().borders(Borders::ALL)) + .widths(widths) + .column_spacing(0); + f.render_widget(table, size); + }) + .unwrap(); + terminal.backend().assert_buffer(&expected); + }; + + // columns of zero width show nothing + test_case( + &[ + Constraint::Percentage(0), + Constraint::Percentage(0), + Constraint::Percentage(0), + ], + Buffer::with_lines(vec![ + "┌────────────────────────────┐", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "└────────────────────────────┘", + ]), + ); + + // columns of not enough width trims the data + test_case( + &[ + Constraint::Percentage(11), + Constraint::Percentage(11), + Constraint::Percentage(11), + ], + Buffer::with_lines(vec![ + "┌────────────────────────────┐", + "│HeaHeaHea │", + "│ │", + "│RowRowRow │", + "│RowRowRow │", + "│RowRowRow │", + "│RowRowRow │", + "│ │", + "│ │", + "└────────────────────────────┘", + ]), + ); + + // columns of large width just before pushing a column off + test_case( + &[ + Constraint::Percentage(33), + Constraint::Percentage(33), + Constraint::Percentage(33), + ], + Buffer::with_lines(vec![ + "┌────────────────────────────┐", + "│Head1 Head2 Head3 │", + "│ │", + "│Row11 Row12 Row13 │", + "│Row21 Row22 Row23 │", + "│Row31 Row32 Row33 │", + "│Row41 Row42 Row43 │", + "│ │", + "│ │", + "└────────────────────────────┘", + ]), + ); + + // percentages summing to 100 should give equal widths + test_case( + &[Constraint::Percentage(50), Constraint::Percentage(50)], + Buffer::with_lines(vec![ + "┌────────────────────────────┐", + "│Head1 Head2 │", + "│ │", + "│Row11 Row12 │", + "│Row21 Row22 │", + "│Row31 Row32 │", + "│Row41 Row42 │", + "│ │", + "│ │", + "└────────────────────────────┘", + ]), + ); +} + +#[test] +fn widgets_table_columns_widths_can_use_mixed_constraints() { + let test_case = |widths, expected| { + let backend = TestBackend::new(30, 10); + let mut terminal = Terminal::new(backend).unwrap(); + + terminal + .draw(|f| { + let size = f.size(); + let table = Table::new(vec![ + Row::new(vec!["Row11", "Row12", "Row13"]), + Row::new(vec!["Row21", "Row22", "Row23"]), + Row::new(vec!["Row31", "Row32", "Row33"]), + Row::new(vec!["Row41", "Row42", "Row43"]), + ]) + .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1)) + .block(Block::default().borders(Borders::ALL)) + .widths(widths); + f.render_widget(table, size); + }) + .unwrap(); + terminal.backend().assert_buffer(&expected); + }; + + // columns of zero width show nothing + test_case( + &[ + Constraint::Percentage(0), + Constraint::Length(0), + Constraint::Percentage(0), + ], + Buffer::with_lines(vec![ + "┌────────────────────────────┐", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "└────────────────────────────┘", + ]), + ); + + // columns of not enough width trims the data + test_case( + &[ + Constraint::Percentage(11), + Constraint::Length(20), + Constraint::Percentage(11), + ], + Buffer::with_lines(vec![ + "┌────────────────────────────┐", + "│Hea Head2 Hea│", + "│ │", + "│Row Row12 Row│", + "│Row Row22 Row│", + "│Row Row32 Row│", + "│Row Row42 Row│", + "│ │", + "│ │", + "└────────────────────────────┘", + ]), + ); + + // columns of large width just before pushing a column off + test_case( + &[ + Constraint::Percentage(33), + Constraint::Length(10), + Constraint::Percentage(33), + ], + Buffer::with_lines(vec![ + "┌────────────────────────────┐", + "│Head1 Head2 Head3 │", + "│ │", + "│Row11 Row12 Row13 │", + "│Row21 Row22 Row23 │", + "│Row31 Row32 Row33 │", + "│Row41 Row42 Row43 │", + "│ │", + "│ │", + "└────────────────────────────┘", + ]), + ); + + // columns of large size (>100% total) hide the last column + test_case( + &[ + Constraint::Percentage(60), + Constraint::Length(10), + Constraint::Percentage(60), + ], + Buffer::with_lines(vec![ + "┌────────────────────────────┐", + "│Head1 Head2 │", + "│ │", + "│Row11 Row12 │", + "│Row21 Row22 │", + "│Row31 Row32 │", + "│Row41 Row42 │", + "│ │", + "│ │", + "└────────────────────────────┘", + ]), + ); +} + +#[test] +fn widgets_table_columns_widths_can_use_ratio_constraints() { + let test_case = |widths, expected| { + let backend = TestBackend::new(30, 10); + let mut terminal = Terminal::new(backend).unwrap(); + + terminal + .draw(|f| { + let size = f.size(); + let table = Table::new(vec![ + Row::new(vec!["Row11", "Row12", "Row13"]), + Row::new(vec!["Row21", "Row22", "Row23"]), + Row::new(vec!["Row31", "Row32", "Row33"]), + Row::new(vec!["Row41", "Row42", "Row43"]), + ]) + .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1)) + .block(Block::default().borders(Borders::ALL)) + .widths(widths) + .column_spacing(0); + f.render_widget(table, size); + }) + .unwrap(); + terminal.backend().assert_buffer(&expected); + }; + + // columns of zero width show nothing + test_case( + &[ + Constraint::Ratio(0, 1), + Constraint::Ratio(0, 1), + Constraint::Ratio(0, 1), + ], + Buffer::with_lines(vec![ + "┌────────────────────────────┐", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "└────────────────────────────┘", + ]), + ); + + // columns of not enough width trims the data + test_case( + &[ + Constraint::Ratio(1, 9), + Constraint::Ratio(1, 9), + Constraint::Ratio(1, 9), + ], + Buffer::with_lines(vec![ + "┌────────────────────────────┐", + "│HeaHeaHea │", + "│ │", + "│RowRowRow │", + "│RowRowRow │", + "│RowRowRow │", + "│RowRowRow │", + "│ │", + "│ │", + "└────────────────────────────┘", + ]), + ); + + // columns of large width just before pushing a column off + test_case( + &[ + Constraint::Ratio(1, 3), + Constraint::Ratio(1, 3), + Constraint::Ratio(1, 3), + ], + Buffer::with_lines(vec![ + "┌────────────────────────────┐", + "│Head1 Head2 Head3 │", + "│ │", + "│Row11 Row12 Row13 │", + "│Row21 Row22 Row23 │", + "│Row31 Row32 Row33 │", + "│Row41 Row42 Row43 │", + "│ │", + "│ │", + "└────────────────────────────┘", + ]), + ); + + // percentages summing to 100 should give equal widths + test_case( + &[Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)], + Buffer::with_lines(vec![ + "┌────────────────────────────┐", + "│Head1 Head2 │", + "│ │", + "│Row11 Row12 │", + "│Row21 Row22 │", + "│Row31 Row32 │", + "│Row41 Row42 │", + "│ │", + "│ │", + "└────────────────────────────┘", + ]), + ); +} + +#[test] +fn widgets_table_can_have_rows_with_multi_lines() { + let test_case = |state: &mut TableState, expected: Buffer| { + let backend = TestBackend::new(30, 8); + let mut terminal = Terminal::new(backend).unwrap(); + terminal + .draw(|f| { + let size = f.size(); + let table = Table::new(vec![ + Row::new(vec!["Row11", "Row12", "Row13"]), + Row::new(vec!["Row21", "Row22", "Row23"]).height(2), + Row::new(vec!["Row31", "Row32", "Row33"]), + Row::new(vec!["Row41", "Row42", "Row43"]).height(2), + ]) + .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1)) + .block(Block::default().borders(Borders::ALL)) + .highlight_symbol(">> ") + .widths(&[ + Constraint::Length(5), + Constraint::Length(5), + Constraint::Length(5), + ]) + .column_spacing(1); + f.render_stateful_widget(table, size, state); + }) + .unwrap(); + terminal.backend().assert_buffer(&expected); + }; + + let mut state = TableState::default(); + // no selection + test_case( + &mut state, + Buffer::with_lines(vec![ + "┌────────────────────────────┐", + "│Head1 Head2 Head3 │", + "│ │", + "│Row11 Row12 Row13 │", + "│Row21 Row22 Row23 │", + "│ │", + "│Row31 Row32 Row33 │", + "└────────────────────────────┘", + ]), + ); + + // select first + state.select(Some(0)); + test_case( + &mut state, + Buffer::with_lines(vec![ + "┌────────────────────────────┐", + "│ Head1 Head2 Head3 │", + "│ │", + "│>> Row11 Row12 Row13 │", + "│ Row21 Row22 Row23 │", + "│ │", + "│ Row31 Row32 Row33 │", + "└────────────────────────────┘", + ]), + ); + + // select second (we don't show partially the 4th row) + state.select(Some(1)); + test_case( + &mut state, + Buffer::with_lines(vec![ + "┌────────────────────────────┐", + "│ Head1 Head2 Head3 │", + "│ │", + "│ Row11 Row12 Row13 │", + "│>> Row21 Row22 Row23 │", + "│ │", + "│ Row31 Row32 Row33 │", + "└────────────────────────────┘", + ]), + ); + + // select 4th (we don't show partially the 1st row) + state.select(Some(3)); + test_case( + &mut state, + Buffer::with_lines(vec![ + "┌────────────────────────────┐", + "│ Head1 Head2 Head3 │", + "│ │", + "│ Row31 Row32 Row33 │", + "│>> Row41 Row42 Row43 │", + "│ │", + "│ │", + "└────────────────────────────┘", + ]), + ); +} + +#[test] +fn widgets_table_can_have_elements_styled_individually() { + let backend = TestBackend::new(30, 4); + let mut terminal = Terminal::new(backend).unwrap(); + let mut state = TableState::default(); + state.select(Some(0)); + terminal + .draw(|f| { + let size = f.size(); + let table = Table::new(vec![ + Row::new(vec!["Row11", "Row12", "Row13"]).style(Style::default().fg(Color::Green)), + Row::new(vec![ + Cell::from("Row21"), + Cell::from("Row22").style(Style::default().fg(Color::Yellow)), + Cell::from(Spans::from(vec![ + Span::raw("Row"), + Span::styled("23", Style::default().fg(Color::Blue)), + ])) + .style(Style::default().fg(Color::Red)), + ]) + .style(Style::default().fg(Color::LightGreen)), + ]) + .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1)) + .block(Block::default().borders(Borders::LEFT | Borders::RIGHT)) + .highlight_symbol(">> ") + .highlight_style(Style::default().add_modifier(Modifier::BOLD)) + .widths(&[ + Constraint::Length(6), + Constraint::Length(6), + Constraint::Length(6), + ]) + .column_spacing(1); + f.render_stateful_widget(table, size, &mut state); + }) + .unwrap(); + + let mut expected = Buffer::with_lines(vec![ + "│ Head1 Head2 Head3 │", + "│ │", + "│>> Row11 Row12 Row13 │", + "│ Row21 Row22 Row23 │", + ]); + // First row = row color + highlight style + for col in 1..=28 { + expected.get_mut(col, 2).set_style( + Style::default() + .fg(Color::Green) + .add_modifier(Modifier::BOLD), + ); + } + // Second row: + // 1. row color + for col in 1..=28 { + expected + .get_mut(col, 3) + .set_style(Style::default().fg(Color::LightGreen)); + } + // 2. cell color + for col in 11..=16 { + expected + .get_mut(col, 3) + .set_style(Style::default().fg(Color::Yellow)); + } + for col in 18..=23 { + expected + .get_mut(col, 3) + .set_style(Style::default().fg(Color::Red)); + } + // 3. text color + for col in 21..=22 { + expected + .get_mut(col, 3) + .set_style(Style::default().fg(Color::Blue)); + } + terminal.backend().assert_buffer(&expected); +} + +#[test] +fn widgets_table_should_render_even_if_empty() { + let backend = TestBackend::new(30, 4); + let mut terminal = Terminal::new(backend).unwrap(); + terminal + .draw(|f| { + let size = f.size(); + let table = Table::new(vec![]) + .header(Row::new(vec!["Head1", "Head2", "Head3"])) + .block(Block::default().borders(Borders::LEFT | Borders::RIGHT)) + .widths(&[ + Constraint::Length(6), + Constraint::Length(6), + Constraint::Length(6), + ]) + .column_spacing(1); + f.render_widget(table, size); + }) + .unwrap(); + + let expected = Buffer::with_lines(vec![ + "│Head1 Head2 Head3 │", + "│ │", + "│ │", + "│ │", + ]); + + terminal.backend().assert_buffer(&expected); +} -- cgit v1.2.3-70-g09d2