diff options
| author | Vladislav Stepanov <8uk.8ak@gmail.com> | 2023-04-14 23:18:58 +0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-04-14 20:18:58 +0100 |
| commit | c05d2850420a2c163b8f62c33a6cef7c0ae1ad8d (patch) | |
| tree | 2c44a44eda7e76fa74e78ac1fd02f55c1ed4d804 /src/ratatui/widgets/table.rs | |
| parent | Switch to uuidv7 (#864) (diff) | |
| download | atuin-c05d2850420a2c163b8f62c33a6cef7c0ae1ad8d.zip | |
Workspace reorder (#868)
* Try different workspace structure
Move main crate (atuin) to be on the same level with other crates in
this workspace
* extract common dependencies to the workspace definition
* fix base64 v0.21 deprecation warning
* questionable: update deps & fix chrono deprecations
possible panic sites are unchanged, they're just more visible now
* Revert "questionable: update deps & fix chrono deprecations"
This reverts commit 993e60f8dea81a1625a04285a617959ad09a0866.
Diffstat (limited to 'src/ratatui/widgets/table.rs')
| -rw-r--r-- | src/ratatui/widgets/table.rs | 504 |
1 files changed, 0 insertions, 504 deletions
diff --git a/src/ratatui/widgets/table.rs b/src/ratatui/widgets/table.rs deleted file mode 100644 index d987a8d9..00000000 --- a/src/ratatui/widgets/table.rs +++ /dev/null @@ -1,504 +0,0 @@ -use crate::ratatui::{ - buffer::Buffer, - layout::{Constraint, Direction, Layout, Rect}, - style::Style, - text::Text, - widgets::{Block, StatefulWidget, Widget}, -}; -use unicode_width::UnicodeWidthStr; - -/// A [`Cell`] contains the [`Text`] to be displayed in a [`Row`] of a [`Table`]. -/// -/// It can be created from anything that can be converted to a [`Text`]. -/// ```rust -/// # use ratatui::widgets::Cell; -/// # use ratatui::style::{Style, Modifier}; -/// # use ratatui::text::{Span, Spans, Text}; -/// # use std::borrow::Cow; -/// Cell::from("simple string"); -/// -/// Cell::from(Span::from("span")); -/// -/// Cell::from(Spans::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"))); -/// ``` -/// -/// You can apply a [`Style`] on the entire [`Cell`] using [`Cell::style`] or rely on the styling -/// capabilities of [`Text`]. -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct Cell<'a> { - content: Text<'a>, - style: Style, -} - -impl<'a> Cell<'a> { - /// Set the `Style` of this cell. - pub fn style(mut self, style: Style) -> Self { - self.style = style; - self - } -} - -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(), - } - } -} - -/// Holds data to be displayed in a [`Table`] widget. -/// -/// A [`Row`] is a collection of cells. It can be created from simple strings: -/// ```rust -/// # use ratatui::widgets::Row; -/// Row::new(vec!["Cell1", "Cell2", "Cell3"]); -/// ``` -/// -/// But if you need a bit more control over individual cells, you can explicitly create [`Cell`]s: -/// ```rust -/// # use ratatui::widgets::{Row, Cell}; -/// # use ratatui::style::{Style, Color}; -/// 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::widgets::Row; -/// Row::new(vec![ -/// Cow::Borrowed("hello"), -/// Cow::Owned("world".to_uppercase()), -/// ]); -/// ``` -/// -/// By default, a row has a height of 1 but you can change this using [`Row::height`]. -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct Row<'a> { - cells: Vec<Cell<'a>>, - height: u16, - style: Style, - bottom_margin: u16, -} - -impl<'a> Row<'a> { - /// Creates a new [`Row`] from an iterator where items can be converted to a [`Cell`]. - pub fn new<T>(cells: T) -> Self - where - T: IntoIterator, - T::Item: Into<Cell<'a>>, - { - Self { - height: 1, - cells: cells.into_iter().map(|c| c.into()).collect(), - style: Style::default(), - bottom_margin: 0, - } - } - - /// Set the fixed height of the [`Row`]. Any [`Cell`] whose content has more lines than this - /// height will see its content truncated. - pub fn height(mut self, height: u16) -> Self { - self.height = height; - self - } - - /// Set the [`Style`] of the entire row. This [`Style`] can be overridden by the [`Style`] of a - /// any individual [`Cell`] or event by their [`Text`] content. - pub fn style(mut self, style: Style) -> Self { - self.style = style; - self - } - - /// Set the bottom margin. By default, the bottom margin is `0`. - pub fn bottom_margin(mut self, margin: u16) -> Self { - self.bottom_margin = margin; - self - } - - /// Returns the total height of the row. - fn total_height(&self) -> u16 { - self.height.saturating_add(self.bottom_margin) - } -} - -/// A widget to display data in formatted columns. -/// -/// It is a collection of [`Row`]s, themselves composed of [`Cell`]s: -/// ```rust -/// # use ratatui::widgets::{Block, Borders, Table, Row, Cell}; -/// # use ratatui::layout::Constraint; -/// # use ratatui::style::{Style, Color, Modifier}; -/// # use ratatui::text::{Text, Spans, Span}; -/// Table::new(vec![ -/// // Row can be created from simple strings. -/// Row::new(vec!["Row11", "Row12", "Row13"]), -/// // You can style the entire row. -/// Row::new(vec!["Row21", "Row22", "Row23"]).style(Style::default().fg(Color::Blue)), -/// // If you need more control over the styling you may need to create Cells directly -/// Row::new(vec![ -/// Cell::from("Row31"), -/// Cell::from("Row32").style(Style::default().fg(Color::Yellow)), -/// Cell::from(Spans::from(vec![ -/// Span::raw("Row"), -/// Span::styled("33", Style::default().fg(Color::Green)) -/// ])), -/// ]), -/// // If a Row need to display some content over multiple lines, you just have to change -/// // its height. -/// Row::new(vec![ -/// Cell::from("Row\n41"), -/// Cell::from("Row\n42"), -/// Cell::from("Row\n43"), -/// ]).height(2), -/// ]) -/// // You can set the style of the entire Table. -/// .style(Style::default().fg(Color::White)) -/// // It has an optional header, which is simply a Row always visible at the top. -/// .header( -/// Row::new(vec!["Col1", "Col2", "Col3"]) -/// .style(Style::default().fg(Color::Yellow)) -/// // If you want some space between the header and the rest of the rows, you can always -/// // specify some margin at the bottom. -/// .bottom_margin(1) -/// ) -/// // As any other widget, a Table can be wrapped in a Block. -/// .block(Block::default().title("Table")) -/// // Columns widths are constrained in the same way as Layout... -/// .widths(&[Constraint::Length(5), Constraint::Length(5), Constraint::Length(10)]) -/// // ...and they can be separated by a fixed spacing. -/// .column_spacing(1) -/// // If you wish to highlight a row in any specific way when it is selected... -/// .highlight_style(Style::default().add_modifier(Modifier::BOLD)) -/// // ...and potentially show a symbol in front of the selection. -/// .highlight_symbol(">>"); -/// ``` -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Table<'a> { - /// A block to wrap the widget in - block: Option<Block<'a>>, - /// Base style for the widget - style: Style, - /// Width constraints for each column - widths: &'a [Constraint], - /// Space between each column - column_spacing: u16, - /// Style used to render the selected row - highlight_style: Style, - /// Symbol in front of the selected rom - highlight_symbol: Option<&'a str>, - /// Optional header - header: Option<Row<'a>>, - /// Data to display in each row - rows: Vec<Row<'a>>, -} - -impl<'a> Table<'a> { - pub fn new<T>(rows: T) -> Self - where - T: IntoIterator<Item = Row<'a>>, - { - Self { - block: None, - style: Style::default(), - widths: &[], - column_spacing: 1, - highlight_style: Style::default(), - highlight_symbol: None, - header: None, - rows: rows.into_iter().collect(), - } - } - - pub fn block(mut self, block: Block<'a>) -> Self { - self.block = Some(block); - self - } - - pub fn header(mut self, header: Row<'a>) -> Self { - self.header = Some(header); - self - } - - pub fn widths(mut self, widths: &'a [Constraint]) -> Self { - let between_0_and_100 = |&w| match w { - Constraint::Percentage(p) => p <= 100, - _ => true, - }; - assert!( - widths.iter().all(between_0_and_100), - "Percentages should be between 0 and 100 inclusively." - ); - self.widths = widths; - self - } - - pub fn style(mut self, style: Style) -> Self { - self.style = style; - self - } - - pub fn highlight_symbol(mut self, highlight_symbol: &'a str) -> Self { - self.highlight_symbol = Some(highlight_symbol); - self - } - - pub fn highlight_style(mut self, highlight_style: Style) -> Self { - self.highlight_style = highlight_style; - self - } - - pub fn column_spacing(mut self, spacing: u16) -> Self { - self.column_spacing = spacing; - self - } - - fn get_columns_widths(&self, max_width: u16, has_selection: bool) -> Vec<u16> { - let mut constraints = Vec::with_capacity(self.widths.len() * 2 + 1); - if has_selection { - let highlight_symbol_width = - self.highlight_symbol.map(|s| s.width() as u16).unwrap_or(0); - constraints.push(Constraint::Length(highlight_symbol_width)); - } - for constraint in self.widths { - constraints.push(*constraint); - constraints.push(Constraint::Length(self.column_spacing)); - } - if !self.widths.is_empty() { - constraints.pop(); - } - let chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints(constraints) - .expand_to_fill(false) - .split(Rect { - x: 0, - y: 0, - width: max_width, - height: 1, - }); - let mut chunks = &chunks[..]; - if has_selection { - chunks = &chunks[1..]; - } - chunks.iter().step_by(2).map(|c| c.width).collect() - } - - fn get_row_bounds( - &self, - selected: Option<usize>, - offset: usize, - max_height: u16, - ) -> (usize, usize) { - let offset = offset.min(self.rows.len().saturating_sub(1)); - let mut start = offset; - let mut end = offset; - let mut height = 0; - for item in self.rows.iter().skip(offset) { - if height + item.height > max_height { - break; - } - height += item.total_height(); - end += 1; - } - - let selected = selected.unwrap_or(0).min(self.rows.len() - 1); - while selected >= end { - height = height.saturating_add(self.rows[end].total_height()); - end += 1; - while height > max_height { - height = height.saturating_sub(self.rows[start].total_height()); - start += 1; - } - } - while selected < start { - start -= 1; - height = height.saturating_add(self.rows[start].total_height()); - while height > max_height { - end -= 1; - height = height.saturating_sub(self.rows[end].total_height()); - } - } - (start, end) - } -} - -#[derive(Debug, Clone, Default)] -pub struct TableState { - offset: usize, - selected: Option<usize>, -} - -impl TableState { - pub fn selected(&self) -> Option<usize> { - self.selected - } - - pub fn select(&mut self, index: Option<usize>) { - self.selected = index; - if index.is_none() { - self.offset = 0; - } - } - - /// Returns a copy of the receiver's scroll offset. - /// - /// This is useful, for example, if you need to "synchronize" the scrolling of a `Table` and a `Paragraph`. - pub fn offset(&self) -> usize { - self.offset - } -} - -impl<'a> StatefulWidget for Table<'a> { - type State = TableState; - - fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { - if area.area() == 0 { - return; - } - buf.set_style(area, self.style); - let table_area = match self.block.take() { - Some(b) => { - let inner_area = b.inner(area); - b.render(area, buf); - inner_area - } - None => area, - }; - - let has_selection = state.selected.is_some(); - let columns_widths = self.get_columns_widths(table_area.width, has_selection); - let highlight_symbol = self.highlight_symbol.unwrap_or(""); - let blank_symbol = " ".repeat(highlight_symbol.width()); - let mut current_height = 0; - let mut rows_height = table_area.height; - - // Draw header - if let Some(ref header) = self.header { - let max_header_height = table_area.height.min(header.total_height()); - buf.set_style( - Rect { - x: table_area.left(), - y: table_area.top(), - width: table_area.width, - height: table_area.height.min(header.height), - }, - header.style, - ); - let mut col = table_area.left(); - if has_selection { - col += (highlight_symbol.width() as u16).min(table_area.width); - } - for (width, cell) in columns_widths.iter().zip(header.cells.iter()) { - render_cell( - buf, - cell, - Rect { - x: col, - y: table_area.top(), - width: *width, - height: max_header_height, - }, - ); - col += *width + self.column_spacing; - } - current_height += max_header_height; - rows_height = rows_height.saturating_sub(max_header_height); - } - - // Draw rows - if self.rows.is_empty() { - return; - } - let (start, end) = self.get_row_bounds(state.selected, state.offset, rows_height); - state.offset = start; - for (i, table_row) in self - .rows - .iter_mut() - .enumerate() - .skip(state.offset) - .take(end - start) - { - let (row, col) = (table_area.top() + current_height, table_area.left()); - current_height += table_row.total_height(); - let table_row_area = Rect { - x: col, - y: row, - width: table_area.width, - height: table_row.height, - }; - buf.set_style(table_row_area, table_row.style); - let is_selected = state.selected.map(|s| s == i).unwrap_or(false); - let table_row_start_col = if has_selection { - let symbol = if is_selected { - highlight_symbol - } else { - &blank_symbol - }; - let (col, _) = - buf.set_stringn(col, row, symbol, table_area.width as usize, table_row.style); - col - } else { - col - }; - let mut col = table_row_start_col; - for (width, cell) in columns_widths.iter().zip(table_row.cells.iter()) { - render_cell( - buf, - cell, - Rect { - x: col, - y: row, - width: *width, - height: table_row.height, - }, - ); - col += *width + self.column_spacing; - } - if is_selected { - buf.set_style(table_row_area, self.highlight_style); - } - } - } -} - -fn render_cell(buf: &mut Buffer, cell: &Cell, area: Rect) { - buf.set_style(area, cell.style); - for (i, spans) in cell.content.lines.iter().enumerate() { - if i as u16 >= area.height { - break; - } - buf.set_spans(area.x, area.y + i as u16, spans, area.width); - } -} - -impl<'a> Widget for Table<'a> { - fn render(self, area: Rect, buf: &mut Buffer) { - let mut state = TableState::default(); - StatefulWidget::render(self, area, buf, &mut state); - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - #[should_panic] - fn table_invalid_percentages() { - Table::new(vec![]).widths(&[Constraint::Percentage(110)]); - } -} |
