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:
@@ -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)
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user