Skip to content

Commit

Permalink
feat(table): select first, last, etc to table state (#1198)
Browse files Browse the repository at this point in the history
Add select_previous, select_next, select_first & select_last to
TableState

Used equivalent API as in ListState
  • Loading branch information
robertpsoane authored Jun 25, 2024
1 parent 0a18dcb commit 36d49e5
Show file tree
Hide file tree
Showing 2 changed files with 164 additions and 0 deletions.
62 changes: 62 additions & 0 deletions src/widgets/table/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,14 @@ impl StatefulWidgetRef for Table<'_> {
return;
}

if state.selected.is_some_and(|s| s >= self.rows.len()) {
state.select(Some(self.rows.len().saturating_sub(1)));
}

if self.rows.is_empty() {
state.select(None);
}

let selection_width = self.selection_width(state);
let columns_widths = self.get_columns_widths(table_area.width, selection_width);
let (header_area, rows_area, footer_area) = self.layout(table_area);
Expand Down Expand Up @@ -1016,6 +1024,60 @@ mod tests {
assert_eq!(table.widths, vec![Constraint::Percentage(100)], "vec ref");
}

#[cfg(test)]
mod state {
use rstest::{fixture, rstest};

use super::TableState;
use crate::{
buffer::Buffer,
layout::{Constraint, Rect},
widgets::{Row, StatefulWidget, Table},
};

#[fixture]
fn table_buf() -> Buffer {
Buffer::empty(Rect::new(0, 0, 10, 10))
}

#[rstest]
fn test_list_state_empty_list(mut table_buf: Buffer) {
let mut state = TableState::default();

let rows: Vec<Row> = Vec::new();
let widths = vec![Constraint::Percentage(100)];
let table = Table::new(rows, widths);
state.select_first();
StatefulWidget::render(table, table_buf.area, &mut table_buf, &mut state);
assert_eq!(state.selected, None);
}

#[rstest]
fn test_list_state_single_item(mut table_buf: Buffer) {
let mut state = TableState::default();

let widths = vec![Constraint::Percentage(100)];

let items = vec![Row::new(vec!["Item 1"])];
let table = Table::new(items, widths);
state.select_first();
StatefulWidget::render(&table, table_buf.area, &mut table_buf, &mut state);
assert_eq!(state.selected, Some(0));

state.select_last();
StatefulWidget::render(&table, table_buf.area, &mut table_buf, &mut state);
assert_eq!(state.selected, Some(0));

state.select_previous();
StatefulWidget::render(&table, table_buf.area, &mut table_buf, &mut state);
assert_eq!(state.selected, Some(0));

state.select_next();
StatefulWidget::render(&table, table_buf.area, &mut table_buf, &mut state);
assert_eq!(state.selected, Some(0));
}
}

#[cfg(test)]
mod render {
use rstest::rstest;
Expand Down
102 changes: 102 additions & 0 deletions src/widgets/table/table_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,72 @@ impl TableState {
self.offset = 0;
}
}

/// Selects the next item or the first one if no item is selected
///
/// Note: until the table is rendered, the number of items 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();
/// ```
pub fn select_next(&mut self) {
let next = self.selected.map_or(0, |i| i.saturating_add(1));
self.select(Some(next));
}

/// Selects the previous item or the last one if no item is selected
///
/// Note: until the table is rendered, the number of items 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();
/// ```
pub fn select_previous(&mut self) {
let previous = self.selected.map_or(usize::MAX, |i| i.saturating_sub(1));
self.select(Some(previous));
}

/// Selects the first item
///
/// Note: until the table is rendered, the number of items 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();
/// ```
pub fn select_first(&mut self) {
self.select(Some(0));
}

/// Selects the last item
///
/// Note: until the table is rendered, the number of items 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(&mut self) {
self.select(Some(usize::MAX));
}
}

#[cfg(test)]
Expand Down Expand Up @@ -239,4 +305,40 @@ mod tests {
state.select(None);
assert_eq!(state.selected, None);
}

#[test]
fn test_table_state_navigation() {
let mut state = TableState::default();
state.select_first();
assert_eq!(state.selected, Some(0));

state.select_previous(); // should not go below 0
assert_eq!(state.selected, Some(0));

state.select_next();
assert_eq!(state.selected, Some(1));

state.select_previous();
assert_eq!(state.selected, Some(0));

state.select_last();
assert_eq!(state.selected, Some(usize::MAX));

state.select_next(); // should not go above usize::MAX
assert_eq!(state.selected, Some(usize::MAX));

state.select_previous();
assert_eq!(state.selected, Some(usize::MAX - 1));

state.select_next();
assert_eq!(state.selected, Some(usize::MAX));

let mut state = TableState::default();
state.select_next();
assert_eq!(state.selected, Some(0));

let mut state = TableState::default();
state.select_previous();
assert_eq!(state.selected, Some(usize::MAX));
}
}

0 comments on commit 36d49e5

Please sign in to comment.