use tui::{ backend::TestBackend, buffer::Buffer, layout::{Constraint, Unit}, 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::eq(5), Constraint::eq(5), Constraint::eq(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( vec![Constraint::eq(0), Constraint::eq(0), Constraint::eq(0)], Buffer::with_lines(vec![ "┌────────────────────────────┐", "│ │", "│ │", "│ │", "│ │", "│ │", "│ │", "│ │", "│ │", "└────────────────────────────┘", ]), ); // columns of 1 width trim test_case( vec![Constraint::eq(1), Constraint::eq(1), Constraint::eq(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( vec![Constraint::eq(8), Constraint::eq(8), Constraint::eq(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( vec![ Constraint::eq(Unit::Percentage(0)), Constraint::eq(Unit::Percentage(0)), Constraint::eq(Unit::Percentage(0)), ], Buffer::with_lines(vec![ "┌────────────────────────────┐", "│ │", "│ │", "│ │", "│ │", "│ │", "│ │", "│ │", "│ │", "└────────────────────────────┘", ]), ); // columns of not enough width trims the data test_case( vec![ Constraint::eq(Unit::Percentage(11)), Constraint::eq(Unit::Percentage(11)), Constraint::eq(Unit::Percentage(11)), ], Buffer::with_lines(vec![ "┌────────────────────────────┐", "│HeaHeaHea │", "│ │", "│RowRowRow │", "│RowRowRow │", "│RowRowRow │", "│RowRowRow │", "│ │", "│ │", "└────────────────────────────┘", ]), ); // columns of large width just before pushing a column off test_case( vec![ Constraint::eq(Unit::Percentage(33)), Constraint::eq(Unit::Percentage(33)), Constraint::eq(Unit::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( vec![ Constraint::eq(Unit::Percentage(50)), Constraint::eq(Unit::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( vec![ Constraint::eq(Unit::Percentage(0)), Constraint::eq(0), Constraint::eq(Unit::Percentage(0)), ], Buffer::with_lines(vec![ "┌────────────────────────────┐", "│ │", "│ │", "│ │", "│ │", "│ │", "│ │", "│ │", "│ │", "└────────────────────────────┘", ]), ); // columns of not enough width trims the data test_case( vec![ Constraint::eq(Unit::Percentage(11)), Constraint::eq(20), Constraint::eq(Unit::Percentage(11)), ], Buffer::with_lines(vec![ "┌────────────────────────────┐", "│Hea Head2 He │", "│ │", "│Row Row12 Ro │", "│Row Row22 Ro │", "│Row Row32 Ro │", "│Row Row42 Ro │", "│ │", "│ │", "└────────────────────────────┘", ]), ); // columns of large width just before pushing a column off test_case( vec![ Constraint::eq(Unit::Percentage(33)), Constraint::eq(10), Constraint::eq(Unit::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( vec![ Constraint::eq(Unit::Percentage(60)), Constraint::eq(10), Constraint::eq(Unit::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( vec![ Constraint::eq(Unit::Ratio(0, 1)), Constraint::eq(Unit::Ratio(0, 1)), Constraint::eq(Unit::Ratio(0, 1)), ], Buffer::with_lines(vec![ "┌────────────────────────────┐", "│ │", "│ │", "│ │", "│ │", "│ │", "│ │", "│ │", "│ │", "└────────────────────────────┘", ]), ); // columns of not enough width trims the data test_case( vec![ Constraint::eq(Unit::Ratio(1, 9)), Constraint::eq(Unit::Ratio(1, 9)), Constraint::eq(Unit::Ratio(1, 9)), ], Buffer::with_lines(vec![ "┌────────────────────────────┐", "│HeaHeaHea │", "│ │", "│RowRowRow │", "│RowRowRow │", "│RowRowRow │", "│RowRowRow │", "│ │", "│ │", "└────────────────────────────┘", ]), ); // columns of large width just before pushing a column off test_case( vec![ Constraint::eq(Unit::Ratio(1, 3)), Constraint::eq(Unit::Ratio(1, 3)), Constraint::eq(Unit::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( vec![ Constraint::eq(Unit::Ratio(1, 2)), Constraint::eq(Unit::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::eq(5), Constraint::eq(5), Constraint::eq(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::eq(6), Constraint::eq(6), Constraint::eq(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::eq(6), Constraint::eq(6), Constraint::eq(6)]) .column_spacing(1); f.render_widget(table, size); }) .unwrap(); let expected = Buffer::with_lines(vec![ "│Head1 Head2 Head3 │", "│ │", "│ │", "│ │", ]); terminal.backend().assert_buffer(&expected); } #[test] fn widgets_table_columns_dont_panic() { let test_case = |state: &mut TableState, table: Table, width: u16| { let backend = TestBackend::new(width, 8); let mut terminal = Terminal::new(backend).unwrap(); terminal .draw(|f| { let size = f.size(); f.render_stateful_widget(table, size, state); }) .unwrap(); }; // based on https://github.com/fdehau/tui-rs/issues/470#issuecomment-852562848 let table1_width = 98; let table1 = Table::new(vec![Row::new(vec!["r1", "r2", "r3", "r4"])]) .header(Row::new(vec!["h1", "h2", "h3", "h4"])) .block(Block::default().borders(Borders::ALL)) .highlight_symbol(">> ") .column_spacing(1) .widths([ Constraint::eq(Unit::Percentage(15)), Constraint::eq(Unit::Percentage(15)), Constraint::eq(Unit::Percentage(25)), Constraint::eq(Unit::Percentage(45)), ]); let mut state = TableState::default(); // select first, which would cause a panic before fix state.select(Some(0)); test_case(&mut state, table1.clone(), table1_width); } #[test] fn widgets_table_should_clamp_offset_if_rows_are_removed() { let backend = TestBackend::new(30, 8); let mut terminal = Terminal::new(backend).unwrap(); let mut state = TableState::default(); // render with 6 items => offset will be at 2 state.select(Some(5)); terminal .draw(|f| { let size = f.size(); let table = Table::new(vec![ Row::new(vec!["Row01", "Row02", "Row03"]), Row::new(vec!["Row11", "Row12", "Row13"]), Row::new(vec!["Row21", "Row22", "Row23"]), Row::new(vec!["Row31", "Row32", "Row33"]), Row::new(vec!["Row41", "Row42", "Row43"]), Row::new(vec!["Row51", "Row52", "Row53"]), ]) .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1)) .block(Block::default().borders(Borders::ALL)) .widths([Constraint::eq(5), Constraint::eq(5), Constraint::eq(5)]) .column_spacing(1); f.render_stateful_widget(table, size, &mut state); }) .unwrap(); let expected = Buffer::with_lines(vec![ "┌────────────────────────────┐", "│Head1 Head2 Head3 │", "│ │", "│Row21 Row22 Row23 │", "│Row31 Row32 Row33 │", "│Row41 Row42 Row43 │", "│Row51 Row52 Row53 │", "└────────────────────────────┘", ]); terminal.backend().assert_buffer(&expected); // render with 1 item => offset will be at 1 state.select(Some(1)); terminal .draw(|f| { let size = f.size(); let table = Table::new(vec![Row::new(vec!["Row31", "Row32", "Row33"])]) .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1)) .block(Block::default().borders(Borders::ALL)) .widths([Constraint::eq(5), Constraint::eq(5), Constraint::eq(5)]) .column_spacing(1); f.render_stateful_widget(table, size, &mut state); }) .unwrap(); let expected = Buffer::with_lines(vec![ "┌────────────────────────────┐", "│Head1 Head2 Head3 │", "│ │", "│Row31 Row32 Row33 │", "│ │", "│ │", "│ │", "└────────────────────────────┘", ]); terminal.backend().assert_buffer(&expected); }