mirror of
https://github.com/ratatui-org/ratatui
synced 2024-11-21 20:23:11 +00:00
feat(table)!: add support for selecting column and cell (#1331)
Fixes https://github.com/ratatui-org/ratatui/issues/1250 Adds support for selecting a column and cell in `TableState`. The selected column, and cells style can be set by `Table::column_highlight_style` and `Table::cell_highlight_style` respectively. The table example has also been updated to display the new functionality: https://github.com/user-attachments/assets/e5fd2858-4931-4ce1-a2f6-a5ea1eacbecc BREAKING CHANGE: The Serialized output of the state will now include the "selected_column" field. Software that manually parse the serialized the output (with anything other than the `Serialize` implementation on `TableState`) may have to be refactored if the "selected_column" field is not accounted for. This does not affect users who rely on the `Deserialize`, or `Serialize` implementation on the state. BREAKING CHANGE: The `Table::highlight_style` is now deprecated in favor of `Table::row_highlight_style`. --------- Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
This commit is contained in:
parent
23c0d52c29
commit
dc8d0587ec
9 changed files with 847 additions and 98 deletions
|
@ -10,6 +10,9 @@ GitHub with a [breaking change] label.
|
|||
|
||||
This is a quick summary of the sections below:
|
||||
|
||||
- [v0.29.0](#v0290)
|
||||
- `Table::highlight_style` is now `Table::row_highlight_style`
|
||||
|
||||
- [v0.28.0](#v0280)
|
||||
⁻ `Backend::size` returns `Size` instead of `Rect`
|
||||
- `Backend` trait migrates to `get/set_cursor_position`
|
||||
|
@ -65,6 +68,19 @@ This is a quick summary of the sections below:
|
|||
- MSRV is now 1.63.0
|
||||
- `List` no longer ignores empty strings
|
||||
|
||||
## v0.29.0
|
||||
|
||||
### `Table::highlight_style` is now `Table::row_highlight_style` ([#1331])
|
||||
|
||||
[#1331]: https://github.com/ratatui/ratatui/pull/1331
|
||||
|
||||
The `Table::highlight_style` is now deprecated in favor of `Table::row_highlight_style`.
|
||||
|
||||
Also, the serialized output of the `TableState` will now include the "selected_column" field.
|
||||
Software that manually parse the serialized the output (with anything other than the `Serialize`
|
||||
implementation on `TableState`) may have to be refactored if the "selected_column" field is not accounted for.
|
||||
This does not affect users who rely on the `Deserialize`, or `Serialize` implementation on the state.
|
||||
|
||||
## v0.28.0
|
||||
|
||||
### `Backend::size` returns `Size` instead of `Rect` ([#1254])
|
||||
|
|
|
@ -244,7 +244,7 @@ impl Widget for &PullRequestListWidget {
|
|||
.block(block)
|
||||
.highlight_spacing(HighlightSpacing::Always)
|
||||
.highlight_symbol(">>")
|
||||
.highlight_style(Style::new().on_blue());
|
||||
.row_highlight_style(Style::new().on_blue());
|
||||
|
||||
StatefulWidget::render(table, area, buf, &mut state.table_state);
|
||||
}
|
||||
|
|
|
@ -164,7 +164,7 @@ fn render_ingredients(selected_row: usize, area: Rect, buf: &mut Buffer) {
|
|||
Table::new(rows, [Constraint::Length(7), Constraint::Length(30)])
|
||||
.block(Block::new().style(theme.ingredients))
|
||||
.header(Row::new(vec!["Qty", "Ingredient"]).style(theme.ingredients_header))
|
||||
.highlight_style(Style::new().light_yellow()),
|
||||
.row_highlight_style(Style::new().light_yellow()),
|
||||
area,
|
||||
buf,
|
||||
&mut state,
|
||||
|
|
|
@ -60,7 +60,7 @@ fn render_hops(selected_row: usize, area: Rect, buf: &mut Buffer) {
|
|||
StatefulWidget::render(
|
||||
Table::new(rows, [Constraint::Max(100), Constraint::Length(15)])
|
||||
.header(Row::new(vec!["Host", "Address"]).set_style(THEME.traceroute.header))
|
||||
.highlight_style(THEME.traceroute.selected)
|
||||
.row_highlight_style(THEME.traceroute.selected)
|
||||
.block(block),
|
||||
area,
|
||||
buf,
|
||||
|
|
|
@ -14,12 +14,13 @@
|
|||
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
||||
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::KeyModifiers;
|
||||
use itertools::Itertools;
|
||||
use ratatui::{
|
||||
crossterm::event::{self, Event, KeyCode, KeyEventKind},
|
||||
layout::{Constraint, Layout, Margin, Rect},
|
||||
style::{self, Color, Modifier, Style, Stylize},
|
||||
text::{Line, Text},
|
||||
text::Text,
|
||||
widgets::{
|
||||
Block, BorderType, Cell, HighlightSpacing, Paragraph, Row, Scrollbar, ScrollbarOrientation,
|
||||
ScrollbarState, Table, TableState,
|
||||
|
@ -35,8 +36,10 @@ const PALETTES: [tailwind::Palette; 4] = [
|
|||
tailwind::INDIGO,
|
||||
tailwind::RED,
|
||||
];
|
||||
const INFO_TEXT: &str =
|
||||
"(Esc) quit | (↑) move up | (↓) move down | (→) next color | (←) previous color";
|
||||
const INFO_TEXT: [&str; 2] = [
|
||||
"(Esc) quit | (↑) move up | (↓) move down | (←) move left | (→) move right",
|
||||
"(Shift + →) next color | (Shift + ←) previous color",
|
||||
];
|
||||
|
||||
const ITEM_HEIGHT: usize = 4;
|
||||
|
||||
|
@ -52,7 +55,9 @@ struct TableColors {
|
|||
header_bg: Color,
|
||||
header_fg: Color,
|
||||
row_fg: Color,
|
||||
selected_style_fg: Color,
|
||||
selected_row_style_fg: Color,
|
||||
selected_column_style_fg: Color,
|
||||
selected_cell_style_fg: Color,
|
||||
normal_row_color: Color,
|
||||
alt_row_color: Color,
|
||||
footer_border_color: Color,
|
||||
|
@ -65,7 +70,9 @@ impl TableColors {
|
|||
header_bg: color.c900,
|
||||
header_fg: tailwind::SLATE.c200,
|
||||
row_fg: tailwind::SLATE.c200,
|
||||
selected_style_fg: color.c400,
|
||||
selected_row_style_fg: color.c400,
|
||||
selected_column_style_fg: color.c400,
|
||||
selected_cell_style_fg: color.c600,
|
||||
normal_row_color: tailwind::SLATE.c950,
|
||||
alt_row_color: tailwind::SLATE.c900,
|
||||
footer_border_color: color.c400,
|
||||
|
@ -118,8 +125,7 @@ impl App {
|
|||
items: data_vec,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next(&mut self) {
|
||||
pub fn next_row(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i >= self.items.len() - 1 {
|
||||
|
@ -134,7 +140,7 @@ impl App {
|
|||
self.scroll_state = self.scroll_state.position(i * ITEM_HEIGHT);
|
||||
}
|
||||
|
||||
pub fn previous(&mut self) {
|
||||
pub fn previous_row(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i == 0 {
|
||||
|
@ -149,6 +155,14 @@ impl App {
|
|||
self.scroll_state = self.scroll_state.position(i * ITEM_HEIGHT);
|
||||
}
|
||||
|
||||
pub fn next_column(&mut self) {
|
||||
self.state.select_next_column();
|
||||
}
|
||||
|
||||
pub fn previous_column(&mut self) {
|
||||
self.state.select_previous_column();
|
||||
}
|
||||
|
||||
pub fn next_color(&mut self) {
|
||||
self.color_index = (self.color_index + 1) % PALETTES.len();
|
||||
}
|
||||
|
@ -168,12 +182,17 @@ impl App {
|
|||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
let shift_pressed = key.modifiers.contains(KeyModifiers::SHIFT);
|
||||
match key.code {
|
||||
KeyCode::Char('q') | KeyCode::Esc => return Ok(()),
|
||||
KeyCode::Char('j') | KeyCode::Down => self.next(),
|
||||
KeyCode::Char('k') | KeyCode::Up => self.previous(),
|
||||
KeyCode::Char('l') | KeyCode::Right => self.next_color(),
|
||||
KeyCode::Char('h') | KeyCode::Left => self.previous_color(),
|
||||
KeyCode::Char('j') | KeyCode::Down => self.next_row(),
|
||||
KeyCode::Char('k') | KeyCode::Up => self.previous_row(),
|
||||
KeyCode::Char('l') | KeyCode::Right if shift_pressed => self.next_color(),
|
||||
KeyCode::Char('h') | KeyCode::Left if shift_pressed => {
|
||||
self.previous_color();
|
||||
}
|
||||
KeyCode::Char('l') | KeyCode::Right => self.next_column(),
|
||||
KeyCode::Char('h') | KeyCode::Left => self.previous_column(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
@ -182,7 +201,7 @@ impl App {
|
|||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame) {
|
||||
let vertical = &Layout::vertical([Constraint::Min(5), Constraint::Length(3)]);
|
||||
let vertical = &Layout::vertical([Constraint::Min(5), Constraint::Length(4)]);
|
||||
let rects = vertical.split(frame.area());
|
||||
|
||||
self.set_colors();
|
||||
|
@ -196,9 +215,13 @@ impl App {
|
|||
let header_style = Style::default()
|
||||
.fg(self.colors.header_fg)
|
||||
.bg(self.colors.header_bg);
|
||||
let selected_style = Style::default()
|
||||
let selected_row_style = Style::default()
|
||||
.add_modifier(Modifier::REVERSED)
|
||||
.fg(self.colors.selected_style_fg);
|
||||
.fg(self.colors.selected_row_style_fg);
|
||||
let selected_col_style = Style::default().fg(self.colors.selected_column_style_fg);
|
||||
let selected_cell_style = Style::default()
|
||||
.add_modifier(Modifier::REVERSED)
|
||||
.fg(self.colors.selected_cell_style_fg);
|
||||
|
||||
let header = ["Name", "Address", "Email"]
|
||||
.into_iter()
|
||||
|
@ -229,7 +252,9 @@ impl App {
|
|||
],
|
||||
)
|
||||
.header(header)
|
||||
.highlight_style(selected_style)
|
||||
.row_highlight_style(selected_row_style)
|
||||
.column_highlight_style(selected_col_style)
|
||||
.cell_highlight_style(selected_cell_style)
|
||||
.highlight_symbol(Text::from(vec![
|
||||
"".into(),
|
||||
bar.into(),
|
||||
|
@ -256,7 +281,7 @@ impl App {
|
|||
}
|
||||
|
||||
fn render_footer(&self, frame: &mut Frame, area: Rect) {
|
||||
let info_footer = Paragraph::new(Line::from(INFO_TEXT))
|
||||
let info_footer = Paragraph::new(Text::from_iter(INFO_TEXT))
|
||||
.style(
|
||||
Style::new()
|
||||
.fg(self.colors.row_fg)
|
||||
|
|
|
@ -30,10 +30,11 @@ use crate::{
|
|||
///
|
||||
/// [`Table`] is also a [`StatefulWidget`], which means you can use it with [`TableState`] to allow
|
||||
/// the user to scroll through the rows and select one of them. When rendering a [`Table`] with a
|
||||
/// [`TableState`], the selected row will be highlighted. If the selected row is not visible (based
|
||||
/// on the offset), the table will be scrolled to make the selected row visible.
|
||||
/// [`TableState`], the selected row, column and cell will be highlighted. If the selected row is
|
||||
/// not visible (based on the offset), the table will be scrolled to make the selected row visible.
|
||||
///
|
||||
/// Note: if the `widths` field is empty, the table will be rendered with equal widths.
|
||||
/// Note: Highlight styles are applied in the following order: Row, Column, Cell.
|
||||
///
|
||||
/// See the table example and the recipe and traceroute tabs in the demo2 example in the [Examples]
|
||||
/// directory for a more in depth example of the various configuration options and for how to handle
|
||||
|
@ -57,7 +58,9 @@ use crate::{
|
|||
/// - [`Table::column_spacing`] sets the spacing between each column.
|
||||
/// - [`Table::block`] wraps the table in a [`Block`] widget.
|
||||
/// - [`Table::style`] sets the base style of the widget.
|
||||
/// - [`Table::highlight_style`] sets the style of the selected row.
|
||||
/// - [`Table::row_highlight_style`] sets the style of the selected row.
|
||||
/// - [`Table::column_highlight_style`] sets the style of the selected column.
|
||||
/// - [`Table::cell_highlight_style`] sets the style of the selected cell.
|
||||
/// - [`Table::highlight_symbol`] sets the symbol to be displayed in front of the selected row.
|
||||
/// - [`Table::highlight_spacing`] sets when to show the highlight spacing.
|
||||
///
|
||||
|
@ -93,8 +96,10 @@ use crate::{
|
|||
/// .footer(Row::new(vec!["Updated on Dec 28"]))
|
||||
/// // As any other widget, a Table can be wrapped in a Block.
|
||||
/// .block(Block::new().title("Table"))
|
||||
/// // The selected row and its content can also be styled.
|
||||
/// .highlight_style(Style::new().reversed())
|
||||
/// // The selected row, column, cell and its content can also be styled.
|
||||
/// .row_highlight_style(Style::new().reversed())
|
||||
/// .column_highlight_style(Style::new().red())
|
||||
/// .cell_highlight_style(Style::new().blue())
|
||||
/// // ...and potentially show a symbol in front of the selection.
|
||||
/// .highlight_symbol(">>");
|
||||
/// ```
|
||||
|
@ -220,7 +225,7 @@ use crate::{
|
|||
/// ];
|
||||
/// let table = Table::new(rows, widths)
|
||||
/// .block(Block::new().title("Table"))
|
||||
/// .highlight_style(Style::new().reversed())
|
||||
/// .row_highlight_style(Style::new().reversed())
|
||||
/// .highlight_symbol(">>");
|
||||
///
|
||||
/// frame.render_stateful_widget(table, area, &mut table_state);
|
||||
|
@ -253,7 +258,13 @@ pub struct Table<'a> {
|
|||
style: Style,
|
||||
|
||||
/// Style used to render the selected row
|
||||
highlight_style: Style,
|
||||
row_highlight_style: Style,
|
||||
|
||||
/// Style used to render the selected column
|
||||
column_highlight_style: Style,
|
||||
|
||||
/// Style used to render the selected cell
|
||||
cell_highlight_style: Style,
|
||||
|
||||
/// Symbol in front of the selected row
|
||||
highlight_symbol: Text<'a>,
|
||||
|
@ -275,7 +286,9 @@ impl<'a> Default for Table<'a> {
|
|||
column_spacing: 1,
|
||||
block: None,
|
||||
style: Style::new(),
|
||||
highlight_style: Style::new(),
|
||||
row_highlight_style: Style::new(),
|
||||
column_highlight_style: Style::new(),
|
||||
cell_highlight_style: Style::new(),
|
||||
highlight_symbol: Text::default(),
|
||||
highlight_spacing: HighlightSpacing::default(),
|
||||
flex: Flex::Start,
|
||||
|
@ -564,8 +577,83 @@ impl<'a> Table<'a> {
|
|||
///
|
||||
/// [`Color`]: crate::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn highlight_style<S: Into<Style>>(mut self, highlight_style: S) -> Self {
|
||||
self.highlight_style = highlight_style.into();
|
||||
#[deprecated(note = "use `Table::row_highlight_style` instead")]
|
||||
pub fn highlight_style<S: Into<Style>>(self, highlight_style: S) -> Self {
|
||||
self.row_highlight_style(highlight_style)
|
||||
}
|
||||
|
||||
/// Set the style of the selected row
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// This style will be applied to the entire row, including the selection symbol if it is
|
||||
/// displayed, and will override any style set on the row or on the individual cells.
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// # let rows = [Row::new(vec!["Cell1", "Cell2"])];
|
||||
/// # let widths = [Constraint::Length(5), Constraint::Length(5)];
|
||||
/// let table = Table::new(rows, widths).row_highlight_style(Style::new().red().italic());
|
||||
/// ```
|
||||
/// [`Color`]: crate::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn row_highlight_style<S: Into<Style>>(mut self, highlight_style: S) -> Self {
|
||||
self.row_highlight_style = highlight_style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the style of the selected column
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// This style will be applied to the entire column, and will override any style set on the
|
||||
/// row or on the individual cells.
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// # let rows = [Row::new(vec!["Cell1", "Cell2"])];
|
||||
/// # let widths = [Constraint::Length(5), Constraint::Length(5)];
|
||||
/// let table = Table::new(rows, widths).column_highlight_style(Style::new().red().italic());
|
||||
/// ```
|
||||
/// [`Color`]: crate::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn column_highlight_style<S: Into<Style>>(mut self, highlight_style: S) -> Self {
|
||||
self.column_highlight_style = highlight_style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the style of the selected cell
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// This style will be applied to the selected cell, and will override any style set on the
|
||||
/// row or on the individual cells.
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// # let rows = [Row::new(vec!["Cell1", "Cell2"])];
|
||||
/// # let widths = [Constraint::Length(5), Constraint::Length(5)];
|
||||
/// let table = Table::new(rows, widths).cell_highlight_style(Style::new().red().italic());
|
||||
/// ```
|
||||
/// [`Color`]: crate::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn cell_highlight_style<S: Into<Style>>(mut self, highlight_style: S) -> Self {
|
||||
self.cell_highlight_style = highlight_style.into();
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -707,8 +795,17 @@ impl StatefulWidgetRef for Table<'_> {
|
|||
state.select(None);
|
||||
}
|
||||
|
||||
let column_count = self.column_count();
|
||||
if state.selected_column.is_some_and(|s| s >= column_count) {
|
||||
state.select_column(Some(column_count.saturating_sub(1)));
|
||||
}
|
||||
if column_count == 0 {
|
||||
state.select_column(None);
|
||||
}
|
||||
|
||||
let selection_width = self.selection_width(state);
|
||||
let columns_widths = self.get_columns_widths(table_area.width, selection_width);
|
||||
let columns_widths =
|
||||
self.get_columns_widths(table_area.width, selection_width, column_count);
|
||||
let (header_area, rows_area, footer_area) = self.layout(table_area);
|
||||
|
||||
self.render_header(header_area, buf, &columns_widths);
|
||||
|
@ -786,6 +883,8 @@ impl Table<'_> {
|
|||
state.offset = start_index;
|
||||
|
||||
let mut y_offset = 0;
|
||||
|
||||
let mut selected_row_area = None;
|
||||
for (i, row) in self
|
||||
.rows
|
||||
.iter()
|
||||
|
@ -801,7 +900,7 @@ impl Table<'_> {
|
|||
);
|
||||
buf.set_style(row_area, row.style);
|
||||
|
||||
let is_selected = state.selected().is_some_and(|index| index == i);
|
||||
let is_selected = state.selected.is_some_and(|index| index == i);
|
||||
if selection_width > 0 && is_selected {
|
||||
let selection_area = Rect {
|
||||
width: selection_width,
|
||||
|
@ -817,26 +916,49 @@ impl Table<'_> {
|
|||
);
|
||||
}
|
||||
if is_selected {
|
||||
buf.set_style(row_area, self.highlight_style);
|
||||
selected_row_area = Some(row_area);
|
||||
}
|
||||
y_offset += row.height_with_margin();
|
||||
}
|
||||
|
||||
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,
|
||||
..area
|
||||
})
|
||||
});
|
||||
|
||||
match (selected_row_area, selected_column_area) {
|
||||
(Some(row_area), Some(col_area)) => {
|
||||
buf.set_style(row_area, self.row_highlight_style);
|
||||
buf.set_style(col_area, self.column_highlight_style);
|
||||
let cell_area = row_area.intersection(col_area);
|
||||
buf.set_style(cell_area, self.cell_highlight_style);
|
||||
}
|
||||
(Some(row_area), None) => {
|
||||
buf.set_style(row_area, self.row_highlight_style);
|
||||
}
|
||||
(None, Some(col_area)) => {
|
||||
buf.set_style(col_area, self.column_highlight_style);
|
||||
}
|
||||
(None, None) => (),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all offsets and widths of all user specified columns.
|
||||
///
|
||||
/// Returns (x, width). When self.widths is empty, it is assumed `.widths()` has not been called
|
||||
/// and a default of equal widths is returned.
|
||||
fn get_columns_widths(&self, max_width: u16, selection_width: u16) -> Vec<(u16, u16)> {
|
||||
fn get_columns_widths(
|
||||
&self,
|
||||
max_width: u16,
|
||||
selection_width: u16,
|
||||
col_count: usize,
|
||||
) -> Vec<(u16, u16)> {
|
||||
let widths = if self.widths.is_empty() {
|
||||
let col_count = self
|
||||
.rows
|
||||
.iter()
|
||||
.chain(self.header.iter())
|
||||
.chain(self.footer.iter())
|
||||
.map(|r| r.cells.len())
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
// Divide the space between each column equally
|
||||
vec![Constraint::Length(max_width / col_count.max(1) as u16); col_count]
|
||||
} else {
|
||||
|
@ -900,10 +1022,20 @@ impl Table<'_> {
|
|||
(start, end)
|
||||
}
|
||||
|
||||
fn column_count(&self) -> usize {
|
||||
self.rows
|
||||
.iter()
|
||||
.chain(self.footer.iter())
|
||||
.chain(self.header.iter())
|
||||
.map(|r| r.cells.len())
|
||||
.max()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Returns the width of the selection column if a row is selected, or the `highlight_spacing`
|
||||
/// is set to show the column always, otherwise 0.
|
||||
fn selection_width(&self, state: &TableState) -> u16 {
|
||||
let has_selection = state.selected().is_some();
|
||||
let has_selection = state.selected.is_some();
|
||||
if self.highlight_spacing.should_add(has_selection) {
|
||||
self.highlight_symbol.width() as u16
|
||||
} else {
|
||||
|
@ -953,6 +1085,8 @@ where
|
|||
mod tests {
|
||||
use std::vec;
|
||||
|
||||
use rstest::{fixture, rstest};
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
layout::Constraint::*,
|
||||
|
@ -973,7 +1107,7 @@ mod tests {
|
|||
assert_eq!(table.column_spacing, 1);
|
||||
assert_eq!(table.block, None);
|
||||
assert_eq!(table.style, Style::default());
|
||||
assert_eq!(table.highlight_style, Style::default());
|
||||
assert_eq!(table.row_highlight_style, Style::default());
|
||||
assert_eq!(table.highlight_symbol, Text::default());
|
||||
assert_eq!(table.highlight_spacing, HighlightSpacing::WhenSelected);
|
||||
assert_eq!(table.flex, Flex::Start);
|
||||
|
@ -989,7 +1123,7 @@ mod tests {
|
|||
assert_eq!(table.column_spacing, 1);
|
||||
assert_eq!(table.block, None);
|
||||
assert_eq!(table.style, Style::default());
|
||||
assert_eq!(table.highlight_style, Style::default());
|
||||
assert_eq!(table.row_highlight_style, Style::default());
|
||||
assert_eq!(table.highlight_symbol, Text::default());
|
||||
assert_eq!(table.highlight_spacing, HighlightSpacing::WhenSelected);
|
||||
assert_eq!(table.flex, Flex::Start);
|
||||
|
@ -1072,10 +1206,32 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
#[allow(deprecated)]
|
||||
fn highlight_style() {
|
||||
let style = Style::default().red().italic();
|
||||
let table = Table::default().highlight_style(style);
|
||||
assert_eq!(table.highlight_style, style);
|
||||
assert_eq!(table.row_highlight_style, style);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn row_highlight_style() {
|
||||
let style = Style::default().red().italic();
|
||||
let table = Table::default().row_highlight_style(style);
|
||||
assert_eq!(table.row_highlight_style, style);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn column_highlight_style() {
|
||||
let style = Style::default().red().italic();
|
||||
let table = Table::default().column_highlight_style(style);
|
||||
assert_eq!(table.column_highlight_style, style);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cell_highlight_style() {
|
||||
let style = Style::default().red().italic();
|
||||
let table = Table::default().cell_highlight_style(style);
|
||||
assert_eq!(table.cell_highlight_style, style);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -1122,8 +1278,8 @@ mod tests {
|
|||
|
||||
#[cfg(test)]
|
||||
mod state {
|
||||
use rstest::{fixture, rstest};
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::{Constraint, Rect},
|
||||
|
@ -1145,6 +1301,7 @@ mod tests {
|
|||
state.select_first();
|
||||
StatefulWidget::render(table, table_buf.area, &mut table_buf, &mut state);
|
||||
assert_eq!(state.selected, None);
|
||||
assert_eq!(state.selected_column, None);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
|
@ -1158,25 +1315,49 @@ mod tests {
|
|||
state.select_first();
|
||||
StatefulWidget::render(&table, table_buf.area, &mut table_buf, &mut state);
|
||||
assert_eq!(state.selected, Some(0));
|
||||
assert_eq!(state.selected_column, None);
|
||||
|
||||
state.select_last();
|
||||
StatefulWidget::render(&table, table_buf.area, &mut table_buf, &mut state);
|
||||
assert_eq!(state.selected, Some(0));
|
||||
assert_eq!(state.selected_column, None);
|
||||
|
||||
state.select_previous();
|
||||
StatefulWidget::render(&table, table_buf.area, &mut table_buf, &mut state);
|
||||
assert_eq!(state.selected, Some(0));
|
||||
assert_eq!(state.selected_column, None);
|
||||
|
||||
state.select_next();
|
||||
StatefulWidget::render(&table, table_buf.area, &mut table_buf, &mut state);
|
||||
assert_eq!(state.selected, Some(0));
|
||||
assert_eq!(state.selected_column, None);
|
||||
|
||||
let mut state = TableState::default();
|
||||
|
||||
state.select_first_column();
|
||||
StatefulWidget::render(&table, table_buf.area, &mut table_buf, &mut state);
|
||||
assert_eq!(state.selected_column, Some(0));
|
||||
assert_eq!(state.selected, None);
|
||||
|
||||
state.select_last_column();
|
||||
StatefulWidget::render(&table, table_buf.area, &mut table_buf, &mut state);
|
||||
assert_eq!(state.selected_column, Some(0));
|
||||
assert_eq!(state.selected, None);
|
||||
|
||||
state.select_previous_column();
|
||||
StatefulWidget::render(&table, table_buf.area, &mut table_buf, &mut state);
|
||||
assert_eq!(state.selected_column, Some(0));
|
||||
assert_eq!(state.selected, None);
|
||||
|
||||
state.select_next_column();
|
||||
StatefulWidget::render(&table, table_buf.area, &mut table_buf, &mut state);
|
||||
assert_eq!(state.selected_column, Some(0));
|
||||
assert_eq!(state.selected, None);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod render {
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
use crate::layout::Alignment;
|
||||
|
||||
|
@ -1349,6 +1530,17 @@ mod tests {
|
|||
Widget::render(table, Rect::new(0, 0, 20, 3), &mut buf);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_with_selected_column_and_incorrect_width_count_does_not_panic() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 3));
|
||||
let table = Table::new(
|
||||
vec![Row::new(vec!["Row1", "Row2", "Row3"])],
|
||||
[Constraint::Length(10); 1],
|
||||
);
|
||||
let mut state = TableState::new().with_selected_column(2);
|
||||
StatefulWidget::render(table, Rect::new(0, 0, 20, 3), &mut buf, &mut state);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_with_selected() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
|
||||
|
@ -1357,9 +1549,9 @@ mod tests {
|
|||
Row::new(vec!["Cell3", "Cell4"]),
|
||||
];
|
||||
let table = Table::new(rows, [Constraint::Length(5); 2])
|
||||
.highlight_style(Style::new().red())
|
||||
.row_highlight_style(Style::new().red())
|
||||
.highlight_symbol(">>");
|
||||
let mut state = TableState::new().with_selected(0);
|
||||
let mut state = TableState::new().with_selected(Some(0));
|
||||
StatefulWidget::render(table, Rect::new(0, 0, 15, 3), &mut buf, &mut state);
|
||||
let expected = Buffer::with_lines([
|
||||
">>Cell1 Cell2 ".red(),
|
||||
|
@ -1369,6 +1561,105 @@ mod tests {
|
|||
assert_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_with_selected_column() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
|
||||
let rows = vec![
|
||||
Row::new(vec!["Cell1", "Cell2"]),
|
||||
Row::new(vec!["Cell3", "Cell4"]),
|
||||
];
|
||||
let table = Table::new(rows, [Constraint::Length(5); 2])
|
||||
.column_highlight_style(Style::new().blue())
|
||||
.highlight_symbol(">>");
|
||||
let mut state = TableState::new().with_selected_column(Some(1));
|
||||
StatefulWidget::render(table, Rect::new(0, 0, 15, 3), &mut buf, &mut state);
|
||||
let expected = Buffer::with_lines::<[Line; 3]>([
|
||||
Line::from(vec![
|
||||
"Cell1".into(),
|
||||
" ".into(),
|
||||
"Cell2".blue(),
|
||||
" ".into(),
|
||||
]),
|
||||
Line::from(vec![
|
||||
"Cell3".into(),
|
||||
" ".into(),
|
||||
"Cell4".blue(),
|
||||
" ".into(),
|
||||
]),
|
||||
Line::from(vec![" ".into(), " ".blue(), " ".into()]),
|
||||
]);
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_with_selected_cell() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 4));
|
||||
let rows = vec![
|
||||
Row::new(vec!["Cell1", "Cell2", "Cell3"]),
|
||||
Row::new(vec!["Cell4", "Cell5", "Cell6"]),
|
||||
Row::new(vec!["Cell7", "Cell8", "Cell9"]),
|
||||
];
|
||||
let table = Table::new(rows, [Constraint::Length(5); 3])
|
||||
.highlight_symbol(">>")
|
||||
.cell_highlight_style(Style::new().green());
|
||||
let mut state = TableState::new().with_selected_cell((1, 2));
|
||||
StatefulWidget::render(table, Rect::new(0, 0, 20, 4), &mut buf, &mut state);
|
||||
let expected = Buffer::with_lines::<[Line; 4]>([
|
||||
Line::from(vec![" Cell1 ".into(), "Cell2 ".into(), "Cell3".into()]),
|
||||
Line::from(vec![">>Cell4 Cell5 ".into(), "Cell6".green(), " ".into()]),
|
||||
Line::from(vec![" Cell7 ".into(), "Cell8 ".into(), "Cell9".into()]),
|
||||
Line::from(vec![" ".into()]),
|
||||
]);
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_with_selected_row_and_column() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 4));
|
||||
let rows = vec![
|
||||
Row::new(vec!["Cell1", "Cell2", "Cell3"]),
|
||||
Row::new(vec!["Cell4", "Cell5", "Cell6"]),
|
||||
Row::new(vec!["Cell7", "Cell8", "Cell9"]),
|
||||
];
|
||||
let table = Table::new(rows, [Constraint::Length(5); 3])
|
||||
.highlight_symbol(">>")
|
||||
.row_highlight_style(Style::new().red())
|
||||
.column_highlight_style(Style::new().blue());
|
||||
let mut state = TableState::new().with_selected(1).with_selected_column(2);
|
||||
StatefulWidget::render(table, Rect::new(0, 0, 20, 4), &mut buf, &mut state);
|
||||
let expected = Buffer::with_lines::<[Line; 4]>([
|
||||
Line::from(vec![" Cell1 ".into(), "Cell2 ".into(), "Cell3".blue()]),
|
||||
Line::from(vec![">>Cell4 Cell5 ".red(), "Cell6".blue(), " ".red()]),
|
||||
Line::from(vec![" Cell7 ".into(), "Cell8 ".into(), "Cell9".blue()]),
|
||||
Line::from(vec![" ".into(), " ".blue(), " ".into()]),
|
||||
]);
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_with_selected_row_and_column_and_cell() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 4));
|
||||
let rows = vec![
|
||||
Row::new(vec!["Cell1", "Cell2", "Cell3"]),
|
||||
Row::new(vec!["Cell4", "Cell5", "Cell6"]),
|
||||
Row::new(vec!["Cell7", "Cell8", "Cell9"]),
|
||||
];
|
||||
let table = Table::new(rows, [Constraint::Length(5); 3])
|
||||
.highlight_symbol(">>")
|
||||
.row_highlight_style(Style::new().red())
|
||||
.column_highlight_style(Style::new().blue())
|
||||
.cell_highlight_style(Style::new().green());
|
||||
let mut state = TableState::new().with_selected(1).with_selected_column(2);
|
||||
StatefulWidget::render(table, Rect::new(0, 0, 20, 4), &mut buf, &mut state);
|
||||
let expected = Buffer::with_lines::<[Line; 4]>([
|
||||
Line::from(vec![" Cell1 ".into(), "Cell2 ".into(), "Cell3".blue()]),
|
||||
Line::from(vec![">>Cell4 Cell5 ".red(), "Cell6".green(), " ".red()]),
|
||||
Line::from(vec![" Cell7 ".into(), "Cell8 ".into(), "Cell9".blue()]),
|
||||
Line::from(vec![" ".into(), " ".blue(), " ".into()]),
|
||||
]);
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
|
||||
/// Note that this includes a regression test for a bug where the table would not render the
|
||||
/// correct rows when there is no selection.
|
||||
/// <https://github.com/ratatui/ratatui/issues/1179>
|
||||
|
@ -1391,7 +1682,7 @@ mod tests {
|
|||
let mut buf = Buffer::empty(Rect::new(0, 0, 2, 5));
|
||||
let mut state = TableState::new()
|
||||
.with_offset(50)
|
||||
.with_selected(selected_row);
|
||||
.with_selected(selected_row.into());
|
||||
|
||||
StatefulWidget::render(table.clone(), Rect::new(0, 0, 5, 5), &mut buf, &mut state);
|
||||
|
||||
|
@ -1408,15 +1699,15 @@ mod tests {
|
|||
fn length_constraint() {
|
||||
// without selection, more than needed width
|
||||
let table = Table::default().widths([Length(4), Length(4)]);
|
||||
assert_eq!(table.get_columns_widths(20, 0), [(0, 4), (5, 4)]);
|
||||
assert_eq!(table.get_columns_widths(20, 0, 0), [(0, 4), (5, 4)]);
|
||||
|
||||
// with selection, more than needed width
|
||||
let table = Table::default().widths([Length(4), Length(4)]);
|
||||
assert_eq!(table.get_columns_widths(20, 3), [(3, 4), (8, 4)]);
|
||||
assert_eq!(table.get_columns_widths(20, 3, 0), [(3, 4), (8, 4)]);
|
||||
|
||||
// without selection, less than needed width
|
||||
let table = Table::default().widths([Length(4), Length(4)]);
|
||||
assert_eq!(table.get_columns_widths(7, 0), [(0, 3), (4, 3)]);
|
||||
assert_eq!(table.get_columns_widths(7, 0, 0), [(0, 3), (4, 3)]);
|
||||
|
||||
// with selection, less than needed width
|
||||
// <--------7px-------->
|
||||
|
@ -1425,26 +1716,26 @@ mod tests {
|
|||
// └────────┘x└────────┘
|
||||
// column spacing (i.e. `x`) is always prioritized
|
||||
let table = Table::default().widths([Length(4), Length(4)]);
|
||||
assert_eq!(table.get_columns_widths(7, 3), [(3, 2), (6, 1)]);
|
||||
assert_eq!(table.get_columns_widths(7, 3, 0), [(3, 2), (6, 1)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_constraint() {
|
||||
// without selection, more than needed width
|
||||
let table = Table::default().widths([Max(4), Max(4)]);
|
||||
assert_eq!(table.get_columns_widths(20, 0), [(0, 4), (5, 4)]);
|
||||
assert_eq!(table.get_columns_widths(20, 0, 0), [(0, 4), (5, 4)]);
|
||||
|
||||
// with selection, more than needed width
|
||||
let table = Table::default().widths([Max(4), Max(4)]);
|
||||
assert_eq!(table.get_columns_widths(20, 3), [(3, 4), (8, 4)]);
|
||||
assert_eq!(table.get_columns_widths(20, 3, 0), [(3, 4), (8, 4)]);
|
||||
|
||||
// without selection, less than needed width
|
||||
let table = Table::default().widths([Max(4), Max(4)]);
|
||||
assert_eq!(table.get_columns_widths(7, 0), [(0, 3), (4, 3)]);
|
||||
assert_eq!(table.get_columns_widths(7, 0, 0), [(0, 3), (4, 3)]);
|
||||
|
||||
// with selection, less than needed width
|
||||
let table = Table::default().widths([Max(4), Max(4)]);
|
||||
assert_eq!(table.get_columns_widths(7, 3), [(3, 2), (6, 1)]);
|
||||
assert_eq!(table.get_columns_widths(7, 3, 0), [(3, 2), (6, 1)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -1455,42 +1746,42 @@ mod tests {
|
|||
|
||||
// without selection, more than needed width
|
||||
let table = Table::default().widths([Min(4), Min(4)]);
|
||||
assert_eq!(table.get_columns_widths(20, 0), [(0, 10), (11, 9)]);
|
||||
assert_eq!(table.get_columns_widths(20, 0, 0), [(0, 10), (11, 9)]);
|
||||
|
||||
// with selection, more than needed width
|
||||
let table = Table::default().widths([Min(4), Min(4)]);
|
||||
assert_eq!(table.get_columns_widths(20, 3), [(3, 8), (12, 8)]);
|
||||
assert_eq!(table.get_columns_widths(20, 3, 0), [(3, 8), (12, 8)]);
|
||||
|
||||
// without selection, less than needed width
|
||||
// allocates spacer
|
||||
let table = Table::default().widths([Min(4), Min(4)]);
|
||||
assert_eq!(table.get_columns_widths(7, 0), [(0, 3), (4, 3)]);
|
||||
assert_eq!(table.get_columns_widths(7, 0, 0), [(0, 3), (4, 3)]);
|
||||
|
||||
// with selection, less than needed width
|
||||
// always allocates selection and spacer
|
||||
let table = Table::default().widths([Min(4), Min(4)]);
|
||||
assert_eq!(table.get_columns_widths(7, 3), [(3, 2), (6, 1)]);
|
||||
assert_eq!(table.get_columns_widths(7, 3, 0), [(3, 2), (6, 1)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn percentage_constraint() {
|
||||
// without selection, more than needed width
|
||||
let table = Table::default().widths([Percentage(30), Percentage(30)]);
|
||||
assert_eq!(table.get_columns_widths(20, 0), [(0, 6), (7, 6)]);
|
||||
assert_eq!(table.get_columns_widths(20, 0, 0), [(0, 6), (7, 6)]);
|
||||
|
||||
// with selection, more than needed width
|
||||
let table = Table::default().widths([Percentage(30), Percentage(30)]);
|
||||
assert_eq!(table.get_columns_widths(20, 3), [(3, 5), (9, 5)]);
|
||||
assert_eq!(table.get_columns_widths(20, 3, 0), [(3, 5), (9, 5)]);
|
||||
|
||||
// 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_columns_widths(7, 0), [(0, 2), (3, 2)]);
|
||||
assert_eq!(table.get_columns_widths(7, 0, 0), [(0, 2), (3, 2)]);
|
||||
|
||||
// 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_columns_widths(7, 3), [(3, 1), (5, 1)]);
|
||||
assert_eq!(table.get_columns_widths(7, 3, 0), [(3, 1), (5, 1)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -1498,22 +1789,22 @@ 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_columns_widths(20, 0), [(0, 7), (8, 6)]);
|
||||
assert_eq!(table.get_columns_widths(20, 0, 0), [(0, 7), (8, 6)]);
|
||||
|
||||
// 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_columns_widths(20, 3), [(3, 6), (10, 5)]);
|
||||
assert_eq!(table.get_columns_widths(20, 3, 0), [(3, 6), (10, 5)]);
|
||||
|
||||
// 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_columns_widths(7, 0), [(0, 2), (3, 3)]);
|
||||
assert_eq!(table.get_columns_widths(7, 0, 0), [(0, 2), (3, 3)]);
|
||||
|
||||
// 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_columns_widths(7, 3), [(3, 1), (5, 2)]);
|
||||
assert_eq!(table.get_columns_widths(7, 3, 0), [(3, 1), (5, 2)]);
|
||||
}
|
||||
|
||||
/// When more width is available than requested, the behavior is controlled by flex
|
||||
|
@ -1521,7 +1812,7 @@ mod tests {
|
|||
fn underconstrained_flex() {
|
||||
let table = Table::default().widths([Min(10), Min(10), Min(1)]);
|
||||
assert_eq!(
|
||||
table.get_columns_widths(62, 0),
|
||||
table.get_columns_widths(62, 0, 0),
|
||||
&[(0, 20), (21, 20), (42, 20)]
|
||||
);
|
||||
|
||||
|
@ -1529,7 +1820,7 @@ mod tests {
|
|||
.widths([Min(10), Min(10), Min(1)])
|
||||
.flex(Flex::Legacy);
|
||||
assert_eq!(
|
||||
table.get_columns_widths(62, 0),
|
||||
table.get_columns_widths(62, 0, 0),
|
||||
&[(0, 10), (11, 10), (22, 40)]
|
||||
);
|
||||
|
||||
|
@ -1537,7 +1828,7 @@ mod tests {
|
|||
.widths([Min(10), Min(10), Min(1)])
|
||||
.flex(Flex::SpaceBetween);
|
||||
assert_eq!(
|
||||
table.get_columns_widths(62, 0),
|
||||
table.get_columns_widths(62, 0, 0),
|
||||
&[(0, 20), (21, 20), (42, 20)]
|
||||
);
|
||||
}
|
||||
|
@ -1548,7 +1839,7 @@ mod tests {
|
|||
fn underconstrained_segment_size() {
|
||||
let table = Table::default().widths([Min(10), Min(10), Min(1)]);
|
||||
assert_eq!(
|
||||
table.get_columns_widths(62, 0),
|
||||
table.get_columns_widths(62, 0, 0),
|
||||
&[(0, 20), (21, 20), (42, 20)]
|
||||
);
|
||||
|
||||
|
@ -1556,7 +1847,7 @@ mod tests {
|
|||
.widths([Min(10), Min(10), Min(1)])
|
||||
.flex(Flex::Legacy);
|
||||
assert_eq!(
|
||||
table.get_columns_widths(62, 0),
|
||||
table.get_columns_widths(62, 0, 0),
|
||||
&[(0, 10), (11, 10), (22, 40)]
|
||||
);
|
||||
}
|
||||
|
@ -1573,7 +1864,7 @@ mod tests {
|
|||
.footer(Row::new(vec!["h", "i"]))
|
||||
.column_spacing(0);
|
||||
assert_eq!(
|
||||
table.get_columns_widths(30, 0),
|
||||
table.get_columns_widths(30, 0, 3),
|
||||
&[(0, 10), (10, 10), (20, 10)]
|
||||
);
|
||||
}
|
||||
|
@ -1584,7 +1875,7 @@ mod tests {
|
|||
.rows(vec![])
|
||||
.header(Row::new(vec!["f", "g"]))
|
||||
.column_spacing(0);
|
||||
assert_eq!(table.get_columns_widths(10, 0), [(0, 5), (5, 5)]);
|
||||
assert_eq!(table.get_columns_widths(10, 0, 2), [(0, 5), (5, 5)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -1593,7 +1884,7 @@ mod tests {
|
|||
.rows(vec![])
|
||||
.footer(Row::new(vec!["h", "i"]))
|
||||
.column_spacing(0);
|
||||
assert_eq!(table.get_columns_widths(10, 0), [(0, 5), (5, 5)]);
|
||||
assert_eq!(table.get_columns_widths(10, 0, 2), [(0, 5), (5, 5)]);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
|
@ -1962,4 +2253,49 @@ mod tests {
|
|||
.remove_modifier(Modifier::CROSSED_OUT)
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::no_columns(vec![], vec![], vec![], 0)]
|
||||
#[case::only_header(vec!["H1", "H2"], vec![], vec![], 2)]
|
||||
#[case::only_rows(
|
||||
vec![],
|
||||
vec![vec!["C1", "C2"], vec!["C1", "C2", "C3"]],
|
||||
vec![],
|
||||
3
|
||||
)]
|
||||
#[case::only_footer(vec![], vec![], vec!["F1", "F2", "F3", "F4"], 4)]
|
||||
#[case::rows_longer(
|
||||
vec!["H1", "H2", "H3", "H4"],
|
||||
vec![vec!["C1", "C2"],vec!["C1", "C2", "C3"]],
|
||||
vec!["F1", "F2"],
|
||||
4
|
||||
)]
|
||||
#[case::rows_longer(
|
||||
vec!["H1", "H2"],
|
||||
vec![vec!["C1", "C2"], vec!["C1", "C2", "C3", "C4"]],
|
||||
vec!["F1", "F2"],
|
||||
4
|
||||
)]
|
||||
#[case::footer_longer(
|
||||
vec!["H1", "H2"],
|
||||
vec![vec!["C1", "C2"], vec!["C1", "C2", "C3"]],
|
||||
vec!["F1", "F2", "F3", "F4"],
|
||||
4
|
||||
)]
|
||||
|
||||
fn column_count(
|
||||
#[case] header: Vec<&str>,
|
||||
#[case] rows: Vec<Vec<&str>>,
|
||||
#[case] footer: Vec<&str>,
|
||||
#[case] expected: usize,
|
||||
) {
|
||||
let header = Row::new(header);
|
||||
let footer = Row::new(footer);
|
||||
let rows: Vec<Row> = rows.into_iter().map(Row::new).collect();
|
||||
let table = Table::new(rows, Vec::<Constraint>::new())
|
||||
.header(header)
|
||||
.footer(footer);
|
||||
let column_count = table.column_count();
|
||||
assert_eq!(column_count, expected);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,19 @@
|
|||
/// 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.
|
||||
/// rendered as a stateful widget, the selected row, column and cell 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
|
||||
/// - [`selected_column`]: the index of the selected column, which can be `None` if no column is
|
||||
/// selected
|
||||
///
|
||||
/// [`offset`]: TableState::offset()
|
||||
/// [`selected`]: TableState::selected()
|
||||
/// [`selected_column`]: TableState::selected_column()
|
||||
///
|
||||
/// See the `table` example and the `recipe` and `traceroute` tabs in the demo2 example in the
|
||||
/// [Examples] directory for a more in depth example of the various configuration options and for
|
||||
|
@ -38,6 +41,7 @@
|
|||
/// 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)
|
||||
/// table_state.select_column(Some(2)); // select the third column (0-indexed)
|
||||
///
|
||||
/// frame.render_stateful_widget(table, area, &mut table_state);
|
||||
/// # }
|
||||
|
@ -54,6 +58,7 @@
|
|||
pub struct TableState {
|
||||
pub(crate) offset: usize,
|
||||
pub(crate) selected: Option<usize>,
|
||||
pub(crate) selected_column: Option<usize>,
|
||||
}
|
||||
|
||||
impl TableState {
|
||||
|
@ -70,6 +75,7 @@ impl TableState {
|
|||
Self {
|
||||
offset: 0,
|
||||
selected: None,
|
||||
selected_column: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -110,6 +116,51 @@ impl TableState {
|
|||
self
|
||||
}
|
||||
|
||||
/// Sets the index of the selected column
|
||||
///
|
||||
/// 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_column(Some(1));
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn with_selected_column<T>(mut self, selected: T) -> Self
|
||||
where
|
||||
T: Into<Option<usize>>,
|
||||
{
|
||||
self.selected_column = selected.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the indexes of the selected cell
|
||||
///
|
||||
/// 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_cell(Some((1, 5)));
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn with_selected_cell<T>(mut self, selected: T) -> Self
|
||||
where
|
||||
T: Into<Option<(usize, usize)>>,
|
||||
{
|
||||
if let Some((r, c)) = selected.into() {
|
||||
self.selected = Some(r);
|
||||
self.selected_column = Some(c);
|
||||
} else {
|
||||
self.selected = None;
|
||||
self.selected_column = None;
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Index of the first row to be displayed
|
||||
///
|
||||
/// # Examples
|
||||
|
@ -154,6 +205,39 @@ impl TableState {
|
|||
self.selected
|
||||
}
|
||||
|
||||
/// Index of the selected column
|
||||
///
|
||||
/// Returns `None` if no column is selected
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let state = TableState::new();
|
||||
/// assert_eq!(state.selected_column(), None);
|
||||
/// ```
|
||||
pub const fn selected_column(&self) -> Option<usize> {
|
||||
self.selected_column
|
||||
}
|
||||
|
||||
/// Indexes of the selected cell
|
||||
///
|
||||
/// Returns `None` if no cell is selected
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let state = TableState::new();
|
||||
/// assert_eq!(state.selected_cell(), None);
|
||||
/// ```
|
||||
pub const fn selected_cell(&self) -> Option<(usize, usize)> {
|
||||
if let (Some(r), Some(c)) = (self.selected, self.selected_column) {
|
||||
return Some((r, c));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Mutable reference to the index of the selected row
|
||||
///
|
||||
/// Returns `None` if no row is selected
|
||||
|
@ -170,6 +254,21 @@ impl TableState {
|
|||
&mut self.selected
|
||||
}
|
||||
|
||||
/// Mutable reference to the index of the selected column
|
||||
///
|
||||
/// Returns `None` if no column is selected
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let mut state = TableState::default();
|
||||
/// *state.selected_column_mut() = Some(1);
|
||||
/// ```
|
||||
pub fn selected_column_mut(&mut self) -> &mut Option<usize> {
|
||||
&mut self.selected_column
|
||||
}
|
||||
|
||||
/// Sets the index of the selected row
|
||||
///
|
||||
/// Set to `None` if no row is selected. This will also reset the offset to `0`.
|
||||
|
@ -189,9 +288,44 @@ impl TableState {
|
|||
}
|
||||
}
|
||||
|
||||
/// Selects the next item or the first one if no item is selected
|
||||
/// Sets the index of the selected column
|
||||
///
|
||||
/// Note: until the table is rendered, the number of items is not known, so the index is set to
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let mut state = TableState::default();
|
||||
/// state.select_column(Some(1));
|
||||
/// ```
|
||||
pub fn select_column(&mut self, index: Option<usize>) {
|
||||
self.selected_column = index;
|
||||
}
|
||||
|
||||
/// Sets the indexes of the selected cell
|
||||
///
|
||||
/// Set to `None` if no cell is selected. This will also reset the row offset to `0`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let mut state = TableState::default();
|
||||
/// state.select_cell(Some((1, 5)));
|
||||
/// ```
|
||||
pub fn select_cell(&mut self, indexes: Option<(usize, usize)>) {
|
||||
if let Some((r, c)) = indexes {
|
||||
self.selected = Some(r);
|
||||
self.selected_column = Some(c);
|
||||
} else {
|
||||
self.offset = 0;
|
||||
self.selected = None;
|
||||
self.selected_column = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// Selects the next row or the first one if no row is selected
|
||||
///
|
||||
/// Note: until the table is rendered, the number of rows is not known, so the index is set to
|
||||
/// `0` and will be corrected when the table is rendered
|
||||
///
|
||||
/// # Examples
|
||||
|
@ -207,9 +341,26 @@ impl TableState {
|
|||
self.select(Some(next));
|
||||
}
|
||||
|
||||
/// Selects the previous item or the last one if no item is selected
|
||||
/// Selects the next column or the first one if no column is selected
|
||||
///
|
||||
/// Note: until the table is rendered, the number of items is not known, so the index is set to
|
||||
/// Note: until the table is rendered, the number of columns is not known, so the index is set
|
||||
/// to `0` and will be corrected when the table is rendered
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let mut state = TableState::default();
|
||||
/// state.select_next_column();
|
||||
/// ```
|
||||
pub fn select_next_column(&mut self) {
|
||||
let next = self.selected_column.map_or(0, |i| i.saturating_add(1));
|
||||
self.select_column(Some(next));
|
||||
}
|
||||
|
||||
/// Selects the previous row or the last one if no item is selected
|
||||
///
|
||||
/// Note: until the table is rendered, the number of rows is not known, so the index is set to
|
||||
/// `usize::MAX` and will be corrected when the table is rendered
|
||||
///
|
||||
/// # Examples
|
||||
|
@ -225,9 +376,28 @@ impl TableState {
|
|||
self.select(Some(previous));
|
||||
}
|
||||
|
||||
/// Selects the first item
|
||||
/// Selects the previous column or the last one if no column is selected
|
||||
///
|
||||
/// Note: until the table is rendered, the number of items is not known, so the index is set to
|
||||
/// Note: until the table is rendered, the number of columns is not known, so the index is set
|
||||
/// to `usize::MAX` and will be corrected when the table is rendered
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let mut state = TableState::default();
|
||||
/// state.select_previous_column();
|
||||
/// ```
|
||||
pub fn select_previous_column(&mut self) {
|
||||
let previous = self
|
||||
.selected_column
|
||||
.map_or(usize::MAX, |i| i.saturating_sub(1));
|
||||
self.select_column(Some(previous));
|
||||
}
|
||||
|
||||
/// Selects the first row
|
||||
///
|
||||
/// Note: until the table is rendered, the number of rows is not known, so the index is set to
|
||||
/// `0` and will be corrected when the table is rendered
|
||||
///
|
||||
/// # Examples
|
||||
|
@ -242,9 +412,25 @@ impl TableState {
|
|||
self.select(Some(0));
|
||||
}
|
||||
|
||||
/// Selects the last item
|
||||
/// Selects the first column
|
||||
///
|
||||
/// Note: until the table is rendered, the number of items is not known, so the index is set to
|
||||
/// Note: until the table is rendered, the number of columns is not known, so the index is set
|
||||
/// to `0` and will be corrected when the table is rendered
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let mut state = TableState::default();
|
||||
/// state.select_first_column();
|
||||
/// ```
|
||||
pub fn select_first_column(&mut self) {
|
||||
self.select_column(Some(0));
|
||||
}
|
||||
|
||||
/// Selects the last row
|
||||
///
|
||||
/// Note: until the table is rendered, the number of rows is not known, so the index is set to
|
||||
/// `usize::MAX` and will be corrected when the table is rendered
|
||||
///
|
||||
/// # Examples
|
||||
|
@ -259,11 +445,27 @@ impl TableState {
|
|||
self.select(Some(usize::MAX));
|
||||
}
|
||||
|
||||
/// Selects the last column
|
||||
///
|
||||
/// Note: until the table is rendered, the number of columns is not known, so the index is set
|
||||
/// to `usize::MAX` and will be corrected when the table is rendered
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let mut state = TableState::default();
|
||||
/// state.select_last();
|
||||
/// ```
|
||||
pub fn select_last_column(&mut self) {
|
||||
self.select_column(Some(usize::MAX));
|
||||
}
|
||||
|
||||
/// Scrolls down by a specified `amount` in the table.
|
||||
///
|
||||
/// This method updates the selected index by moving it down by the given `amount`.
|
||||
/// If the `amount` causes the index to go out of bounds (i.e., if the index is greater than
|
||||
/// the length of the table), the last item in the table will be selected.
|
||||
/// the number of rows in the table), the last row in the table will be selected.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
|
@ -282,7 +484,7 @@ impl TableState {
|
|||
///
|
||||
/// This method updates the selected index by moving it up by the given `amount`.
|
||||
/// If the `amount` causes the index to go out of bounds (i.e., less than zero),
|
||||
/// the first item in the table will be selected.
|
||||
/// the first row in the table will be selected.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
|
@ -296,6 +498,42 @@ impl TableState {
|
|||
let selected = self.selected.unwrap_or_default();
|
||||
self.select(Some(selected.saturating_sub(amount as usize)));
|
||||
}
|
||||
|
||||
/// Scrolls right by a specified `amount` in the table.
|
||||
///
|
||||
/// This method updates the selected index by moving it right by the given `amount`.
|
||||
/// If the `amount` causes the index to go out of bounds (i.e., if the index is greater than
|
||||
/// the number of columns in the table), the last column in the table will be selected.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let mut state = TableState::default();
|
||||
/// state.scroll_right_by(4);
|
||||
/// ```
|
||||
pub fn scroll_right_by(&mut self, amount: u16) {
|
||||
let selected = self.selected_column.unwrap_or_default();
|
||||
self.select_column(Some(selected.saturating_add(amount as usize)));
|
||||
}
|
||||
|
||||
/// Scrolls left by a specified `amount` in the table.
|
||||
///
|
||||
/// This method updates the selected index by moving it left by the given `amount`.
|
||||
/// If the `amount` causes the index to go out of bounds (i.e., less than zero),
|
||||
/// the first item in the table will be selected.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let mut state = TableState::default();
|
||||
/// state.scroll_left_by(4);
|
||||
/// ```
|
||||
pub fn scroll_left_by(&mut self, amount: u16) {
|
||||
let selected = self.selected_column.unwrap_or_default();
|
||||
self.select_column(Some(selected.saturating_sub(amount as usize)));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -307,6 +545,7 @@ mod tests {
|
|||
let state = TableState::new();
|
||||
assert_eq!(state.offset, 0);
|
||||
assert_eq!(state.selected, None);
|
||||
assert_eq!(state.selected_column, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -321,6 +560,19 @@ mod tests {
|
|||
assert_eq!(state.selected, Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_selected_column() {
|
||||
let state = TableState::new().with_selected_column(Some(1));
|
||||
assert_eq!(state.selected_column, Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_selected_cell_none() {
|
||||
let state = TableState::new().with_selected_cell(None);
|
||||
assert_eq!(state.selected, None);
|
||||
assert_eq!(state.selected_column, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offset() {
|
||||
let state = TableState::new();
|
||||
|
@ -340,6 +592,18 @@ mod tests {
|
|||
assert_eq!(state.selected(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selected_column() {
|
||||
let state = TableState::new();
|
||||
assert_eq!(state.selected_column(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selected_cell() {
|
||||
let state = TableState::new();
|
||||
assert_eq!(state.selected_cell(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selected_mut() {
|
||||
let mut state = TableState::new();
|
||||
|
@ -347,6 +611,13 @@ mod tests {
|
|||
assert_eq!(state.selected, Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selected_column_mut() {
|
||||
let mut state = TableState::new();
|
||||
*state.selected_column_mut() = Some(1);
|
||||
assert_eq!(state.selected_column, Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select() {
|
||||
let mut state = TableState::new();
|
||||
|
@ -361,6 +632,36 @@ mod tests {
|
|||
assert_eq!(state.selected, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_column() {
|
||||
let mut state = TableState::new();
|
||||
state.select_column(Some(1));
|
||||
assert_eq!(state.selected_column, Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_column_none() {
|
||||
let mut state = TableState::new().with_selected_column(Some(1));
|
||||
state.select_column(None);
|
||||
assert_eq!(state.selected_column, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_cell() {
|
||||
let mut state = TableState::new();
|
||||
state.select_cell(Some((1, 5)));
|
||||
assert_eq!(state.selected_cell(), Some((1, 5)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_cell_none() {
|
||||
let mut state = TableState::new().with_selected_cell(Some((1, 5)));
|
||||
state.select_cell(None);
|
||||
assert_eq!(state.selected, None);
|
||||
assert_eq!(state.selected_column, None);
|
||||
assert_eq!(state.selected_cell(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_table_state_navigation() {
|
||||
let mut state = TableState::default();
|
||||
|
@ -411,5 +712,37 @@ mod tests {
|
|||
|
||||
state.scroll_up_by(4);
|
||||
assert_eq!(state.selected, Some(0));
|
||||
|
||||
let mut state = TableState::default();
|
||||
state.select_first_column();
|
||||
assert_eq!(state.selected_column, Some(0));
|
||||
|
||||
state.select_previous_column();
|
||||
assert_eq!(state.selected_column, Some(0));
|
||||
|
||||
state.select_next_column();
|
||||
assert_eq!(state.selected_column, Some(1));
|
||||
|
||||
state.select_previous_column();
|
||||
assert_eq!(state.selected_column, Some(0));
|
||||
|
||||
state.select_last_column();
|
||||
assert_eq!(state.selected_column, Some(usize::MAX));
|
||||
|
||||
state.select_previous_column();
|
||||
assert_eq!(state.selected_column, Some(usize::MAX - 1));
|
||||
|
||||
let mut state = TableState::default().with_selected_column(Some(12));
|
||||
state.scroll_right_by(4);
|
||||
assert_eq!(state.selected_column, Some(16));
|
||||
|
||||
state.scroll_left_by(20);
|
||||
assert_eq!(state.selected_column, Some(0));
|
||||
|
||||
state.scroll_right_by(100);
|
||||
assert_eq!(state.selected_column, Some(100));
|
||||
|
||||
state.scroll_left_by(20);
|
||||
assert_eq!(state.selected_column, Some(80));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ impl Default for AppState {
|
|||
impl AppState {
|
||||
fn select(&mut self, index: usize) {
|
||||
self.list.select(Some(index));
|
||||
self.table.select(Some(index));
|
||||
self.table.select_cell(Some((index, index)));
|
||||
self.scrollbar = self.scrollbar.position(index);
|
||||
}
|
||||
}
|
||||
|
@ -107,7 +107,8 @@ const DEFAULT_STATE_REPR: &str = r#"{
|
|||
},
|
||||
"table": {
|
||||
"offset": 0,
|
||||
"selected": null
|
||||
"selected": null,
|
||||
"selected_column": null
|
||||
},
|
||||
"scrollbar": {
|
||||
"content_length": 10,
|
||||
|
@ -144,7 +145,8 @@ const SELECTED_STATE_REPR: &str = r#"{
|
|||
},
|
||||
"table": {
|
||||
"offset": 0,
|
||||
"selected": 1
|
||||
"selected": 1,
|
||||
"selected_column": 0
|
||||
},
|
||||
"scrollbar": {
|
||||
"content_length": 10,
|
||||
|
@ -183,7 +185,8 @@ const SCROLLED_STATE_REPR: &str = r#"{
|
|||
},
|
||||
"table": {
|
||||
"offset": 4,
|
||||
"selected": 8
|
||||
"selected": 8,
|
||||
"selected_column": 0
|
||||
},
|
||||
"scrollbar": {
|
||||
"content_length": 10,
|
||||
|
@ -206,3 +209,23 @@ fn scrolled_state_deserialize() {
|
|||
let mut state: AppState = serde_json::from_str(SCROLLED_STATE_REPR).unwrap();
|
||||
assert_buffer(&mut state, SCROLLED_STATE_BUFFER);
|
||||
}
|
||||
|
||||
// For backwards compatibility these fields should be enough to deserialize the state.
|
||||
const OLD_TABLE_DESERIALIZE: &str = r#"{
|
||||
"offset": 0,
|
||||
"selected": 1
|
||||
}"#;
|
||||
|
||||
const NEW_TABLE_DESERIALIZE: &str = r#"{
|
||||
"offset": 0,
|
||||
"selected": 1,
|
||||
"selected_column": null
|
||||
}"#;
|
||||
|
||||
// This test is to check for backwards compatibility with the old states.
|
||||
#[test]
|
||||
fn table_state_backwards_compatibility() {
|
||||
let old_state: TableState = serde_json::from_str(OLD_TABLE_DESERIALIZE).unwrap();
|
||||
let new_state: TableState = serde_json::from_str(NEW_TABLE_DESERIALIZE).unwrap();
|
||||
assert_eq!(old_state, new_state);
|
||||
}
|
||||
|
|
|
@ -632,6 +632,7 @@ fn widgets_table_can_have_elements_styled_individually() {
|
|||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
let mut state = TableState::default();
|
||||
state.select(Some(0));
|
||||
state.select_column(Some(1));
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let table = Table::new(
|
||||
|
@ -658,7 +659,9 @@ fn widgets_table_can_have_elements_styled_individually() {
|
|||
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
|
||||
.block(Block::new().borders(Borders::LEFT | Borders::RIGHT))
|
||||
.highlight_symbol(">> ")
|
||||
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
|
||||
.row_highlight_style(Style::default().add_modifier(Modifier::BOLD))
|
||||
.column_highlight_style(Style::default().add_modifier(Modifier::ITALIC))
|
||||
.cell_highlight_style(Style::default().add_modifier(Modifier::DIM))
|
||||
.column_spacing(1);
|
||||
f.render_stateful_widget(table, f.area(), &mut state);
|
||||
})
|
||||
|
@ -678,6 +681,19 @@ fn widgets_table_can_have_elements_styled_individually() {
|
|||
.add_modifier(Modifier::BOLD),
|
||||
);
|
||||
}
|
||||
|
||||
// Second column highlight style
|
||||
for row in 2..=3 {
|
||||
for col in 11..=16 {
|
||||
expected[(col, row)].set_style(Style::default().add_modifier(Modifier::ITALIC));
|
||||
}
|
||||
}
|
||||
|
||||
// First row, second column highlight style (cell highlight)
|
||||
for col in 11..=16 {
|
||||
expected[(col, 2)].set_style(Style::default().add_modifier(Modifier::DIM));
|
||||
}
|
||||
|
||||
// Second row:
|
||||
// 1. row color
|
||||
for col in 1..=28 {
|
||||
|
|
Loading…
Reference in a new issue