From c69ca47922619332f76488f5d9e70541b496fe1c Mon Sep 17 00:00:00 2001 From: Eric Lunderberg Date: Wed, 10 Jan 2024 19:32:58 -0600 Subject: [PATCH] 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`, it now accepts `IntoIterator>`. 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. --- BREAKING-CHANGES.md | 24 +++++++++-- examples/demo2/tabs/recipe.rs | 2 +- src/widgets/table/table.rs | 76 +++++++++++++++++++++++++++++------ tests/widgets_table.rs | 2 +- 4 files changed, 86 insertions(+), 18 deletions(-) mode change 100644 => 100755 tests/widgets_table.rs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index fcb0cd62..d796f540 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -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>>` + - `Table::new` now accepts `IntoIterator>>`. - [v0.25.0](#v0250) - Removed `Axis::title_style` and `Buffer::set_background` - `List::new()` now accepts `IntoIterator>>` @@ -47,20 +48,37 @@ This is a quick summary of the sections below: ## v0.26.0 (unreleased) +### `Table::new()` now accepts `IntoIterator>>` ([#774]) + +[#774]: https://github.com/ratatui-org/ratatui/pull/774 + +Previously, `Table::new()` accepted `IntoIterator>`. The argument change to +`IntoIterator>>`, 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::::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>>` ([#776]) [#776]: https://github.com/ratatui-org/ratatui/pull/776 Previously, `Tabs::new()` accepted `Vec` where `T: Into>`. 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]) diff --git a/examples/demo2/tabs/recipe.rs b/examples/demo2/tabs/recipe.rs index 8385690b..1f252600 100644 --- a/examples/demo2/tabs/recipe.rs +++ b/examples/demo2/tabs/recipe.rs @@ -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)]) diff --git a/src/widgets/table/table.rs b/src/widgets/table/table.rs index 841caccc..1098c5e5 100644 --- a/src/widgets/table/table.rs +++ b/src/widgets/table/table.rs @@ -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::() +/// .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(rows: R, widths: C) -> Self where - R: IntoIterator>, + R: IntoIterator, + R::Item: Into>, C: IntoIterator, C::Item: Into, { 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::::new(), widths) /// .segment_size(SegmentSize::LastTakesRemainder); /// ``` #[stability::unstable( @@ -807,6 +825,20 @@ impl<'a> Styled for Table<'a> { } } +impl<'a, Item> FromIterator for Table<'a> +where + Item: Into>, +{ + /// 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>(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::
() + .widths([Constraint::Percentage(25); 4]); + + let expected_rows: Vec = 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::::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::::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::::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::::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::::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::::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::::new(), constraints).segment_size(segment_size); let widths = table.get_columns_widths(available_width, selection_width); assert_eq!(widths, expected); diff --git a/tests/widgets_table.rs b/tests/widgets_table.rs old mode 100644 new mode 100755 index cbe97fa8..fb2d2eba --- a/tests/widgets_table.rs +++ b/tests/widgets_table.rs @@ -849,7 +849,7 @@ fn widgets_table_should_render_even_if_empty() { .draw(|f| { let size = f.size(); let table = Table::new( - vec![], + Vec::::new(), [ Constraint::Length(6), Constraint::Length(6),