diff --git a/examples/apps/table/src/main.rs b/examples/apps/table/src/main.rs index ee37b249..fda67b22 100644 --- a/examples/apps/table/src/main.rs +++ b/examples/apps/table/src/main.rs @@ -222,6 +222,19 @@ impl App { let item = data.ref_array(); item.into_iter() .map(|content| Cell::from(Text::from(format!("\n{content}\n")))) + .enumerate() + .map(|(idx, cell)| { + if i == 3 && idx == 1 { + Cell::from(Text::from( + // Gratuitously long error message to demonstrate column_span(2) + "\n[no address or email address is available for this person]\n" + .to_string(), + )) + .column_span(2) + } else { + cell + } + }) .collect::() .style(Style::new().fg(self.colors.row_fg).bg(color)) .height(4) diff --git a/ratatui-widgets/src/table.rs b/ratatui-widgets/src/table.rs index 0bf57fdf..583b9ddd 100644 --- a/ratatui-widgets/src/table.rs +++ b/ratatui-widgets/src/table.rs @@ -776,7 +776,7 @@ impl StatefulWidget for &Table<'_> { self.render_header(header_area, buf, &column_widths); - self.render_rows(rows_area, buf, state, selection_width, &column_widths); + self.render_rows(rows_area, buf, selection_width, state, &column_widths); self.render_footer(footer_area, buf, &column_widths); } @@ -806,31 +806,47 @@ impl Table<'_> { (header_area, rows_area, footer_area) } - fn render_header(&self, area: Rect, buf: &mut Buffer, column_widths: &[(u16, u16)]) { + /// Render the header cells, if they are not `None` + /// + /// The `x` and `width` fields of each `Rect` in `column_widths` denote the starting + /// x-coordinate and width of each column in the table. + fn render_header(&self, area: Rect, buf: &mut Buffer, column_widths: &[Rect]) { if let Some(ref header) = self.header { buf.set_style(area, header.style); - for ((x, width), cell) in column_widths.iter().zip(header.cells.iter()) { - cell.render(Rect::new(area.x + x, area.y, *width, area.height), buf); + for (cell_area, cell) in column_widths.iter().zip(header.cells.iter()) { + let new_x = area.x + cell_area.x; + let area_to_render = Rect::new(new_x, area.y, cell_area.width, area.height); + cell.render(area_to_render, buf); } } } - fn render_footer(&self, area: Rect, buf: &mut Buffer, column_widths: &[(u16, u16)]) { + /// Render the footer cells, if they are not `None` + /// + /// The `x` and `width` fields of each `Rect` in `column_widths` denote the starting + /// x-coordinate and width of each column in the table. + fn render_footer(&self, area: Rect, buf: &mut Buffer, column_widths: &[Rect]) { if let Some(ref footer) = self.footer { buf.set_style(area, footer.style); - for ((x, width), cell) in column_widths.iter().zip(footer.cells.iter()) { - cell.render(Rect::new(area.x + x, area.y, *width, area.height), buf); + for (cell_area, cell) in column_widths.iter().zip(footer.cells.iter()) { + let new_x = area.x + cell_area.x; + let area_to_render = Rect::new(new_x, area.y, cell_area.width, area.height); + cell.render(area_to_render, buf); } } } + /// Render the table rows + /// + /// The `x` and `width` fields of each `Rect` in `column_widths` denote the starting + /// x-coordinate and width of each column in the table. fn render_rows( &self, area: Rect, buf: &mut Buffer, - state: &mut TableState, selection_width: u16, - columns_widths: &[(u16, u16)], + state: &mut TableState, + columns_widths: &[Rect], ) { if self.rows.is_empty() { return; @@ -856,19 +872,9 @@ impl Table<'_> { let is_selected = state.selected.is_some_and(|index| index == i); if selection_width > 0 && is_selected { - let selection_area = Rect { - width: selection_width, - ..row_area - }; - buf.set_style(selection_area, row.style); - (&self.highlight_symbol).render(selection_area, buf); - } - for ((x, width), cell) in columns_widths.iter().zip(row.cells.iter()) { - cell.render( - Rect::new(row_area.x + x, row_area.y, *width, row_area.height), - buf, - ); + self.set_selection_style(buf, selection_width, row_area, row); } + self.render_row_cells(buf, columns_widths.iter().collect(), &row.cells, row_area); if is_selected { selected_row_area = Some(row_area); } @@ -878,9 +884,9 @@ impl Table<'_> { let selected_column_area = state.selected_column.and_then(|s| { // The selection is clamped by the column count. Since a user can manually specify an // incorrect number of widths, we should use panic free methods. - columns_widths.get(s).map(|(x, width)| Rect { - x: x + area.x, - width: *width, + columns_widths.get(s).map(|cell_area| Rect { + x: cell_area.x + area.x, + width: cell_area.width, ..area }) }); @@ -902,6 +908,83 @@ impl Table<'_> { } } + /// Render cells into the columns of a row + /// + /// Render `Cell`s from `cells` into columns specified by `column_widths`, stopping + /// if either of these iterators are finished. Each `Cell` gets rendered across + /// [`Cell::get_column_span`] columns plus the gaps between them, if this value is > 1. + fn render_row_cells( + &self, + buf: &mut Buffer, + column_widths: Vec<&Rect>, + cells: &Vec, + row_area: Rect, + ) { + let mut column_widths_iterator = column_widths.into_iter(); + for current_cell in cells { + if let Some(cell_area) = Self::get_cell_area( + &mut column_widths_iterator, + current_cell.column_span, + self.column_spacing, + ) { + let new_x = row_area.x + cell_area.x; + let area_to_render = Rect::new(new_x, row_area.y, cell_area.width, row_area.height); + current_cell.render(area_to_render, buf); + } + } + } + + /// Set the row style and render the highlight symbol + fn set_selection_style( + &self, + buf: &mut Buffer, + selection_width: u16, + row_area: Rect, + row: &Row, + ) { + let selection_area = Rect { + width: selection_width, + ..row_area + }; + buf.set_style(selection_area, row.style); + (&self.highlight_symbol).render(selection_area, buf); + } + + /// Return the area that a [`Cell`] should occupy, taking into account its + /// [`Cell::column_span`]. + /// + /// Returns `None` when there are no more columns for the [`Cell`] to occupy. + /// + /// Otherwise, returns `Some(Rect{x, y = 0, width, height = 0})`, representing the start + /// x-coordinate and width of the [`Cell`]. + /// + /// This function consumes `cell_column_span` `Rect`s from `column_widths_iterator` (or all the + /// `Rects` if the iterator is less than `cell_column_span` `Rect`s long). This function adds + /// the width of each `Rect` plus `column_spacing` to a running total of the final width. The + /// return value is the original x coordinate and the final width, or `None` if + /// `column_widths_iterator` is empty or `cell_column_span` is `0`. + fn get_cell_area<'a, T>( + column_widths_iterator: &mut T, + cell_column_span: u16, + column_spacing: u16, + ) -> Option + where + T: Iterator, + { + if cell_column_span == 0 { + return None; + } + let first = column_widths_iterator.next()?; + let (n_columns_taken, all_columns_width) = column_widths_iterator + .take((cell_column_span - 1).into()) + .map(|rect| (1, rect.width)) + .fold((1, first.width), |so_far, next_column| { + (next_column.0 + so_far.0, next_column.1 + so_far.1) + }); + let width = all_columns_width + (n_columns_taken - 1) * column_spacing; + Some(Rect::new(first.x, first.y, width, 1)) + } + /// Return the indexes of the visible rows. /// /// The algorithm works as follows: @@ -960,7 +1043,7 @@ impl Table<'_> { max_width: u16, selection_width: u16, col_count: usize, - ) -> Vec<(u16, u16)> { + ) -> Vec { let widths = if self.widths.is_empty() { // Divide the space between each column equally vec![Constraint::Length(max_width / col_count.max(1) as u16); col_count] @@ -975,7 +1058,10 @@ impl Table<'_> { .flex(self.flex) .spacing(self.column_spacing) .split(columns_area); - rects.iter().map(|c| (c.x, c.width)).collect() + rects + .iter() + .map(|c| Rect::new(c.x, 0, c.width, 1)) + .collect() } fn column_count(&self) -> usize { @@ -1352,6 +1438,101 @@ mod tests { assert_eq!(buf, expected); } + #[rstest] + #[case(15, 5, vec![ + Row::new(vec![ + Cell::new("Cell1").column_span(1), + Cell::new("Cell2").column_span(1), + ]), + Row::new(vec![ + Cell::new("Cell3").column_span(1), + Cell::new("Cell4").column_span(1), + ]), + ], + &Buffer::with_lines(["Cell1 Cell2 ", "Cell3 Cell4 "]))] + #[case(15, 5, vec![ + Row::new(vec![ + Cell::new("Cell1").column_span(0), + Cell::new("Cell2").column_span(1), + ]), + Row::new(vec![ + Cell::new("Cell3").column_span(1), + Cell::new("Cell4").column_span(1), + ]), + ], &Buffer::with_lines(["Cell2 ", "Cell3 Cell4 "]))] + #[case(15, 5, vec![ + Row::new(vec![ + Cell::new("Cell1").column_span(2), + Cell::new("Cell2").column_span(1), + ]), + Row::new(vec![ + Cell::new("Cell3").column_span(1), + Cell::new("Cell4").column_span(1), + ]), + ], &Buffer::with_lines(["Cell1 ", "Cell3 Cell4 "]))] + fn test_colspans_2_cols<'rows, Rows>( + #[case] width: u16, + #[case] column_width: u16, + #[case] rows: Rows, + #[case] expected: &Buffer, + ) where + Rows: IntoIterator>, + { + let mut buf = Buffer::empty(Rect::new(0, 0, width, 2)); + let table = Table::new(rows, [Constraint::Length(column_width); 2]); + Widget::render(table, Rect::new(0, 0, width, 2), &mut buf); + assert_eq!(buf, *expected); + } + + #[rstest] + #[case(17, 5, vec![ + Row::new(vec![ + Cell::new("Cell1").column_span(2), + Cell::new("Cell2").column_span(1), + ]), + Row::new(vec![ + Cell::new("Cell3").column_span(1), + Cell::new("Cell4").column_span(1), + Cell::new("Cell5").column_span(1), + ]), + ], &Buffer::with_lines(["Cell1 Cell2", "Cell3 Cell4 Cell5"]))] + #[case(17, 5, vec![ + Row::new(vec![ + Cell::new("Cell1").column_span(1), + Cell::new("Cell2").column_span(2), + Cell::new("Cell3").column_span(1), + ]), + Row::new(vec![ + Cell::new("Cell4").column_span(1), + Cell::new("Cell5").column_span(1), + Cell::new("Cell6").column_span(1), + ]), + ], &Buffer::with_lines(["Cell1 Cell2 ", "Cell4 Cell5 Cell6"]))] + #[case(15, 5, vec![ + Row::new(vec![ + Cell::new("11111111111111111111").column_span(2), + Cell::new("22222222222222222222").column_span(1), + ]), + Row::new(vec![ + Cell::new("33333333333333333333").column_span(1), + Cell::new("44444444444444444444").column_span(2), + Cell::new("55555555555555555555").column_span(1), + ]), + ], &Buffer::with_lines(["1111111111 2222", "3333 4444444444"]))] + fn test_colspans_3_cols<'rows, Rows>( + #[case] width: u16, + #[case] column_width: u16, + #[case] rows: Rows, + #[case] expected: &Buffer, + ) where + Rows: IntoIterator>, + { + let mut buf = Buffer::empty(Rect::new(0, 0, width, 2)); + let table = Table::new(rows, [Constraint::Length(column_width); 3]); + Widget::render(table, Rect::new(0, 0, width, 2), &mut buf); + assert_eq!(buf, *expected); + } + #[test] fn render_with_header() { let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3)); @@ -1676,15 +1857,24 @@ mod tests { fn length_constraint() { // without selection, more than needed width let table = Table::default().widths([Length(4), Length(4)]); - assert_eq!(table.get_column_widths(20, 0, 0), [(0, 4), (5, 4)]); + assert_eq!( + table.get_column_widths(20, 0, 0), + [Rect::new(0, 0, 4, 1), Rect::new(5, 0, 4, 1),] + ); // with selection, more than needed width let table = Table::default().widths([Length(4), Length(4)]); - assert_eq!(table.get_column_widths(20, 3, 0), [(3, 4), (8, 4)]); + assert_eq!( + table.get_column_widths(20, 3, 0), + [Rect::new(3, 0, 4, 1), Rect::new(8, 0, 4, 1)] + ); // without selection, less than needed width let table = Table::default().widths([Length(4), Length(4)]); - assert_eq!(table.get_column_widths(7, 0, 0), [(0, 3), (4, 3)]); + assert_eq!( + table.get_column_widths(7, 0, 0), + [Rect::new(0, 0, 3, 1), Rect::new(4, 0, 3, 1)] + ); // with selection, less than needed width // <--------7px--------> @@ -1693,26 +1883,41 @@ mod tests { // └────────┘x└────────┘ // column spacing (i.e. `x`) is always prioritized let table = Table::default().widths([Length(4), Length(4)]); - assert_eq!(table.get_column_widths(7, 3, 0), [(3, 2), (6, 1)]); + assert_eq!( + table.get_column_widths(7, 3, 0), + [Rect::new(3, 0, 2, 1), Rect::new(6, 0, 1, 1)] + ); } #[test] fn max_constraint() { // without selection, more than needed width let table = Table::default().widths([Max(4), Max(4)]); - assert_eq!(table.get_column_widths(20, 0, 0), [(0, 4), (5, 4)]); + assert_eq!( + table.get_column_widths(20, 0, 0), + [Rect::new(0, 0, 4, 1), Rect::new(5, 0, 4, 1)] + ); // with selection, more than needed width let table = Table::default().widths([Max(4), Max(4)]); - assert_eq!(table.get_column_widths(20, 3, 0), [(3, 4), (8, 4)]); + assert_eq!( + table.get_column_widths(20, 3, 0), + [Rect::new(3, 0, 4, 1), Rect::new(8, 0, 4, 1)] + ); // without selection, less than needed width let table = Table::default().widths([Max(4), Max(4)]); - assert_eq!(table.get_column_widths(7, 0, 0), [(0, 3), (4, 3)]); + assert_eq!( + table.get_column_widths(7, 0, 0), + [Rect::new(0, 0, 3, 1), Rect::new(4, 0, 3, 1)] + ); // with selection, less than needed width let table = Table::default().widths([Max(4), Max(4)]); - assert_eq!(table.get_column_widths(7, 3, 0), [(3, 2), (6, 1)]); + assert_eq!( + table.get_column_widths(7, 3, 0), + [Rect::new(3, 0, 2, 1), Rect::new(6, 0, 1, 1)] + ); } #[test] @@ -1723,42 +1928,66 @@ mod tests { // without selection, more than needed width let table = Table::default().widths([Min(4), Min(4)]); - assert_eq!(table.get_column_widths(20, 0, 0), [(0, 10), (11, 9)]); + assert_eq!( + table.get_column_widths(20, 0, 0), + [Rect::new(0, 0, 10, 1), Rect::new(11, 0, 9, 1)] + ); // with selection, more than needed width let table = Table::default().widths([Min(4), Min(4)]); - assert_eq!(table.get_column_widths(20, 3, 0), [(3, 8), (12, 8)]); + assert_eq!( + table.get_column_widths(20, 3, 0), + [Rect::new(3, 0, 8, 1), Rect::new(12, 0, 8, 1)] + ); // without selection, less than needed width // allocates spacer let table = Table::default().widths([Min(4), Min(4)]); - assert_eq!(table.get_column_widths(7, 0, 0), [(0, 3), (4, 3)]); + assert_eq!( + table.get_column_widths(7, 0, 0), + [Rect::new(0, 0, 3, 1), Rect::new(4, 0, 3, 1)] + ); // with selection, less than needed width // always allocates selection and spacer let table = Table::default().widths([Min(4), Min(4)]); - assert_eq!(table.get_column_widths(7, 3, 0), [(3, 2), (6, 1)]); + assert_eq!( + table.get_column_widths(7, 3, 0), + [Rect::new(3, 0, 2, 1), Rect::new(6, 0, 1, 1)] + ); } #[test] fn percentage_constraint() { // without selection, more than needed width let table = Table::default().widths([Percentage(30), Percentage(30)]); - assert_eq!(table.get_column_widths(20, 0, 0), [(0, 6), (7, 6)]); + assert_eq!( + table.get_column_widths(20, 0, 0), + [Rect::new(0, 0, 6, 1), Rect::new(7, 0, 6, 1)] + ); // with selection, more than needed width let table = Table::default().widths([Percentage(30), Percentage(30)]); - assert_eq!(table.get_column_widths(20, 3, 0), [(3, 5), (9, 5)]); + assert_eq!( + table.get_column_widths(20, 3, 0), + [Rect::new(3, 0, 5, 1), Rect::new(9, 0, 5, 1)] + ); // without selection, less than needed width // rounds from positions: [0.0, 0.0, 2.1, 3.1, 5.2, 7.0] let table = Table::default().widths([Percentage(30), Percentage(30)]); - assert_eq!(table.get_column_widths(7, 0, 0), [(0, 2), (3, 2)]); + assert_eq!( + table.get_column_widths(7, 0, 0), + [Rect::new(0, 0, 2, 1), Rect::new(3, 0, 2, 1)] + ); // with selection, less than needed width // rounds from positions: [0.0, 3.0, 5.1, 6.1, 7.0, 7.0] let table = Table::default().widths([Percentage(30), Percentage(30)]); - assert_eq!(table.get_column_widths(7, 3, 0), [(3, 1), (5, 1)]); + assert_eq!( + table.get_column_widths(7, 3, 0), + [Rect::new(3, 0, 1, 1), Rect::new(5, 0, 1, 1)] + ); } #[test] @@ -1766,22 +1995,34 @@ mod tests { // without selection, more than needed width // rounds from positions: [0.00, 0.00, 6.67, 7.67, 14.33] let table = Table::default().widths([Ratio(1, 3), Ratio(1, 3)]); - assert_eq!(table.get_column_widths(20, 0, 0), [(0, 7), (8, 6)]); + assert_eq!( + table.get_column_widths(20, 0, 0), + [Rect::new(0, 0, 7, 1), Rect::new(8, 0, 6, 1)] + ); // with selection, more than needed width // rounds from positions: [0.00, 3.00, 10.67, 17.33, 20.00] let table = Table::default().widths([Ratio(1, 3), Ratio(1, 3)]); - assert_eq!(table.get_column_widths(20, 3, 0), [(3, 6), (10, 5)]); + assert_eq!( + table.get_column_widths(20, 3, 0), + [Rect::new(3, 0, 6, 1), Rect::new(10, 0, 5, 1)] + ); // without selection, less than needed width // rounds from positions: [0.00, 2.33, 3.33, 5.66, 7.00] let table = Table::default().widths([Ratio(1, 3), Ratio(1, 3)]); - assert_eq!(table.get_column_widths(7, 0, 0), [(0, 2), (3, 3)]); + assert_eq!( + table.get_column_widths(7, 0, 0), + [Rect::new(0, 0, 2, 1), Rect::new(3, 0, 3, 1)] + ); // with selection, less than needed width // rounds from positions: [0.00, 3.00, 5.33, 6.33, 7.00, 7.00] let table = Table::default().widths([Ratio(1, 3), Ratio(1, 3)]); - assert_eq!(table.get_column_widths(7, 3, 0), [(3, 1), (5, 2)]); + assert_eq!( + table.get_column_widths(7, 3, 0), + [Rect::new(3, 0, 1, 1), Rect::new(5, 0, 2, 1)] + ); } /// When more width is available than requested, the behavior is controlled by flex @@ -1790,7 +2031,11 @@ mod tests { let table = Table::default().widths([Min(10), Min(10), Min(1)]); assert_eq!( table.get_column_widths(62, 0, 0), - &[(0, 20), (21, 20), (42, 20)] + &[ + Rect::new(0, 0, 20, 1), + Rect::new(21, 0, 20, 1), + Rect::new(42, 0, 20, 1) + ] ); let table = Table::default() @@ -1798,7 +2043,11 @@ mod tests { .flex(Flex::Legacy); assert_eq!( table.get_column_widths(62, 0, 0), - &[(0, 10), (11, 10), (22, 40)] + &[ + Rect::new(0, 0, 10, 1), + Rect::new(11, 0, 10, 1), + Rect::new(22, 0, 40, 1) + ] ); let table = Table::default() @@ -1806,7 +2055,11 @@ mod tests { .flex(Flex::SpaceBetween); assert_eq!( table.get_column_widths(62, 0, 0), - &[(0, 20), (21, 20), (42, 20)] + &[ + Rect::new(0, 0, 20, 1), + Rect::new(21, 0, 20, 1), + Rect::new(42, 0, 20, 1) + ] ); } @@ -1815,7 +2068,11 @@ mod tests { let table = Table::default().widths([Min(10), Min(10), Min(1)]); assert_eq!( table.get_column_widths(62, 0, 0), - &[(0, 20), (21, 20), (42, 20)] + &[ + Rect::new(0, 0, 20, 1), + Rect::new(21, 0, 20, 1), + Rect::new(42, 0, 20, 1) + ] ); let table = Table::default() @@ -1823,7 +2080,11 @@ mod tests { .flex(Flex::Legacy); assert_eq!( table.get_column_widths(62, 0, 0), - &[(0, 10), (11, 10), (22, 40)] + &[ + Rect::new(0, 0, 10, 1), + Rect::new(11, 0, 10, 1), + Rect::new(22, 0, 40, 1) + ] ); } @@ -1840,7 +2101,11 @@ mod tests { .column_spacing(0); assert_eq!( table.get_column_widths(30, 0, 3), - &[(0, 10), (10, 10), (20, 10)] + &[ + Rect::new(0, 0, 10, 1), + Rect::new(10, 0, 10, 1), + Rect::new(20, 0, 10, 1) + ] ); } @@ -1850,7 +2115,10 @@ mod tests { .rows(vec![]) .header(Row::new(vec!["f", "g"])) .column_spacing(0); - assert_eq!(table.get_column_widths(10, 0, 2), [(0, 5), (5, 5)]); + assert_eq!( + table.get_column_widths(10, 0, 2), + [Rect::new(0, 0, 5, 1), Rect::new(5, 0, 5, 1)] + ); } #[test] @@ -1859,7 +2127,10 @@ mod tests { .rows(vec![]) .footer(Row::new(vec!["h", "i"])) .column_spacing(0); - assert_eq!(table.get_column_widths(10, 0, 2), [(0, 5), (5, 5)]); + assert_eq!( + table.get_column_widths(10, 0, 2), + [Rect::new(0, 0, 5, 1), Rect::new(5, 0, 5, 1)] + ); } #[track_caller] @@ -2302,4 +2573,145 @@ mod tests { // This should not panic, even if the buffer has zero size. Widget::render(table, buffer.area, &mut buffer); } + + #[test] + fn get_area_for_column_span_one_no_more_columns() { + let columns = []; + let column_span = Table::get_cell_area(&mut columns.iter(), 1, 1); + assert!(column_span.is_none()); + } + + #[test] + fn get_area_for_column_span_two_no_more_columns() { + let columns = []; + let column_span = Table::get_cell_area(&mut columns.iter(), 2, 1); + assert!(column_span.is_none()); + } + + #[rstest] + #[case(&[Rect{x: 3, width: 2, y: 0, height: 1}, Rect{x: 3, width: 2, y: 0, height: 1}, Rect{x: 3, width: 2, y: 0, height: 1}], 2, 5)] + #[case(&[Rect{x: 3, width: 2, y: 0, height: 1}, Rect{x: 3, width: 2, y: 0, height: 1}], 2, 5,)] + #[case(&[Rect{x: 3, width: 2, y: 0, height: 1}, Rect{x: 3, width: 2, y: 0, height: 1}], 1, 2)] + #[case(&[Rect{x: 3, width: 2, y: 0, height: 1}, Rect{x: 3, width: 2, y: 0, height: 1}], 3, 5)] + #[case(&[Rect{x: 3, width: 2, y: 0, height: 1}], 1, 2)] + #[case(&[Rect{x: 3, width: 2, y: 0, height: 1}], 2, 2)] + #[case(&[ + Rect{x: 3, width: 2, y: 0, height: 1}, + Rect{x: 3, width: 2, y: 0, height: 1}, + Rect{x: 3, width: 2, y: 0, height: 1}, + Rect{x: 3, width: 2, y: 0, height: 1}, + ], 3, 8)] + #[case(&[ + Rect{x: 3, width: 2, y: 0, height: 1}, + Rect{x: 3, width: 2, y: 0, height: 1}, + Rect{x: 3, width: 2, y: 0, height: 1}, + ], 3, 8)] + fn test_colspan_width_single_column_spacing( + #[case] columns: &[Rect], + #[case] column_span: u16, + #[case] expected_column_width: u16, + ) { + let column_span = Table::get_cell_area(&mut columns.iter(), column_span, 1); + assert!(column_span.is_some()); + assert_eq!(column_span.unwrap().width, expected_column_width); + } + + #[rstest] + #[case(&[Rect{x: 3, width: 2, y: 0, height: 1}, Rect{x: 3, width: 2, y: 0, height: 1}, Rect{x: 3, width: 2, y: 0, height: 1}], 3, 10)] + #[case(&[Rect{x: 3, width: 2, y: 0, height: 1}], 3, 2)] + fn test_colspan_width_two_column_spacing( + #[case] columns: &[Rect], + #[case] column_span: u16, + #[case] expected_column_width: u16, + ) { + let column_span = Table::get_cell_area(&mut columns.iter(), column_span, 2); + assert!(column_span.is_some()); + assert_eq!(column_span.unwrap().width, expected_column_width); + } + + #[rstest] + #[case( + HighlightSpacing::Always, + 15, // width + 1, // spacing + None, // selection + [ + Cell::new("ABCDEFGHIJK").column_span(2), + Cell::new("12345678901"), + Cell::new("XYZXYZXYZXY"), + ], + [ + " ABCDEFGH 123", + " ", // row 2 + " ", // row 3 + ])] + #[case( + HighlightSpacing::Always, + 15, // width + 1, // spacing + Some(0), // selection + [ + Cell::new("ABCDEFGHIJK").column_span(2), + Cell::new("12345678901"), + Cell::new("XYZXYZXYZXY"), + ], + [ + ">>>ABCDEFGH 123", + " ", // row 2 + " ", // row 3 + ])] + #[case( + HighlightSpacing::WhenSelected, + 15, // width + 1, // spacing + None, // selection + [ + Cell::new("ABCDEFGHIJK").column_span(2), + Cell::new("12345678901"), + Cell::new("XYZXYZXYZXY"), + ], + [ + "ABCDEFGHIJ 1234", + " ", // row 2 + " ", // row 3 + ])] + #[case( + HighlightSpacing::WhenSelected, + 15, // width + 1, // spacing + Some(0), // selection + [ + Cell::new("ABCDEFGHIJK").column_span(2), + Cell::new("12345678901"), + Cell::new("XYZXYZXYZXY"), + ], + [ + ">>>ABCDEFGH 123", + " ", // row 2 + " ", // row 3 + ])] + fn test_table_with_selection_and_column_spans<'line, 'cell, Lines, Cells>( + #[case] highlight_spacing: HighlightSpacing, + #[case] columns: u16, + #[case] spacing: u16, + #[case] selection: Option, + #[case] cells: Cells, + #[case] expected: Lines, + ) where + Cells: IntoIterator, + Cells::Item: Into>, + Lines: IntoIterator, + Lines::Item: Into>, + { + let table = Table::default() + .rows(vec![Row::new(cells)]) + .highlight_spacing(highlight_spacing) + .highlight_symbol(">>>") + .column_spacing(spacing); + let area = Rect::new(0, 0, columns, 3); + let mut buf = Buffer::empty(area); + let mut state = TableState::default().with_selected(selection); + StatefulWidget::render(table, area, &mut buf, &mut state); + assert_eq!(buf, Buffer::with_lines(expected)); + } } diff --git a/ratatui-widgets/src/table/cell.rs b/ratatui-widgets/src/table/cell.rs index 6784d50a..9d1d5dd5 100644 --- a/ratatui-widgets/src/table/cell.rs +++ b/ratatui-widgets/src/table/cell.rs @@ -51,6 +51,8 @@ use ratatui_core::widgets::Widget; pub struct Cell<'a> { content: Text<'a>, style: Style, + /// The number of columns this cell will extend over + pub(crate) column_span: u16, } impl<'a> Cell<'a> { @@ -80,6 +82,7 @@ impl<'a> Cell<'a> { Self { content: content.into(), style: Style::default(), + column_span: 1, } } @@ -113,6 +116,26 @@ impl<'a> Cell<'a> { self } + /// Set the `column_span` of this cell + /// + /// This is a fluent setter method which must be chained or used as it consumes self + /// + /// # Example + /// ```rust + /// use ratatui::widgets::{Cell, Row}; + /// let rows = vec![ + /// Row::new(vec![Cell::new("12345").column_span(2)]), + /// Row::new(vec![Cell::new("xx"), Cell::new("yy")]), + /// ]; + /// // "12345", + /// // "xx yy", + /// ``` + #[must_use = "method moves the value of self and returns the modified value"] + pub const fn column_span(mut self, column_span: u16) -> Self { + self.column_span = column_span; + self + } + /// Set the `Style` of this cell /// /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or @@ -167,6 +190,7 @@ where Self { content: content.into(), style: Style::default(), + column_span: 1, } } }