mirror of
https://github.com/ratatui-org/ratatui
synced 2024-11-25 22:20:31 +00:00
feat(table): Add Table::footer and Row::top_margin methods (#722)
* feat(table): Add a Table::footer method Signed-off-by: Antonio Yang <yanganto@gmail.com> * feat(table): Add a Row::top_margin method - add Row::top_margin - update table example Signed-off-by: Antonio Yang <yanganto@gmail.com> --------- Signed-off-by: Antonio Yang <yanganto@gmail.com>
This commit is contained in:
parent
63645333d6
commit
f025d2bfa2
3 changed files with 171 additions and 13 deletions
|
@ -127,6 +127,13 @@ fn ui(f: &mut Frame, app: &mut App) {
|
|||
.style(normal_style)
|
||||
.height(1)
|
||||
.bottom_margin(1);
|
||||
let footer_cells = ["Footer1", "Footer2", "Footer3"]
|
||||
.iter()
|
||||
.map(|f| Cell::from(*f).style(Style::default().fg(Color::Yellow)));
|
||||
let footer = Row::new(footer_cells)
|
||||
.style(normal_style)
|
||||
.height(1)
|
||||
.top_margin(1);
|
||||
let rows = app.items.iter().map(|item| {
|
||||
let height = item
|
||||
.iter()
|
||||
|
@ -146,6 +153,7 @@ fn ui(f: &mut Frame, app: &mut App) {
|
|||
],
|
||||
)
|
||||
.header(header)
|
||||
.footer(footer)
|
||||
.block(Block::default().borders(Borders::ALL).title("Table"))
|
||||
.highlight_style(selected_style)
|
||||
.highlight_symbol(">> ");
|
||||
|
|
|
@ -59,6 +59,7 @@ use crate::prelude::*;
|
|||
pub struct Row<'a> {
|
||||
pub(crate) cells: Vec<Cell<'a>>,
|
||||
pub(crate) height: u16,
|
||||
pub(crate) top_margin: u16,
|
||||
pub(crate) bottom_margin: u16,
|
||||
pub(crate) style: Style,
|
||||
}
|
||||
|
@ -141,6 +142,25 @@ impl<'a> Row<'a> {
|
|||
self
|
||||
}
|
||||
|
||||
/// Set the top margin. By default, the top margin is `0`.
|
||||
///
|
||||
/// The top margin is the number of blank lines to be displayed before the row.
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// # let cells = vec!["Cell 1", "Cell 2", "Cell 3"];
|
||||
/// let row = Row::default().top_margin(1);
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn top_margin(mut self, margin: u16) -> Self {
|
||||
self.top_margin = margin;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the bottom margin. By default, the bottom margin is `0`.
|
||||
///
|
||||
/// The bottom margin is the number of blank lines to be displayed after the row.
|
||||
|
@ -194,7 +214,9 @@ impl<'a> Row<'a> {
|
|||
impl Row<'_> {
|
||||
/// Returns the total height of the row.
|
||||
pub(crate) fn height_with_margin(&self) -> u16 {
|
||||
self.height.saturating_add(self.bottom_margin)
|
||||
self.height
|
||||
.saturating_add(self.top_margin)
|
||||
.saturating_add(self.bottom_margin)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -237,6 +259,12 @@ mod tests {
|
|||
assert_eq!(row.height, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn top_margin() {
|
||||
let row = Row::default().top_margin(1);
|
||||
assert_eq!(row.top_margin, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bottom_margin() {
|
||||
let row = Row::default().bottom_margin(1);
|
||||
|
|
|
@ -44,6 +44,7 @@ use crate::{
|
|||
///
|
||||
/// - [`Table::rows`] sets the rows of the [`Table`].
|
||||
/// - [`Table::header`] sets the header row of the [`Table`].
|
||||
/// - [`Table::footer`] sets the footer row of the [`Table`].
|
||||
/// - [`Table::widths`] sets the width constraints of each column.
|
||||
/// - [`Table::column_spacing`] sets the spacing between each column.
|
||||
/// - [`Table::block`] wraps the table in a [`Block`] widget.
|
||||
|
@ -76,6 +77,8 @@ use crate::{
|
|||
/// // To add space between the header and the rest of the rows, specify the margin
|
||||
/// .bottom_margin(1),
|
||||
/// )
|
||||
/// // It has an optional footer, which is simply a Row always visible at the bottom.
|
||||
/// .footer(Row::new(vec!["Updated on Dec 28"]))
|
||||
/// // As any other widget, a Table can be wrapped in a Block.
|
||||
/// .block(Block::default().title("Table"))
|
||||
/// // The selected row and its content can also be styled.
|
||||
|
@ -178,6 +181,9 @@ pub struct Table<'a> {
|
|||
/// Optional header
|
||||
header: Option<Row<'a>>,
|
||||
|
||||
/// Optional footer
|
||||
footer: Option<Row<'a>>,
|
||||
|
||||
/// Width constraints for each column
|
||||
widths: Vec<Constraint>,
|
||||
|
||||
|
@ -294,6 +300,28 @@ impl<'a> Table<'a> {
|
|||
self
|
||||
}
|
||||
|
||||
/// Sets the footer row
|
||||
///
|
||||
/// The `footer` parameter is a [`Row`] which will be displayed at the bottom of the [`Table`]
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let footer = Row::new(vec![
|
||||
/// Cell::from("Footer Cell 1"),
|
||||
/// Cell::from("Footer Cell 2"),
|
||||
/// ]);
|
||||
/// let table = Table::default().footer(footer);
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn footer(mut self, footer: Row<'a>) -> Self {
|
||||
self.footer = Some(footer);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the widths of the columns.
|
||||
///
|
||||
/// The `widths` parameter accepts anything which be converted to an Iterator of Constraints
|
||||
|
@ -521,7 +549,7 @@ impl StatefulWidget for Table<'_> {
|
|||
let columns_widths = self.get_columns_widths(table_area.width, selection_width);
|
||||
let highlight_symbol = self.highlight_symbol.unwrap_or("");
|
||||
|
||||
let (header_area, rows_area) = self.layout(table_area);
|
||||
let (header_area, rows_area, footer_area) = self.layout(table_area);
|
||||
|
||||
self.render_header(header_area, buf, &columns_widths);
|
||||
|
||||
|
@ -531,22 +559,37 @@ impl StatefulWidget for Table<'_> {
|
|||
state,
|
||||
selection_width,
|
||||
highlight_symbol,
|
||||
columns_widths,
|
||||
&columns_widths,
|
||||
);
|
||||
|
||||
self.render_footer(footer_area, buf, columns_widths);
|
||||
}
|
||||
}
|
||||
|
||||
// private methods for rendering
|
||||
impl Table<'_> {
|
||||
/// Splits the table area into a header and rows area
|
||||
fn layout(&self, area: Rect) -> (Rect, Rect) {
|
||||
let header_height = self.header.as_ref().map_or(0, |h| h.height_with_margin());
|
||||
/// Splits the table area into a header, rows area and a footer
|
||||
fn layout(&self, area: Rect) -> (Rect, Rect, Rect) {
|
||||
let header_top_margin = self.header.as_ref().map_or(0, |h| h.top_margin);
|
||||
let header_height = self.header.as_ref().map_or(0, |h| h.height);
|
||||
let header_bottom_margin = self.header.as_ref().map_or(0, |h| h.bottom_margin);
|
||||
let footer_top_margin = self.footer.as_ref().map_or(0, |h| h.top_margin);
|
||||
let footer_height = self.footer.as_ref().map_or(0, |f| f.height);
|
||||
let footer_bottom_margin = self.footer.as_ref().map_or(0, |h| h.bottom_margin);
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Length(header_height), Constraint::Min(0)])
|
||||
.constraints([
|
||||
Constraint::Length(header_top_margin),
|
||||
Constraint::Length(header_height),
|
||||
Constraint::Length(header_bottom_margin),
|
||||
Constraint::Min(0),
|
||||
Constraint::Length(footer_top_margin),
|
||||
Constraint::Length(footer_height),
|
||||
Constraint::Length(footer_bottom_margin),
|
||||
])
|
||||
.split(area);
|
||||
let (header_area, rows_area) = (layout[0], layout[1]);
|
||||
(header_area, rows_area)
|
||||
let (header_area, rows_area, footer_area) = (layout[1], layout[3], layout[5]);
|
||||
(header_area, rows_area, footer_area)
|
||||
}
|
||||
|
||||
fn render_block(&mut self, area: Rect, buf: &mut Buffer) -> Rect {
|
||||
|
@ -568,6 +611,15 @@ impl Table<'_> {
|
|||
}
|
||||
}
|
||||
|
||||
fn render_footer(&self, area: Rect, buf: &mut Buffer, column_widths: Vec<(u16, u16)>) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_rows(
|
||||
&self,
|
||||
area: Rect,
|
||||
|
@ -575,7 +627,7 @@ impl Table<'_> {
|
|||
state: &mut TableState,
|
||||
selection_width: u16,
|
||||
highlight_symbol: &str,
|
||||
columns_widths: Vec<(u16, u16)>,
|
||||
columns_widths: &[(u16, u16)],
|
||||
) {
|
||||
if self.rows.is_empty() {
|
||||
return;
|
||||
|
@ -595,9 +647,9 @@ impl Table<'_> {
|
|||
{
|
||||
let row_area = Rect::new(
|
||||
area.x,
|
||||
area.y + y_offset,
|
||||
area.y + y_offset + row.top_margin,
|
||||
area.width,
|
||||
row.height_with_margin(),
|
||||
row.height_with_margin() - row.top_margin,
|
||||
);
|
||||
buf.set_style(row_area, row.style);
|
||||
|
||||
|
@ -637,6 +689,7 @@ impl Table<'_> {
|
|||
.rows
|
||||
.iter()
|
||||
.chain(self.header.iter())
|
||||
.chain(self.footer.iter())
|
||||
.map(|r| r.cells.len())
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
|
@ -807,6 +860,13 @@ mod tests {
|
|||
assert_eq!(table.header, Some(header));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn footer() {
|
||||
let footer = Row::new(vec![Cell::from("")]);
|
||||
let table = Table::default().footer(footer.clone());
|
||||
assert_eq!(table.footer, Some(footer));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn highlight_style() {
|
||||
let style = Style::default().red().italic();
|
||||
|
@ -914,6 +974,42 @@ mod tests {
|
|||
assert_buffer_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_with_footer() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
|
||||
let footer = Row::new(vec!["Foot1", "Foot2"]);
|
||||
let rows = vec![
|
||||
Row::new(vec!["Cell1", "Cell2"]),
|
||||
Row::new(vec!["Cell3", "Cell4"]),
|
||||
];
|
||||
let table = Table::new(rows, [Constraint::Length(5); 2]).footer(footer);
|
||||
Widget::render(table, Rect::new(0, 0, 15, 3), &mut buf);
|
||||
let expected = Buffer::with_lines(vec![
|
||||
"Cell1 Cell2 ",
|
||||
"Cell3 Cell4 ",
|
||||
"Foot1 Foot2 ",
|
||||
]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_with_header_and_footer() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
|
||||
let header = Row::new(vec!["Head1", "Head2"]);
|
||||
let footer = Row::new(vec!["Foot1", "Foot2"]);
|
||||
let rows = vec![Row::new(vec!["Cell1", "Cell2"])];
|
||||
let table = Table::new(rows, [Constraint::Length(5); 2])
|
||||
.header(header)
|
||||
.footer(footer);
|
||||
Widget::render(table, Rect::new(0, 0, 15, 3), &mut buf);
|
||||
let expected = Buffer::with_lines(vec![
|
||||
"Head1 Head2 ",
|
||||
"Cell1 Cell2 ",
|
||||
"Foot1 Foot2 ",
|
||||
]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_with_header_margin() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
|
||||
|
@ -932,6 +1028,21 @@ mod tests {
|
|||
assert_buffer_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_with_footer_margin() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
|
||||
let footer = Row::new(vec!["Foot1", "Foot2"]).top_margin(1);
|
||||
let rows = vec![Row::new(vec!["Cell1", "Cell2"])];
|
||||
let table = Table::new(rows, [Constraint::Length(5); 2]).footer(footer);
|
||||
Widget::render(table, Rect::new(0, 0, 15, 3), &mut buf);
|
||||
let expected = Buffer::with_lines(vec![
|
||||
"Cell1 Cell2 ",
|
||||
" ",
|
||||
"Foot1 Foot2 ",
|
||||
]);
|
||||
assert_buffer_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_with_row_margin() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
|
||||
|
@ -971,7 +1082,8 @@ mod tests {
|
|||
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])
|
||||
.header(Row::new([Line::from("").alignment(Alignment::Right)]));
|
||||
.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);
|
||||
}
|
||||
|
||||
|
@ -1259,6 +1371,7 @@ mod tests {
|
|||
])
|
||||
// rows should get precedence over header
|
||||
.header(Row::new(vec!["f", "g"]))
|
||||
.footer(Row::new(vec!["h", "i"]))
|
||||
.column_spacing(0);
|
||||
assert_eq!(
|
||||
table.get_columns_widths(30, 0),
|
||||
|
@ -1274,6 +1387,15 @@ mod tests {
|
|||
.column_spacing(0);
|
||||
assert_eq!(table.get_columns_widths(10, 0), &[(0, 5), (5, 5)])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_constraint_with_footer() {
|
||||
let table = Table::default()
|
||||
.rows(vec![])
|
||||
.footer(Row::new(vec!["h", "i"]))
|
||||
.column_spacing(0);
|
||||
assert_eq!(table.get_columns_widths(10, 0), &[(0, 5), (5, 5)])
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
Loading…
Reference in a new issue