ratatui/tests/widgets_table.rs

987 lines
38 KiB
Rust
Raw Normal View History

#![allow(deprecated)]
use ratatui::{
backend::TestBackend,
buffer::Buffer,
layout::Constraint,
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Cell, HighlightSpacing, 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"]),
],
[
Constraint::Length(5),
Constraint::Length(5),
Constraint::Length(5),
],
)
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
.block(Block::default().borders(Borders::ALL))
feat: add stateful widgets Most widgets can be drawn directly based on the input parameters. However, some features may require some kind of associated state to be implemented. For example, the `List` widget can highlight the item currently selected. This can be translated in an offset, which is the number of elements to skip in order to have the selected item within the viewport currently allocated to this widget. The widget can therefore only provide the following behavior: whenever the selected item is out of the viewport scroll to a predefined position (make the selected item the last viewable item or the one in the middle). Nonetheless, if the widget has access to the last computed offset then it can implement a natural scrolling experience where the last offset is reused until the selected item is out of the viewport. To allow such behavior within the widgets, this commit introduces the following changes: - Add a `StatefulWidget` trait with an associated `State` type. Widgets that can take advantage of having a "memory" between two draw calls needs to implement this trait. - Add a `render_stateful_widget` method on `Frame` where the associated state is given as a parameter. The chosen approach is thus to let the developers manage their widgets' states themselves as they are already responsible for the lifecycle of the wigets (given that the crate exposes an immediate mode api). The following changes were also introduced: - `Widget::render` has been deleted. Developers should use `Frame::render_widget` instead. - `Widget::background` has been deleted. Developers should use `Buffer::set_background` instead. - `SelectableList` has been deleted. Developers can directly use `List` where `SelectableList` features have been back-ported.
2019-12-15 20:38:18 +00:00
.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![
"┌────────────────────────────┐",
feat(layout)!: Change `Flex::default()` (#881) This PR makes a number of simplifications to the layout and constraint features that were added after v0.25.0. For users upgrading from v0.25.0, the net effect of this PR (along with the other PRs) is the following: - New `Flex` modes have been added. - `Flex::Start` (new default) - `Flex::Center` - `Flex::End` - `Flex::SpaceAround` - `Flex::SpaceBetween` - `Flex::Legacy` (old default) - `Min(v)` grows to allocate excess space in all `Flex` modes instead of shrinking (except in `Flex::Legacy` where it retains old behavior). - `Fill(1)` grows to allocate excess space, growing equally with `Min(v)`. --- The following contains a summary of the changes in this PR and the motivation behind them. **`Flex`** - Removes `Flex::Stretch` - Renames `Flex::StretchLast` to `Flex::Legacy` **`Constraint`** - Removes `Fixed` - Makes `Min(v)` grow as much as possible everywhere (except `Flex::Legacy` where it retains the old behavior) - Makes `Min(v)` grow equally as `Fill(1)` while respecting `Min` lower bounds. When `Fill` and `Min` are used together, they both fill excess space equally. Allowing `Min(v)` to grow still allows users to build the same layouts as before with `Flex::Start` with no breaking changes to the behavior. This PR also removes the unstable feature `SegmentSize`. This is a breaking change to the behavior of constraints. If users want old behavior, they can use `Flex::Legacy`. ```rust Layout::vertical([Length(25), Length(25)]).flex(Flex::Legacy) ``` Users that have constraint that exceed the available space will probably not see any difference or see an improvement in their layouts. Any layout with `Min` will be identical in `Flex::Start` and `Flex::Legacy` so any layout with `Min` will not be breaking. Previously, `Table` used `EvenDistribution` internally by default, but with that gone the default is now `Flex::Start`. This changes the behavior of `Table` (for the better in most cases). The only way for users to get exactly the same as the old behavior is to change their constraints. I imagine most users will be happier out of the box with the new Table default. Resolves https://github.com/ratatui-org/ratatui/issues/843 Thanks to @joshka for the direction
2024-01-29 14:37:50 +00:00
"│Head1 Head Head3│",
"│ │",
feat(layout)!: Change `Flex::default()` (#881) This PR makes a number of simplifications to the layout and constraint features that were added after v0.25.0. For users upgrading from v0.25.0, the net effect of this PR (along with the other PRs) is the following: - New `Flex` modes have been added. - `Flex::Start` (new default) - `Flex::Center` - `Flex::End` - `Flex::SpaceAround` - `Flex::SpaceBetween` - `Flex::Legacy` (old default) - `Min(v)` grows to allocate excess space in all `Flex` modes instead of shrinking (except in `Flex::Legacy` where it retains old behavior). - `Fill(1)` grows to allocate excess space, growing equally with `Min(v)`. --- The following contains a summary of the changes in this PR and the motivation behind them. **`Flex`** - Removes `Flex::Stretch` - Renames `Flex::StretchLast` to `Flex::Legacy` **`Constraint`** - Removes `Fixed` - Makes `Min(v)` grow as much as possible everywhere (except `Flex::Legacy` where it retains the old behavior) - Makes `Min(v)` grow equally as `Fill(1)` while respecting `Min` lower bounds. When `Fill` and `Min` are used together, they both fill excess space equally. Allowing `Min(v)` to grow still allows users to build the same layouts as before with `Flex::Start` with no breaking changes to the behavior. This PR also removes the unstable feature `SegmentSize`. This is a breaking change to the behavior of constraints. If users want old behavior, they can use `Flex::Legacy`. ```rust Layout::vertical([Length(25), Length(25)]).flex(Flex::Legacy) ``` Users that have constraint that exceed the available space will probably not see any difference or see an improvement in their layouts. Any layout with `Min` will be identical in `Flex::Start` and `Flex::Legacy` so any layout with `Min` will not be breaking. Previously, `Table` used `EvenDistribution` internally by default, but with that gone the default is now `Flex::Start`. This changes the behavior of `Table` (for the better in most cases). The only way for users to get exactly the same as the old behavior is to change their constraints. I imagine most users will be happier out of the box with the new Table default. Resolves https://github.com/ratatui-org/ratatui/issues/843 Thanks to @joshka for the direction
2024-01-29 14:37:50 +00:00
"│Row11 Row1 Row13│",
"│Row21 Row2 Row23│",
"│Row31 Row3 Row33│",
"│Row41 Row4 Row43│",
"│ │",
"│ │",
"└────────────────────────────┘",
]),
);
}
#[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"]),
],
widths,
)
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
.block(Block::default().borders(Borders::ALL));
feat: add stateful widgets Most widgets can be drawn directly based on the input parameters. However, some features may require some kind of associated state to be implemented. For example, the `List` widget can highlight the item currently selected. This can be translated in an offset, which is the number of elements to skip in order to have the selected item within the viewport currently allocated to this widget. The widget can therefore only provide the following behavior: whenever the selected item is out of the viewport scroll to a predefined position (make the selected item the last viewable item or the one in the middle). Nonetheless, if the widget has access to the last computed offset then it can implement a natural scrolling experience where the last offset is reused until the selected item is out of the viewport. To allow such behavior within the widgets, this commit introduces the following changes: - Add a `StatefulWidget` trait with an associated `State` type. Widgets that can take advantage of having a "memory" between two draw calls needs to implement this trait. - Add a `render_stateful_widget` method on `Frame` where the associated state is given as a parameter. The chosen approach is thus to let the developers manage their widgets' states themselves as they are already responsible for the lifecycle of the wigets (given that the crate exposes an immediate mode api). The following changes were also introduced: - `Widget::render` has been deleted. Developers should use `Frame::render_widget` instead. - `Widget::background` has been deleted. Developers should use `Buffer::set_background` instead. - `SelectableList` has been deleted. Developers can directly use `List` where `SelectableList` features have been back-ported.
2019-12-15 20:38:18 +00:00
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() {
#[track_caller]
fn test_case(widths: &[Constraint], expected: Buffer) {
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"]),
],
widths,
)
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
.block(Block::default().borders(Borders::ALL))
feat: add stateful widgets Most widgets can be drawn directly based on the input parameters. However, some features may require some kind of associated state to be implemented. For example, the `List` widget can highlight the item currently selected. This can be translated in an offset, which is the number of elements to skip in order to have the selected item within the viewport currently allocated to this widget. The widget can therefore only provide the following behavior: whenever the selected item is out of the viewport scroll to a predefined position (make the selected item the last viewable item or the one in the middle). Nonetheless, if the widget has access to the last computed offset then it can implement a natural scrolling experience where the last offset is reused until the selected item is out of the viewport. To allow such behavior within the widgets, this commit introduces the following changes: - Add a `StatefulWidget` trait with an associated `State` type. Widgets that can take advantage of having a "memory" between two draw calls needs to implement this trait. - Add a `render_stateful_widget` method on `Frame` where the associated state is given as a parameter. The chosen approach is thus to let the developers manage their widgets' states themselves as they are already responsible for the lifecycle of the wigets (given that the crate exposes an immediate mode api). The following changes were also introduced: - `Widget::render` has been deleted. Developers should use `Frame::render_widget` instead. - `Widget::background` has been deleted. Developers should use `Buffer::set_background` instead. - `SelectableList` has been deleted. Developers can directly use `List` where `SelectableList` features have been back-ported.
2019-12-15 20:38:18 +00:00
.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() {
#[track_caller]
fn test_case(widths: &[Constraint], expected: Buffer) {
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"]),
],
widths,
)
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
.block(Block::default().borders(Borders::ALL));
feat: add stateful widgets Most widgets can be drawn directly based on the input parameters. However, some features may require some kind of associated state to be implemented. For example, the `List` widget can highlight the item currently selected. This can be translated in an offset, which is the number of elements to skip in order to have the selected item within the viewport currently allocated to this widget. The widget can therefore only provide the following behavior: whenever the selected item is out of the viewport scroll to a predefined position (make the selected item the last viewable item or the one in the middle). Nonetheless, if the widget has access to the last computed offset then it can implement a natural scrolling experience where the last offset is reused until the selected item is out of the viewport. To allow such behavior within the widgets, this commit introduces the following changes: - Add a `StatefulWidget` trait with an associated `State` type. Widgets that can take advantage of having a "memory" between two draw calls needs to implement this trait. - Add a `render_stateful_widget` method on `Frame` where the associated state is given as a parameter. The chosen approach is thus to let the developers manage their widgets' states themselves as they are already responsible for the lifecycle of the wigets (given that the crate exposes an immediate mode api). The following changes were also introduced: - `Widget::render` has been deleted. Developers should use `Frame::render_widget` instead. - `Widget::background` has been deleted. Developers should use `Buffer::set_background` instead. - `SelectableList` has been deleted. Developers can directly use `List` where `SelectableList` features have been back-ported.
2019-12-15 20:38:18 +00:00
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![
"┌────────────────────────────┐",
feat(layout)!: Change `Flex::default()` (#881) This PR makes a number of simplifications to the layout and constraint features that were added after v0.25.0. For users upgrading from v0.25.0, the net effect of this PR (along with the other PRs) is the following: - New `Flex` modes have been added. - `Flex::Start` (new default) - `Flex::Center` - `Flex::End` - `Flex::SpaceAround` - `Flex::SpaceBetween` - `Flex::Legacy` (old default) - `Min(v)` grows to allocate excess space in all `Flex` modes instead of shrinking (except in `Flex::Legacy` where it retains old behavior). - `Fill(1)` grows to allocate excess space, growing equally with `Min(v)`. --- The following contains a summary of the changes in this PR and the motivation behind them. **`Flex`** - Removes `Flex::Stretch` - Renames `Flex::StretchLast` to `Flex::Legacy` **`Constraint`** - Removes `Fixed` - Makes `Min(v)` grow as much as possible everywhere (except `Flex::Legacy` where it retains the old behavior) - Makes `Min(v)` grow equally as `Fill(1)` while respecting `Min` lower bounds. When `Fill` and `Min` are used together, they both fill excess space equally. Allowing `Min(v)` to grow still allows users to build the same layouts as before with `Flex::Start` with no breaking changes to the behavior. This PR also removes the unstable feature `SegmentSize`. This is a breaking change to the behavior of constraints. If users want old behavior, they can use `Flex::Legacy`. ```rust Layout::vertical([Length(25), Length(25)]).flex(Flex::Legacy) ``` Users that have constraint that exceed the available space will probably not see any difference or see an improvement in their layouts. Any layout with `Min` will be identical in `Flex::Start` and `Flex::Legacy` so any layout with `Min` will not be breaking. Previously, `Table` used `EvenDistribution` internally by default, but with that gone the default is now `Flex::Start`. This changes the behavior of `Table` (for the better in most cases). The only way for users to get exactly the same as the old behavior is to change their constraints. I imagine most users will be happier out of the box with the new Table default. Resolves https://github.com/ratatui-org/ratatui/issues/843 Thanks to @joshka for the direction
2024-01-29 14:37:50 +00:00
"│Head1 Head2 Head3 │",
"│ │",
feat(layout)!: Change `Flex::default()` (#881) This PR makes a number of simplifications to the layout and constraint features that were added after v0.25.0. For users upgrading from v0.25.0, the net effect of this PR (along with the other PRs) is the following: - New `Flex` modes have been added. - `Flex::Start` (new default) - `Flex::Center` - `Flex::End` - `Flex::SpaceAround` - `Flex::SpaceBetween` - `Flex::Legacy` (old default) - `Min(v)` grows to allocate excess space in all `Flex` modes instead of shrinking (except in `Flex::Legacy` where it retains old behavior). - `Fill(1)` grows to allocate excess space, growing equally with `Min(v)`. --- The following contains a summary of the changes in this PR and the motivation behind them. **`Flex`** - Removes `Flex::Stretch` - Renames `Flex::StretchLast` to `Flex::Legacy` **`Constraint`** - Removes `Fixed` - Makes `Min(v)` grow as much as possible everywhere (except `Flex::Legacy` where it retains the old behavior) - Makes `Min(v)` grow equally as `Fill(1)` while respecting `Min` lower bounds. When `Fill` and `Min` are used together, they both fill excess space equally. Allowing `Min(v)` to grow still allows users to build the same layouts as before with `Flex::Start` with no breaking changes to the behavior. This PR also removes the unstable feature `SegmentSize`. This is a breaking change to the behavior of constraints. If users want old behavior, they can use `Flex::Legacy`. ```rust Layout::vertical([Length(25), Length(25)]).flex(Flex::Legacy) ``` Users that have constraint that exceed the available space will probably not see any difference or see an improvement in their layouts. Any layout with `Min` will be identical in `Flex::Start` and `Flex::Legacy` so any layout with `Min` will not be breaking. Previously, `Table` used `EvenDistribution` internally by default, but with that gone the default is now `Flex::Start`. This changes the behavior of `Table` (for the better in most cases). The only way for users to get exactly the same as the old behavior is to change their constraints. I imagine most users will be happier out of the box with the new Table default. Resolves https://github.com/ratatui-org/ratatui/issues/843 Thanks to @joshka for the direction
2024-01-29 14:37:50 +00:00
"│Row11 Row12 Row13 │",
"│Row21 Row22 Row23 │",
"│Row31 Row32 Row33 │",
"│Row41 Row42 Row43 │",
"│ │",
"│ │",
"└────────────────────────────┘",
]),
);
}
#[test]
fn widgets_table_columns_widths_can_use_ratio_constraints() {
#[track_caller]
fn test_case(widths: &[Constraint], expected: Buffer) {
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"]),
],
widths,
)
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
.block(Block::default().borders(Borders::ALL))
.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),
],
[
Constraint::Length(5),
Constraint::Length(5),
Constraint::Length(5),
],
)
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
.block(Block::default().borders(Borders::ALL))
.highlight_symbol(">> ")
.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_enable_always_highlight_spacing() {
let test_case = |state: &mut TableState, space: HighlightSpacing, 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),
],
[
Constraint::Length(5),
Constraint::Length(5),
Constraint::Length(5),
],
)
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
.block(Block::default().borders(Borders::ALL))
.highlight_symbol(">> ")
.highlight_spacing(space)
.column_spacing(1);
f.render_stateful_widget(table, size, state);
})
.unwrap();
terminal.backend().assert_buffer(&expected);
};
assert_eq!(HighlightSpacing::default(), HighlightSpacing::WhenSelected);
let mut state = TableState::default();
// no selection, "WhenSelected" should only allocate if selected
test_case(
&mut state,
HighlightSpacing::default(),
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Head1 Head2 Head3 │",
"│ │",
"│Row11 Row12 Row13 │",
"│Row21 Row22 Row23 │",
"│ │",
"│Row31 Row32 Row33 │",
"└────────────────────────────┘",
]),
);
// no selection, "Always" should allocate regardless if selected or not
test_case(
&mut state,
HighlightSpacing::Always,
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│ Head1 Head2 Head3 │",
"│ │",
"│ Row11 Row12 Row13 │",
"│ Row21 Row22 Row23 │",
"│ │",
"│ Row31 Row32 Row33 │",
"└────────────────────────────┘",
]),
);
// no selection, "Never" should never allocate regadless if selected or not
test_case(
&mut state,
HighlightSpacing::Never,
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Head1 Head2 Head3 │",
"│ │",
"│Row11 Row12 Row13 │",
"│Row21 Row22 Row23 │",
"│ │",
"│Row31 Row32 Row33 │",
"└────────────────────────────┘",
]),
);
// select first, "WhenSelected" should only allocate if selected
state.select(Some(0));
test_case(
&mut state,
HighlightSpacing::default(),
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│ Head1 Head2 Head3 │",
"│ │",
"│>> Row11 Row12 Row13 │",
"│ Row21 Row22 Row23 │",
"│ │",
"│ Row31 Row32 Row33 │",
"└────────────────────────────┘",
]),
);
// select first, "Always" should allocate regardless if selected or not
state.select(Some(0));
test_case(
&mut state,
HighlightSpacing::Always,
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│ Head1 Head2 Head3 │",
"│ │",
"│>> Row11 Row12 Row13 │",
"│ Row21 Row22 Row23 │",
"│ │",
"│ Row31 Row32 Row33 │",
"└────────────────────────────┘",
]),
);
// select first, "Never" should never allocate regadless if selected or not
state.select(Some(0));
test_case(
&mut state,
HighlightSpacing::Never,
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Head1 Head2 Head3 │",
"│ │",
"│Row11 Row12 Row13 │",
"│Row21 Row22 Row23 │",
"│ │",
"│Row31 Row32 Row33 │",
"└────────────────────────────┘",
]),
);
}
#[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(Line::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)),
],
[
Constraint::Length(6),
Constraint::Length(6),
Constraint::Length(6),
],
)
.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))
.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::<Row>::new(),
[
Constraint::Length(6),
Constraint::Length(6),
Constraint::Length(6),
],
)
.header(Row::new(vec!["Head1", "Head2", "Head3"]))
.block(Block::default().borders(Borders::LEFT | Borders::RIGHT))
.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"])],
[
Constraint::Percentage(15),
Constraint::Percentage(15),
Constraint::Percentage(25),
Constraint::Percentage(45),
],
)
.header(Row::new(vec!["h1", "h2", "h3", "h4"]))
.block(Block::default().borders(Borders::ALL))
.highlight_symbol(">> ")
.column_spacing(1);
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"]),
],
[
Constraint::Length(5),
Constraint::Length(5),
Constraint::Length(5),
],
)
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
.block(Block::default().borders(Borders::ALL))
.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"])],
[
Constraint::Length(5),
Constraint::Length(5),
Constraint::Length(5),
],
)
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
.block(Block::default().borders(Borders::ALL))
.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);
}