feat(table)!: Collect iterator of Row into Table (#774)

Any iterator whose item is convertible into `Row` can now be
collected into a `Table`.

Where previously, `Table::new` accepted `IntoIterator<Item = Row>`, it
now accepts `IntoIterator<Item: Into<Row>>`.

BREAKING CHANGE:
The compiler can no longer infer the element type of the container
passed to `Table::new()`.  For example, `Table::new(vec![], widths)`
will no longer compile, as the type of `vec![]` can no longer be
inferred.
This commit is contained in:
Eric Lunderberg 2024-01-10 19:32:58 -06:00 committed by GitHub
parent f29c73fb1c
commit c69ca47922
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 86 additions and 18 deletions

View File

@ -16,6 +16,7 @@ This is a quick summary of the sections below:
- `Line` now has an extra `style` field which applies the style to the entire line
- `Block` style methods cannot be created in a const context
- `Tabs::new()` now accepts `IntoIterator<Item: Into<Line<'a>>>`
- `Table::new` now accepts `IntoIterator<Item: Into<Row<'a>>>`.
- [v0.25.0](#v0250)
- Removed `Axis::title_style` and `Buffer::set_background`
- `List::new()` now accepts `IntoIterator<Item = Into<ListItem<'a>>>`
@ -47,20 +48,37 @@ This is a quick summary of the sections below:
## v0.26.0 (unreleased)
### `Table::new()` now accepts `IntoIterator<Item: Into<Row<'a>>>` ([#774])
[#774]: https://github.com/ratatui-org/ratatui/pull/774
Previously, `Table::new()` accepted `IntoIterator<Item=Row<'a>>`. The argument change to
`IntoIterator<Item: Into<Row<'a>>>`, This allows more flexible types from calling scopes, though it
can some break type inference in the calling scope for empty containers.
This can be resolved either by providing an explicit type (e.g. `Vec::<Row>::new()`), or by using
`Table::default()`.
```diff
- let table = Table::new(vec![], widths);
// becomes
+ let table = Table::default().widths(widths);
```
### `Tabs::new()` now accepts `IntoIterator<Item: Into<Line<'a>>>` ([#776])
[#776]: https://github.com/ratatui-org/ratatui/pull/776
Previously, `Tabs::new()` accepted `Vec<T>` where `T: Into<Line<'a>>`. This allows more flexible
types from calling scopes, though it can break type inference when the calling scope.
types from calling scopes, though it can break some type inference in the calling scope.
This typically occurs when collecting an iterator prior to calling `Tabs::new`, and can be resolved
by removing the call to `.collect()`.
```diff
- let table = Tabs::new((0.3).map(|i| format!("{i}")).collect());
- let tabs = Tabs::new((0.3).map(|i| format!("{i}")).collect());
// becomes
+ let table = Tabs::new((0.3).map(|i| format!("{i}")));
+ let tabs = Tabs::new((0.3).map(|i| format!("{i}")));
```
### Table::default() now sets segment_size to None and column_spacing to ([#751])

View File

@ -146,7 +146,7 @@ fn render_recipe(area: Rect, buf: &mut Buffer) {
fn render_ingredients(selected_row: usize, area: Rect, buf: &mut Buffer) {
let mut state = TableState::default().with_selected(Some(selected_row));
let rows = INGREDIENTS.iter().map(|&i| i.into()).collect_vec();
let rows = INGREDIENTS.iter().cloned();
let theme = THEME.recipe;
StatefulWidget::render(
Table::new(rows, [Constraint::Length(7), Constraint::Length(30)])

View File

@ -132,6 +132,22 @@ use crate::{
/// Cell::from(Text::from("text"));
/// ```
///
/// Just as rows can be collected from iterators of `Cell`s, tables can be collected from iterators
/// of `Row`s. This will create a table with column widths evenly dividing the space available.
/// These default columns widths can be overridden using the `Table::widths` method.
///
/// ```rust
/// use ratatui::{prelude::*, widgets::*};
///
/// let text = "Mary had a\nlittle lamb.";
///
/// let table = text
/// .split("\n")
/// .map(|line: &str| -> Row { line.split_ascii_whitespace().collect() })
/// .collect::<Table>()
/// .widths([Constraint::Length(10); 3]);
/// ```
///
/// `Table` also implements the [`Styled`] trait, which means you can use style shorthands from
/// the [`Stylize`] trait to set the style of the widget more concisely.
///
@ -250,14 +266,17 @@ impl<'a> Table<'a> {
/// ```
pub fn new<R, C>(rows: R, widths: C) -> Self
where
R: IntoIterator<Item = Row<'a>>,
R: IntoIterator,
R::Item: Into<Row<'a>>,
C: IntoIterator,
C::Item: Into<Constraint>,
{
let widths = widths.into_iter().map(Into::into).collect_vec();
ensure_percentages_less_than_100(&widths);
let rows = rows.into_iter().map(Into::into).collect();
Self {
rows: rows.into_iter().collect(),
rows,
widths,
..Default::default()
}
@ -534,11 +553,10 @@ impl<'a> Table<'a> {
/// to the last column.
#[cfg_attr(feature = "unstable", doc = " ```")]
#[cfg_attr(not(feature = "unstable"), doc = " ```ignore")]
/// # use ratatui::layout::Constraint;
/// # use ratatui::layout::SegmentSize;
/// # use ratatui::widgets::Table;
/// # use ratatui::layout::{Constraint, SegmentSize};
/// # use ratatui::widgets::{Table, Row};
/// let widths = [Constraint::Min(10), Constraint::Min(10), Constraint::Min(10)];
/// let table = Table::new([], widths)
/// let table = Table::new(Vec::<Row>::new(), widths)
/// .segment_size(SegmentSize::LastTakesRemainder);
/// ```
#[stability::unstable(
@ -807,6 +825,20 @@ impl<'a> Styled for Table<'a> {
}
}
impl<'a, Item> FromIterator<Item> for Table<'a>
where
Item: Into<Row<'a>>,
{
/// Collects an iterator of rows into a table.
///
/// When collecting from an iterator into a table, the user must provide the widths using
/// `Table::widths` after construction.
fn from_iter<Iter: IntoIterator<Item = Item>>(rows: Iter) -> Self {
let widths: [Constraint; 0] = [];
Table::new(rows, widths)
}
}
#[cfg(test)]
mod tests {
use std::vec;
@ -853,6 +885,24 @@ mod tests {
assert_eq!(table.segment_size, SegmentSize::None);
}
#[test]
fn collect() {
let table = (0..4)
.map(|i| -> Row { (0..4).map(|j| format!("{i}*{j} = {}", i * j)).collect() })
.collect::<Table>()
.widths([Constraint::Percentage(25); 4]);
let expected_rows: Vec<Row> = vec![
Row::new(["0*0 = 0", "0*1 = 0", "0*2 = 0", "0*3 = 0"]),
Row::new(["1*0 = 0", "1*1 = 1", "1*2 = 2", "1*3 = 3"]),
Row::new(["2*0 = 0", "2*1 = 2", "2*2 = 4", "2*3 = 6"]),
Row::new(["3*0 = 0", "3*1 = 3", "3*2 = 6", "3*3 = 9"]),
];
assert_eq!(table.rows, expected_rows);
assert_eq!(table.widths, [Constraint::Percentage(25); 4]);
}
#[test]
fn widths() {
let table = Table::default().widths([Constraint::Length(100)]);
@ -934,24 +984,24 @@ mod tests {
#[test]
fn widths_conversions() {
let array = [Constraint::Percentage(100)];
let table = Table::new(vec![], array);
let table = Table::new(Vec::<Row>::new(), array);
assert_eq!(table.widths, vec![Constraint::Percentage(100)], "array");
let array_ref = &[Constraint::Percentage(100)];
let table = Table::new(vec![], array_ref);
let table = Table::new(Vec::<Row>::new(), array_ref);
assert_eq!(table.widths, vec![Constraint::Percentage(100)], "array ref");
let vec = vec![Constraint::Percentage(100)];
let slice = vec.as_slice();
let table = Table::new(vec![], slice);
let table = Table::new(Vec::<Row>::new(), slice);
assert_eq!(table.widths, vec![Constraint::Percentage(100)], "slice");
let vec = vec![Constraint::Percentage(100)];
let table = Table::new(vec![], vec);
let table = Table::new(Vec::<Row>::new(), vec);
assert_eq!(table.widths, vec![Constraint::Percentage(100)], "vec");
let vec_ref = &vec![Constraint::Percentage(100)];
let table = Table::new(vec![], vec_ref);
let table = Table::new(Vec::<Row>::new(), vec_ref);
assert_eq!(table.widths, vec![Constraint::Percentage(100)], "vec ref");
}
@ -1120,7 +1170,7 @@ mod tests {
#[test]
fn render_with_overflow_does_not_panic() {
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 3));
let table = Table::new(vec![], [Constraint::Min(20); 1])
let table = Table::new(Vec::<Row>::new(), [Constraint::Min(20); 1])
.header(Row::new([Line::from("").alignment(Alignment::Right)]))
.footer(Row::new([Line::from("").alignment(Alignment::Right)]));
Widget::render(table, Rect::new(0, 0, 20, 3), &mut buf);
@ -1161,7 +1211,7 @@ mod tests {
selection_width: u16,
expected: &[(u16, u16)],
) {
let table = Table::new(vec![], constraints).segment_size(segment_size);
let table = Table::new(Vec::<Row>::new(), constraints).segment_size(segment_size);
let widths = table.get_columns_widths(available_width, selection_width);
assert_eq!(widths, expected);

2
tests/widgets_table.rs Normal file → Executable file
View File

@ -849,7 +849,7 @@ fn widgets_table_should_render_even_if_empty() {
.draw(|f| {
let size = f.size();
let table = Table::new(
vec![],
Vec::<Row>::new(),
[
Constraint::Length(6),
Constraint::Length(6),