From 36d8c5364590a559913c40ee5f021b5d8e3466e6 Mon Sep 17 00:00:00 2001 From: Josh McKinney Date: Wed, 15 Nov 2023 12:34:02 -0800 Subject: [PATCH] fix(table): widths() now accepts AsRef<[Constraint]> (#628) This allows passing an array, slice or Vec of constraints, which is more ergonomic than requiring this to always be a slice. The following calls now all succeed: ```rust Table::new(rows).widths([Constraint::Length(5), Constraint::Length(5)]); Table::new(rows).widths(&[Constraint::Length(5), Constraint::Length(5)]); // widths could also be computed at runtime let widths = vec![Constraint::Length(5), Constraint::Length(5)]; Table::new(rows).widths(widths.clone()); Table::new(rows).widths(&widths); ``` --- BREAKING-CHANGES.md | 21 +++++++++ examples/demo/ui.rs | 4 +- examples/demo2/tabs/recipe.rs | 2 +- examples/demo2/tabs/traceroute.rs | 2 +- examples/table.rs | 2 +- src/widgets/table.rs | 76 +++++++++++++++++++++---------- tests/widgets_table.rs | 21 +++++---- 7 files changed, 89 insertions(+), 39 deletions(-) diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index eb5e916a..0bf52830 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -10,6 +10,9 @@ github with a [breaking change] label. This is a quick summary of the sections below: +- Unreleased (0.24.1) + - + - [v0.24.0](#v0240) - MSRV is now 1.70.0 - `ScrollbarState`: `position`, `content_length`, and `viewport_content_length` are now `usize` @@ -32,6 +35,24 @@ This is a quick summary of the sections below: - MSRV is now 1.63.0 - `List` no longer ignores empty strings +## Unreleased (v0.24.1) + +### `Table::widths()` now accepts `AsRef<[Constraint]>` ([#628]) + +Previously `Table::widths()` took a slice (`&'a [Constraint]`). This change will introduce clippy +`needless_borrow` warnings for places where slices are passed to this method. To fix these, remove +the `&`. + +E.g. + +```rust +let table = Table::new(rows).widths(&[Constraint::Length(1)]); +// becomes +let table = Table::new(rows).widths([Constraint::Length(1)]); +``` + +[#628]: https://github.com/ratatui-org/ratatui/pull/628 + ## [v0.24.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.24.0) ### ScrollbarState field type changed from `u16` to `usize` ([#456]) diff --git a/examples/demo/ui.rs b/examples/demo/ui.rs index 41b15e10..f40f712d 100644 --- a/examples/demo/ui.rs +++ b/examples/demo/ui.rs @@ -296,7 +296,7 @@ fn draw_second_tab(f: &mut Frame, app: &mut App, area: Rect) { .bottom_margin(1), ) .block(Block::default().title("Servers").borders(Borders::ALL)) - .widths(&[ + .widths([ Constraint::Length(15), Constraint::Length(15), Constraint::Length(10), @@ -395,7 +395,7 @@ fn draw_third_tab(f: &mut Frame, _app: &mut App, area: Rect) { .collect(); let table = Table::new(items) .block(Block::default().title("Colors").borders(Borders::ALL)) - .widths(&[ + .widths([ Constraint::Ratio(1, 3), Constraint::Ratio(1, 3), Constraint::Ratio(1, 3), diff --git a/examples/demo2/tabs/recipe.rs b/examples/demo2/tabs/recipe.rs index e5d17ef5..67aaef54 100644 --- a/examples/demo2/tabs/recipe.rs +++ b/examples/demo2/tabs/recipe.rs @@ -149,7 +149,7 @@ fn render_ingredients(selected_row: usize, area: Rect, buf: &mut Buffer) { Table::new(rows) .block(Block::new().style(theme.ingredients)) .header(Row::new(vec!["Qty", "Ingredient"]).style(theme.ingredients_header)) - .widths(&[Constraint::Length(7), Constraint::Length(30)]) + .widths([Constraint::Length(7), Constraint::Length(30)]) .highlight_style(Style::new().light_yellow()), area, buf, diff --git a/examples/demo2/tabs/traceroute.rs b/examples/demo2/tabs/traceroute.rs index ae9d7006..40a79eb7 100644 --- a/examples/demo2/tabs/traceroute.rs +++ b/examples/demo2/tabs/traceroute.rs @@ -52,7 +52,7 @@ fn render_hops(selected_row: usize, area: Rect, buf: &mut Buffer) { StatefulWidget::render( Table::new(rows) .header(Row::new(vec!["Host", "Address"]).set_style(THEME.traceroute.header)) - .widths(&[Constraint::Max(100), Constraint::Length(15)]) + .widths([Constraint::Max(100), Constraint::Length(15)]) .highlight_style(THEME.traceroute.selected) .block(block), area, diff --git a/examples/table.rs b/examples/table.rs index 0b503647..000086eb 100644 --- a/examples/table.rs +++ b/examples/table.rs @@ -142,7 +142,7 @@ fn ui(f: &mut Frame, app: &mut App) { .block(Block::default().borders(Borders::ALL).title("Table")) .highlight_style(selected_style) .highlight_symbol(">> ") - .widths(&[ + .widths([ Constraint::Percentage(50), Constraint::Max(30), Constraint::Min(10), diff --git a/src/widgets/table.rs b/src/widgets/table.rs index 906411bd..db541356 100644 --- a/src/widgets/table.rs +++ b/src/widgets/table.rs @@ -1,3 +1,6 @@ +use std::iter; + +use itertools::Itertools; use strum::{Display, EnumString}; use unicode_width::UnicodeWidthStr; @@ -251,7 +254,7 @@ pub struct Table<'a> { /// Base style for the widget style: Style, /// Width constraints for each column - widths: &'a [Constraint], + widths: Vec, /// Space between each column column_spacing: u16, /// Style used to render the selected row @@ -294,7 +297,7 @@ impl<'a> Table<'a> { Self { block: None, style: Style::default(), - widths: &[], + widths: vec![], column_spacing: 1, highlight_style: Style::default(), highlight_symbol: None, @@ -355,7 +358,24 @@ impl<'a> Table<'a> { self } - pub fn widths(mut self, widths: &'a [Constraint]) -> Self { + /// Set the widths of the columns of the [`Table`] widget. + /// + /// The `widths` parameter accepts `AsRef<[Constraint]>`` which can be an array, slice, or Vec. + /// + /// # Examples + /// + /// ```rust + /// # use ratatui::{prelude::*, widgets::*}; + /// let table = Table::new(vec![]).widths([Constraint::Length(5), Constraint::Length(5)]); + /// let table = Table::new(vec![]).widths(&[Constraint::Length(5), Constraint::Length(5)]); + /// + /// // widths could also be computed at runtime + /// let widths = vec![Constraint::Length(5), Constraint::Length(5)]; + /// let table = Table::new(vec![]).widths(widths.clone()); + /// let table = Table::new(vec![]).widths(&widths); + /// ``` + pub fn widths>(mut self, widths: T) -> Self { + let widths = widths.as_ref().to_vec(); let between_0_and_100 = |&w| match w { Constraint::Percentage(p) => p <= 100, _ => true, @@ -399,29 +419,21 @@ impl<'a> Table<'a> { /// Get all offsets and widths of all user specified columns /// Returns (x, width) fn get_columns_widths(&self, max_width: u16, selection_width: u16) -> Vec<(u16, u16)> { - let mut constraints = Vec::with_capacity(self.widths.len() * 2 + 1); - constraints.push(Constraint::Length(selection_width)); - for constraint in self.widths { - constraints.push(*constraint); - constraints.push(Constraint::Length(self.column_spacing)); - } - if !self.widths.is_empty() { - constraints.pop(); - } - let chunks = Layout::default() + let constraints = iter::once(Constraint::Length(selection_width)) + .chain(Itertools::intersperse( + self.widths.iter().cloned(), + Constraint::Length(self.column_spacing), + )) + .collect_vec(); + let layout = Layout::default() .direction(Direction::Horizontal) .constraints(constraints) .segment_size(SegmentSize::None) - .split(Rect { - x: 0, - y: 0, - width: max_width, - height: 1, - }); - chunks + .split(Rect::new(0, 0, max_width, 1)); + layout .iter() - .skip(1) - .step_by(2) + .skip(1) // skip selection column + .step_by(2) // skip spacing between columns .map(|c| (c.x, c.width)) .collect() } @@ -661,7 +673,23 @@ mod tests { #[test] #[should_panic] fn table_invalid_percentages() { - Table::new(vec![]).widths(&[Constraint::Percentage(110)]); + Table::new(vec![]).widths([Constraint::Percentage(110)]); + } + + #[test] + fn widths_conversions() { + let table = Table::new(vec![]).widths([Constraint::Percentage(100)]); + assert_eq!(table.widths, vec![Constraint::Percentage(100)], "array"); + + #[allow(clippy::needless_borrow)] // for backwards compatibility with existing code + let table = Table::new(vec![]).widths(&[Constraint::Percentage(100)]); + assert_eq!(table.widths, vec![Constraint::Percentage(100)], "slice"); + + let table = Table::new(vec![]).widths(vec![Constraint::Percentage(100)]); + assert_eq!(table.widths, vec![Constraint::Percentage(100)], "vec"); + + let table = Table::new(vec![]).widths(&vec![Constraint::Percentage(100)]); + assert_eq!(table.widths, vec![Constraint::Percentage(100)], "vec ref"); } // test how constraints interact with table column width allocation @@ -779,7 +807,7 @@ mod tests { Row::new(vec![Line::from("Center").alignment(Alignment::Center)]), Row::new(vec![Line::from("Right").alignment(Alignment::Right)]), ]) - .widths(&[Percentage(100)]); + .widths([Percentage(100)]); Widget::render(table, Rect::new(0, 0, 20, 3), &mut buf); diff --git a/tests/widgets_table.rs b/tests/widgets_table.rs index 6d34a08f..15ef24ca 100644 --- a/tests/widgets_table.rs +++ b/tests/widgets_table.rs @@ -27,7 +27,7 @@ fn widgets_table_column_spacing_can_be_changed() { ]) .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1)) .block(Block::default().borders(Borders::ALL)) - .widths(&[ + .widths([ Constraint::Length(5), Constraint::Length(5), Constraint::Length(5), @@ -198,7 +198,8 @@ fn widgets_table_columns_widths_can_use_fixed_length_constraints() { #[test] fn widgets_table_columns_widths_can_use_percentage_constraints() { - let test_case = |widths, expected| { + #[track_caller] + fn test_case(widths: &[Constraint], expected: Buffer) { let backend = TestBackend::new(30, 10); let mut terminal = Terminal::new(backend).unwrap(); @@ -219,7 +220,7 @@ fn widgets_table_columns_widths_can_use_percentage_constraints() { }) .unwrap(); terminal.backend().assert_buffer(&expected); - }; + } // columns of zero width show nothing test_case( @@ -536,7 +537,7 @@ fn widgets_table_can_have_rows_with_multi_lines() { .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1)) .block(Block::default().borders(Borders::ALL)) .highlight_symbol(">> ") - .widths(&[ + .widths([ Constraint::Length(5), Constraint::Length(5), Constraint::Length(5), @@ -631,7 +632,7 @@ fn widgets_table_enable_always_highlight_spacing() { .block(Block::default().borders(Borders::ALL)) .highlight_symbol(">> ") .highlight_spacing(space) - .widths(&[ + .widths([ Constraint::Length(5), Constraint::Length(5), Constraint::Length(5), @@ -772,7 +773,7 @@ fn widgets_table_can_have_elements_styled_individually() { .block(Block::default().borders(Borders::LEFT | Borders::RIGHT)) .highlight_symbol(">> ") .highlight_style(Style::default().add_modifier(Modifier::BOLD)) - .widths(&[ + .widths([ Constraint::Length(6), Constraint::Length(6), Constraint::Length(6), @@ -833,7 +834,7 @@ fn widgets_table_should_render_even_if_empty() { let table = Table::new(vec![]) .header(Row::new(vec!["Head1", "Head2", "Head3"])) .block(Block::default().borders(Borders::LEFT | Borders::RIGHT)) - .widths(&[ + .widths([ Constraint::Length(6), Constraint::Length(6), Constraint::Length(6), @@ -873,7 +874,7 @@ fn widgets_table_columns_dont_panic() { .block(Block::default().borders(Borders::ALL)) .highlight_symbol(">> ") .column_spacing(1) - .widths(&[ + .widths([ Constraint::Percentage(15), Constraint::Percentage(15), Constraint::Percentage(25), @@ -908,7 +909,7 @@ fn widgets_table_should_clamp_offset_if_rows_are_removed() { ]) .header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1)) .block(Block::default().borders(Borders::ALL)) - .widths(&[ + .widths([ Constraint::Length(5), Constraint::Length(5), Constraint::Length(5), @@ -937,7 +938,7 @@ fn widgets_table_should_clamp_offset_if_rows_are_removed() { 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(&[ + .widths([ Constraint::Length(5), Constraint::Length(5), Constraint::Length(5),