refactor(table): split table into multiple files (#718)

At close to 2000 lines of code, the table widget was getting a bit
unwieldy. This commit splits it into multiple files, one for each
struct, and one for the table itself.

Also refactors the table rendering code to be easier to maintain.
This commit is contained in:
Josh McKinney 2023-12-27 20:43:01 -08:00 committed by GitHub
parent 5d410c6895
commit 63645333d6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 2023 additions and 1857 deletions

File diff suppressed because it is too large Load diff

212
src/widgets/table/cell.rs Normal file
View file

@ -0,0 +1,212 @@
use crate::prelude::*;
/// A [`Cell`] contains the [`Text`] to be displayed in a [`Row`] of a [`Table`].
///
/// You can apply a [`Style`] to the [`Cell`] using [`Cell::style`]. This will set the style for the
/// entire area of the cell. Any [`Style`] set on the [`Text`] content will be combined with the
/// [`Style`] of the [`Cell`] by adding the [`Style`] of the [`Text`] content to the [`Style`] of
/// the [`Cell`]. Styles set on the text content will only affect the content.
///
/// # Examples
///
/// You can create a `Cell` from anything that can be converted to a [`Text`].
///
/// ```rust
/// use std::borrow::Cow;
///
/// use ratatui::{prelude::*, widgets::*};
///
/// Cell::from("simple string");
/// Cell::from(Span::from("span"));
/// Cell::from(Line::from(vec![
/// Span::raw("a vec of "),
/// Span::styled("spans", Style::default().add_modifier(Modifier::BOLD)),
/// ]));
/// Cell::from(Text::from("a text"));
/// Cell::from(Text::from(Cow::Borrowed("hello")));
/// ```
///
/// `Cell` implements [`Styled`] which means you can use style shorthands from the [`Stylize`] trait
/// to set the style of the cell concisely.
///
/// ```rust
/// use ratatui::{prelude::*, widgets::*};
/// Cell::new("Cell 1").red().italic();
/// ```
///
/// [`Row`]: super::Row
/// [`Table`]: super::Table
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Cell<'a> {
content: Text<'a>,
style: Style,
}
impl<'a> Cell<'a> {
/// Creates a new [`Cell`]
///
/// The `content` parameter accepts any value that can be converted into a [`Text`].
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// Cell::new("simple string");
/// Cell::new(Span::from("span"));
/// Cell::new(Line::from(vec![
/// Span::raw("a vec of "),
/// Span::styled("spans", Style::default().add_modifier(Modifier::BOLD)),
/// ]));
/// Cell::new(Text::from("a text"));
/// ```
pub fn new<T>(content: T) -> Self
where
T: Into<Text<'a>>,
{
Self {
content: content.into(),
style: Style::default(),
}
}
/// Set the content of the [`Cell`]
///
/// The `content` parameter accepts any value that can be converted into a [`Text`].
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// Cell::default().content("simple string");
/// Cell::default().content(Span::from("span"));
/// Cell::default().content(Line::from(vec![
/// Span::raw("a vec of "),
/// Span::styled("spans", Style::new().bold()),
/// ]));
/// Cell::default().content(Text::from("a text"));
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn content<T>(mut self, content: T) -> Self
where
T: Into<Text<'a>>,
{
self.content = content.into();
self
}
/// Set the `Style` of this cell
///
/// This `Style` will override the `Style` of the [`Row`] and can be overridden by the `Style`
/// of the [`Text`] content.
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// Cell::new("Cell 1").style(Style::new().red().italic());
/// ```
///
/// `Cell` also implements the [`Styled`] trait, which means you can use style shorthands from
/// the [`Stylize`] trait to set the style of the widget more concisely.
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// Cell::new("Cell 1").red().italic();
/// ```
///
/// [`Row`]: super::Row
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
}
impl Cell<'_> {
pub(crate) fn render(&self, area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style);
for (i, line) in self.content.lines.iter().enumerate() {
if i as u16 >= area.height {
break;
}
let x_offset = match line.alignment {
Some(Alignment::Center) => (area.width / 2).saturating_sub(line.width() as u16 / 2),
Some(Alignment::Right) => area.width.saturating_sub(line.width() as u16),
_ => 0,
};
let x = area.x + x_offset;
if x >= area.right() {
continue;
}
buf.set_line(x, area.y + i as u16, line, area.width);
}
}
}
impl<'a, T> From<T> for Cell<'a>
where
T: Into<Text<'a>>,
{
fn from(content: T) -> Cell<'a> {
Cell {
content: content.into(),
style: Style::default(),
}
}
}
impl<'a> Styled for Cell<'a> {
type Item = Cell<'a>;
fn style(&self) -> Style {
self.style
}
fn set_style(self, style: Style) -> Self::Item {
self.style(style)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::style::{Color, Modifier, Style, Stylize};
#[test]
fn new() {
let cell = Cell::new("");
assert_eq!(cell.content, Text::from(""));
}
#[test]
fn content() {
let cell = Cell::default().content("");
assert_eq!(cell.content, Text::from(""));
}
#[test]
fn style() {
let style = Style::default().red().italic();
let cell = Cell::default().style(style);
assert_eq!(cell.style, style);
}
#[test]
fn stylize() {
assert_eq!(
Cell::from("").black().on_white().bold().not_dim().style,
Style::default()
.fg(Color::Black)
.bg(Color::White)
.add_modifier(Modifier::BOLD)
.remove_modifier(Modifier::DIM)
)
}
}

269
src/widgets/table/row.rs Normal file
View file

@ -0,0 +1,269 @@
use super::*;
use crate::prelude::*;
/// A single row of data to be displayed in a [`Table`] widget.
///
/// A `Row` is a collection of [`Cell`]s.
///
/// By default, a row has a height of 1 but you can change this using [`Row::height`].
///
/// You can set the style of the entire row using [`Row::style`]. This [`Style`] will be combined
/// with the [`Style`] of each individual [`Cell`] by adding the [`Style`] of the [`Cell`] to the
/// [`Style`] of the [`Row`].
///
/// # Examples
///
/// You can create `Row`s from simple strings.
///
/// ```rust
/// use ratatui::{prelude::*, widgets::*};
///
/// Row::new(vec!["Cell1", "Cell2", "Cell3"]);
/// ```
///
/// If you need a bit more control over individual cells, you can explicitly create [`Cell`]s:
///
/// ```rust
/// use ratatui::{prelude::*, widgets::*};
///
/// Row::new(vec![
/// Cell::from("Cell1"),
/// Cell::from("Cell2").style(Style::default().fg(Color::Yellow)),
/// ]);
/// ```
///
/// You can also construct a row from any type that can be converted into [`Text`]:
///
/// ```rust
/// use std::borrow::Cow;
///
/// use ratatui::{prelude::*, widgets::*};
///
/// Row::new(vec![
/// Cow::Borrowed("hello"),
/// Cow::Owned("world".to_uppercase()),
/// ]);
/// ```
///
/// `Row` implements [`Styled`] which means you can use style shorthands from the [`Stylize`] trait
/// to set the style of the row concisely.
///
/// ```rust
/// use ratatui::{prelude::*, widgets::*};
/// let cells = vec!["Cell1", "Cell2", "Cell3"];
/// Row::new(cells).red().italic();
/// ```
///
/// [`Table`]: super::Table
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Row<'a> {
pub(crate) cells: Vec<Cell<'a>>,
pub(crate) height: u16,
pub(crate) bottom_margin: u16,
pub(crate) style: Style,
}
impl<'a> Row<'a> {
/// Creates a new [`Row`]
///
/// The `cells` parameter accepts any value that can be converted into an iterator of anything
/// that can be converted into a [`Cell`] (e.g. `Vec<&str>`, `&[Cell<'a>]`, `Vec<String>`, etc.)
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let row = Row::new(vec!["Cell 1", "Cell 2", "Cell 3"]);
/// let row = Row::new(vec![
/// Cell::new("Cell 1"),
/// Cell::new("Cell 2"),
/// Cell::new("Cell 3"),
/// ]);
/// ```
pub fn new<T>(cells: T) -> Self
where
T: IntoIterator,
T::Item: Into<Cell<'a>>,
{
Self {
cells: cells.into_iter().map(Into::into).collect(),
height: 1,
..Default::default()
}
}
/// Set the cells of the [`Row`]
///
/// The `cells` parameter accepts any value that can be converted into an iterator of anything
/// that can be converted into a [`Cell`] (e.g. `Vec<&str>`, `&[Cell<'a>]`, `Vec<String>`, etc.)
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let row = Row::default().cells(vec!["Cell 1", "Cell 2", "Cell 3"]);
/// let row = Row::default().cells(vec![
/// Cell::new("Cell 1"),
/// Cell::new("Cell 2"),
/// Cell::new("Cell 3"),
/// ]);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn cells<T>(mut self, cells: T) -> Self
where
T: IntoIterator,
T::Item: Into<Cell<'a>>,
{
self.cells = cells.into_iter().map(Into::into).collect();
self
}
/// Set the fixed height of the [`Row`]
///
/// Any [`Cell`] whose content has more lines than this height will see its content truncated.
///
/// By default, the height is `1`.
///
/// 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\nline 2", "Cell 2", "Cell 3"];
/// let row = Row::new(cells).height(2);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn height(mut self, height: u16) -> Self {
self.height = height;
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.
///
/// 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().bottom_margin(1);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn bottom_margin(mut self, margin: u16) -> Self {
self.bottom_margin = margin;
self
}
/// Set the [`Style`] of the entire row
///
/// This [`Style`] can be overridden by the [`Style`] of a any individual [`Cell`] or by their
/// [`Text`] content.
///
/// 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::new(cells).style(Style::new().red().italic());
/// ```
///
/// `Row` also implements the [`Styled`] trait, which means you can use style shorthands from
/// the [`Stylize`] trait to set the style of the widget more concisely.
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let cells = vec!["Cell 1", "Cell 2", "Cell 3"];
/// let row = Row::new(cells).red().italic();
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
}
// private methods for rendering
impl Row<'_> {
/// Returns the total height of the row.
pub(crate) fn height_with_margin(&self) -> u16 {
self.height.saturating_add(self.bottom_margin)
}
}
impl<'a> Styled for Row<'a> {
type Item = Row<'a>;
fn style(&self) -> Style {
self.style
}
fn set_style(self, style: Style) -> Self::Item {
self.style(style)
}
}
#[cfg(test)]
mod tests {
use std::vec;
use super::*;
use crate::style::{Color, Modifier, Style, Stylize};
#[test]
fn new() {
let cells = vec![Cell::from("")];
let row = Row::new(cells.clone());
assert_eq!(row.cells, cells);
}
#[test]
fn cells() {
let cells = vec![Cell::from("")];
let row = Row::default().cells(cells.clone());
assert_eq!(row.cells, cells);
}
#[test]
fn height() {
let row = Row::default().height(2);
assert_eq!(row.height, 2);
}
#[test]
fn bottom_margin() {
let row = Row::default().bottom_margin(1);
assert_eq!(row.bottom_margin, 1);
}
#[test]
fn style() {
let style = Style::default().red().italic();
let row = Row::default().style(style);
assert_eq!(row.style, style);
}
#[test]
fn stylize() {
assert_eq!(
Row::new(vec![Cell::from("")])
.black()
.on_white()
.bold()
.not_italic()
.style,
Style::default()
.fg(Color::Black)
.bg(Color::White)
.add_modifier(Modifier::BOLD)
.remove_modifier(Modifier::ITALIC)
)
}
}

1295
src/widgets/table/table.rs Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,238 @@
/// State of a [`Table`] widget
///
/// This state can be used to scroll through the rows and select one of them. When the table is
/// rendered as a stateful widget, the selected row will be highlighted and the table will be
/// shifted to ensure that the selected row is visible. This will modify the [`TableState`] object
/// passed to the [`Frame::render_stateful_widget`] method.
///
/// The state consists of two fields:
/// - [`offset`]: the index of the first row to be displayed
/// - [`selected`]: the index of the selected row, which can be `None` if no row is selected
///
/// [`offset`]: TableState::offset()
/// [`selected`]: TableState::selected()
///
/// See the [table example] and the recipe and traceroute tabs in the [demo2 example] for a more in
/// depth example of the various configuration options and for how to handle state.
///
/// [table example]: https://github.com/ratatui-org/ratatui/blob/master/examples/table.rs
/// [demo2 example]: https://github.com/ratatui-org/ratatui/blob/master/examples/demo2/
///
/// # Example
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # fn ui(frame: &mut Frame) {
/// # let area = Rect::default();
/// # let rows = [Row::new(vec!["Cell1", "Cell2"])];
/// # let widths = [Constraint::Length(5), Constraint::Length(5)];
/// let table = Table::new(rows, widths).widths(widths);
///
/// // Note: TableState should be stored in your application state (not constructed in your render
/// // method) so that the selected row is preserved across renders
/// let mut table_state = TableState::default();
/// *table_state.offset_mut() = 1; // display the second row and onwards
/// table_state.select(Some(3)); // select the forth row (0-indexed)
///
/// frame.render_stateful_widget(table, area, &mut table_state);
/// # }
/// ```
///
/// Note that if [`Table::widths`] is not called before rendering, the rendered columns will have
/// equal width.
///
/// [`Table`]: crate::widgets::Table
/// [`Table::widths`]: crate::widgets::Table::widths
/// [`Frame::render_stateful_widget`]: crate::Frame::render_stateful_widget
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct TableState {
pub(crate) offset: usize,
pub(crate) selected: Option<usize>,
}
impl TableState {
/// Creates a new [`TableState`]
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let state = TableState::new();
/// ```
pub fn new() -> Self {
Self::default()
}
/// Sets the index of the first row to be displayed
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let state = TableState::new().with_offset(1);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn with_offset(mut self, offset: usize) -> Self {
self.offset = offset;
self
}
/// Sets the index of the selected row
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let state = TableState::new().with_selected(Some(1));
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn with_selected<T>(mut self, selected: T) -> Self
where
T: Into<Option<usize>>,
{
self.selected = selected.into();
self
}
/// Index of the first row to be displayed
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let state = TableState::new();
/// assert_eq!(state.offset(), 0);
/// ```
pub fn offset(&self) -> usize {
self.offset
}
/// Mutable reference to the index of the first row to be displayed
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = TableState::default();
/// *state.offset_mut() = 1;
/// ```
pub fn offset_mut(&mut self) -> &mut usize {
&mut self.offset
}
/// Index of the selected row
///
/// Returns `None` if no row is selected
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let state = TableState::new();
/// assert_eq!(state.selected(), None);
/// ```
pub fn selected(&self) -> Option<usize> {
self.selected
}
/// Mutable reference to the index of the selected row
///
/// Returns `None` if no row is selected
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = TableState::default();
/// *state.selected_mut() = Some(1);
/// ```
pub fn selected_mut(&mut self) -> &mut Option<usize> {
&mut self.selected
}
/// Sets the index of the selected row
///
/// Set to `None` if no row is selected. This will also reset the offset to `0`.
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = TableState::default();
/// state.select(Some(1));
/// ```
pub fn select(&mut self, index: Option<usize>) {
self.selected = index;
if index.is_none() {
self.offset = 0;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new() {
let state = TableState::new();
assert_eq!(state.offset, 0);
assert_eq!(state.selected, None);
}
#[test]
fn with_offset() {
let state = TableState::new().with_offset(1);
assert_eq!(state.offset, 1);
}
#[test]
fn with_selected() {
let state = TableState::new().with_selected(Some(1));
assert_eq!(state.selected, Some(1));
}
#[test]
fn offset() {
let state = TableState::new();
assert_eq!(state.offset(), 0);
}
#[test]
fn offset_mut() {
let mut state = TableState::new();
*state.offset_mut() = 1;
assert_eq!(state.offset, 1);
}
#[test]
fn selected() {
let state = TableState::new();
assert_eq!(state.selected(), None);
}
#[test]
fn selected_mut() {
let mut state = TableState::new();
*state.selected_mut() = Some(1);
assert_eq!(state.selected, Some(1));
}
#[test]
fn select() {
let mut state = TableState::new();
state.select(Some(1));
assert_eq!(state.selected, Some(1));
}
#[test]
fn select_none() {
let mut state = TableState::new().with_selected(Some(1));
state.select(None);
assert_eq!(state.selected, None);
}
}