mirror of
https://github.com/ratatui/ratatui.git
synced 2025-10-02 07:21:24 +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
@ -127,6 +127,13 @@ fn ui(f: &mut Frame, app: &mut App) {
|
|||||||
.style(normal_style)
|
.style(normal_style)
|
||||||
.height(1)
|
.height(1)
|
||||||
.bottom_margin(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 rows = app.items.iter().map(|item| {
|
||||||
let height = item
|
let height = item
|
||||||
.iter()
|
.iter()
|
||||||
@ -146,6 +153,7 @@ fn ui(f: &mut Frame, app: &mut App) {
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
.header(header)
|
.header(header)
|
||||||
|
.footer(footer)
|
||||||
.block(Block::default().borders(Borders::ALL).title("Table"))
|
.block(Block::default().borders(Borders::ALL).title("Table"))
|
||||||
.highlight_style(selected_style)
|
.highlight_style(selected_style)
|
||||||
.highlight_symbol(">> ");
|
.highlight_symbol(">> ");
|
||||||
|
@ -59,6 +59,7 @@ use crate::prelude::*;
|
|||||||
pub struct Row<'a> {
|
pub struct Row<'a> {
|
||||||
pub(crate) cells: Vec<Cell<'a>>,
|
pub(crate) cells: Vec<Cell<'a>>,
|
||||||
pub(crate) height: u16,
|
pub(crate) height: u16,
|
||||||
|
pub(crate) top_margin: u16,
|
||||||
pub(crate) bottom_margin: u16,
|
pub(crate) bottom_margin: u16,
|
||||||
pub(crate) style: Style,
|
pub(crate) style: Style,
|
||||||
}
|
}
|
||||||
@ -141,6 +142,25 @@ impl<'a> Row<'a> {
|
|||||||
self
|
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`.
|
/// 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.
|
/// 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<'_> {
|
impl Row<'_> {
|
||||||
/// Returns the total height of the row.
|
/// Returns the total height of the row.
|
||||||
pub(crate) fn height_with_margin(&self) -> u16 {
|
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);
|
assert_eq!(row.height, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn top_margin() {
|
||||||
|
let row = Row::default().top_margin(1);
|
||||||
|
assert_eq!(row.top_margin, 1);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn bottom_margin() {
|
fn bottom_margin() {
|
||||||
let row = Row::default().bottom_margin(1);
|
let row = Row::default().bottom_margin(1);
|
||||||
|
@ -44,6 +44,7 @@ use crate::{
|
|||||||
///
|
///
|
||||||
/// - [`Table::rows`] sets the rows of the [`Table`].
|
/// - [`Table::rows`] sets the rows of the [`Table`].
|
||||||
/// - [`Table::header`] sets the header row 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::widths`] sets the width constraints of each column.
|
||||||
/// - [`Table::column_spacing`] sets the spacing between each column.
|
/// - [`Table::column_spacing`] sets the spacing between each column.
|
||||||
/// - [`Table::block`] wraps the table in a [`Block`] widget.
|
/// - [`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
|
/// // To add space between the header and the rest of the rows, specify the margin
|
||||||
/// .bottom_margin(1),
|
/// .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.
|
/// // As any other widget, a Table can be wrapped in a Block.
|
||||||
/// .block(Block::default().title("Table"))
|
/// .block(Block::default().title("Table"))
|
||||||
/// // The selected row and its content can also be styled.
|
/// // The selected row and its content can also be styled.
|
||||||
@ -178,6 +181,9 @@ pub struct Table<'a> {
|
|||||||
/// Optional header
|
/// Optional header
|
||||||
header: Option<Row<'a>>,
|
header: Option<Row<'a>>,
|
||||||
|
|
||||||
|
/// Optional footer
|
||||||
|
footer: Option<Row<'a>>,
|
||||||
|
|
||||||
/// Width constraints for each column
|
/// Width constraints for each column
|
||||||
widths: Vec<Constraint>,
|
widths: Vec<Constraint>,
|
||||||
|
|
||||||
@ -294,6 +300,28 @@ impl<'a> Table<'a> {
|
|||||||
self
|
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.
|
/// Set the widths of the columns.
|
||||||
///
|
///
|
||||||
/// The `widths` parameter accepts anything which be converted to an Iterator of Constraints
|
/// 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 columns_widths = self.get_columns_widths(table_area.width, selection_width);
|
||||||
let highlight_symbol = self.highlight_symbol.unwrap_or("");
|
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);
|
self.render_header(header_area, buf, &columns_widths);
|
||||||
|
|
||||||
@ -531,22 +559,37 @@ impl StatefulWidget for Table<'_> {
|
|||||||
state,
|
state,
|
||||||
selection_width,
|
selection_width,
|
||||||
highlight_symbol,
|
highlight_symbol,
|
||||||
columns_widths,
|
&columns_widths,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
self.render_footer(footer_area, buf, columns_widths);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// private methods for rendering
|
// private methods for rendering
|
||||||
impl Table<'_> {
|
impl Table<'_> {
|
||||||
/// Splits the table area into a header and rows area
|
/// Splits the table area into a header, rows area and a footer
|
||||||
fn layout(&self, area: Rect) -> (Rect, Rect) {
|
fn layout(&self, area: Rect) -> (Rect, Rect, Rect) {
|
||||||
let header_height = self.header.as_ref().map_or(0, |h| h.height_with_margin());
|
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()
|
let layout = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.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);
|
.split(area);
|
||||||
let (header_area, rows_area) = (layout[0], layout[1]);
|
let (header_area, rows_area, footer_area) = (layout[1], layout[3], layout[5]);
|
||||||
(header_area, rows_area)
|
(header_area, rows_area, footer_area)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_block(&mut self, area: Rect, buf: &mut Buffer) -> Rect {
|
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(
|
fn render_rows(
|
||||||
&self,
|
&self,
|
||||||
area: Rect,
|
area: Rect,
|
||||||
@ -575,7 +627,7 @@ impl Table<'_> {
|
|||||||
state: &mut TableState,
|
state: &mut TableState,
|
||||||
selection_width: u16,
|
selection_width: u16,
|
||||||
highlight_symbol: &str,
|
highlight_symbol: &str,
|
||||||
columns_widths: Vec<(u16, u16)>,
|
columns_widths: &[(u16, u16)],
|
||||||
) {
|
) {
|
||||||
if self.rows.is_empty() {
|
if self.rows.is_empty() {
|
||||||
return;
|
return;
|
||||||
@ -595,9 +647,9 @@ impl Table<'_> {
|
|||||||
{
|
{
|
||||||
let row_area = Rect::new(
|
let row_area = Rect::new(
|
||||||
area.x,
|
area.x,
|
||||||
area.y + y_offset,
|
area.y + y_offset + row.top_margin,
|
||||||
area.width,
|
area.width,
|
||||||
row.height_with_margin(),
|
row.height_with_margin() - row.top_margin,
|
||||||
);
|
);
|
||||||
buf.set_style(row_area, row.style);
|
buf.set_style(row_area, row.style);
|
||||||
|
|
||||||
@ -637,6 +689,7 @@ impl Table<'_> {
|
|||||||
.rows
|
.rows
|
||||||
.iter()
|
.iter()
|
||||||
.chain(self.header.iter())
|
.chain(self.header.iter())
|
||||||
|
.chain(self.footer.iter())
|
||||||
.map(|r| r.cells.len())
|
.map(|r| r.cells.len())
|
||||||
.max()
|
.max()
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
@ -807,6 +860,13 @@ mod tests {
|
|||||||
assert_eq!(table.header, Some(header));
|
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]
|
#[test]
|
||||||
fn highlight_style() {
|
fn highlight_style() {
|
||||||
let style = Style::default().red().italic();
|
let style = Style::default().red().italic();
|
||||||
@ -914,6 +974,42 @@ mod tests {
|
|||||||
assert_buffer_eq!(buf, expected);
|
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]
|
#[test]
|
||||||
fn render_with_header_margin() {
|
fn render_with_header_margin() {
|
||||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
|
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
|
||||||
@ -932,6 +1028,21 @@ mod tests {
|
|||||||
assert_buffer_eq!(buf, expected);
|
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]
|
#[test]
|
||||||
fn render_with_row_margin() {
|
fn render_with_row_margin() {
|
||||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
|
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
|
||||||
@ -971,7 +1082,8 @@ mod tests {
|
|||||||
fn render_with_overflow_does_not_panic() {
|
fn render_with_overflow_does_not_panic() {
|
||||||
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 3));
|
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 3));
|
||||||
let table = Table::new(vec![], [Constraint::Min(20); 1])
|
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);
|
Widget::render(table, Rect::new(0, 0, 20, 3), &mut buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1259,6 +1371,7 @@ mod tests {
|
|||||||
])
|
])
|
||||||
// rows should get precedence over header
|
// rows should get precedence over header
|
||||||
.header(Row::new(vec!["f", "g"]))
|
.header(Row::new(vec!["f", "g"]))
|
||||||
|
.footer(Row::new(vec!["h", "i"]))
|
||||||
.column_spacing(0);
|
.column_spacing(0);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
table.get_columns_widths(30, 0),
|
table.get_columns_widths(30, 0),
|
||||||
@ -1274,6 +1387,15 @@ mod tests {
|
|||||||
.column_spacing(0);
|
.column_spacing(0);
|
||||||
assert_eq!(table.get_columns_widths(10, 0), &[(0, 5), (5, 5)])
|
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]
|
#[test]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user