feat(table): let Cells span multiple columns (#2150)

Add a 'column_span' field to table cells. The default value
is 1; larger values will cause cells to span over multiple columns,
being rendered over all columns plus the spaces between them.

Fixes #1568.
This commit is contained in:
Kareem Khazem
2025-12-29 05:25:42 +00:00
committed by GitHub
parent 65c520245a
commit f9d066f4d7
3 changed files with 503 additions and 54 deletions

View File

@@ -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::<Row>()
.style(Style::new().fg(self.colors.row_fg).bg(color))
.height(4)

View File

@@ -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<Cell>,
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<Rect>
where
T: Iterator<Item = &'a Rect>,
{
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<Rect> {
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<Item = Row<'rows>>,
{
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<Item = Row<'rows>>,
{
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<usize>,
#[case] cells: Cells,
#[case] expected: Lines,
) where
Cells: IntoIterator,
Cells::Item: Into<Cell<'cell>>,
Lines: IntoIterator,
Lines::Item: Into<Line<'line>>,
{
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));
}
}

View File

@@ -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,
}
}
}