From dc8d0587ecfd46cde86c9e33a6fd385e2d4810a9 Mon Sep 17 00:00:00 2001 From: Tayfun Bocek Date: Sun, 13 Oct 2024 14:06:29 +0300 Subject: [PATCH] feat(table)!: add support for selecting column and cell (#1331) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- BREAKING-CHANGES.md | 16 ++ examples/async.rs | 2 +- examples/demo2/tabs/recipe.rs | 2 +- examples/demo2/tabs/traceroute.rs | 2 +- examples/table.rs | 59 ++-- src/widgets/table/table.rs | 456 ++++++++++++++++++++++++++---- src/widgets/table/table_state.rs | 359 ++++++++++++++++++++++- tests/state_serde.rs | 31 +- tests/widgets_table.rs | 18 +- 9 files changed, 847 insertions(+), 98 deletions(-) diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 012a5592..4712a426 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -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]) diff --git a/examples/async.rs b/examples/async.rs index 525c96a1..c01761f5 100644 --- a/examples/async.rs +++ b/examples/async.rs @@ -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); } diff --git a/examples/demo2/tabs/recipe.rs b/examples/demo2/tabs/recipe.rs index e17a7892..95df3bbf 100644 --- a/examples/demo2/tabs/recipe.rs +++ b/examples/demo2/tabs/recipe.rs @@ -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, diff --git a/examples/demo2/tabs/traceroute.rs b/examples/demo2/tabs/traceroute.rs index 70a2ae6b..3cf56c30 100644 --- a/examples/demo2/tabs/traceroute.rs +++ b/examples/demo2/tabs/traceroute.rs @@ -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, diff --git a/examples/table.rs b/examples/table.rs index 4e7fad56..cd662725 100644 --- a/examples/table.rs +++ b/examples/table.rs @@ -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) diff --git a/src/widgets/table/table.rs b/src/widgets/table/table.rs index a98dd860..71e1f4e8 100644 --- a/src/widgets/table/table.rs +++ b/src/widgets/table/table.rs @@ -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>(mut self, highlight_style: S) -> Self { - self.highlight_style = highlight_style.into(); + #[deprecated(note = "use `Table::row_highlight_style` instead")] + pub fn highlight_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