Table
Demonstrates the Table widget.
git clone https://github.com/ratatui/ratatui.git --branch latestcd ratatuicargo run --example=table --features=crossterm
//! # [Ratatui] Table example//!//! The latest version of this example is available in the [examples] folder in the repository.//!//! Please note that the examples are designed to be run against the `main` branch of the Github//! repository. This means that you may not be able to compile with the latest release version on//! crates.io, or the one that you have installed locally.//!//! See the [examples readme] for more information on finding examples that match the version of the//! library you are using.//!//! [Ratatui]: https://github.com/ratatui/ratatui//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use color_eyre::Result;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},    widgets::{        Block, BorderType, Cell, HighlightSpacing, Paragraph, Row, Scrollbar, ScrollbarOrientation,        ScrollbarState, Table, TableState,    },    DefaultTerminal, Frame,};use style::palette::tailwind;use unicode_width::UnicodeWidthStr;
const PALETTES: [tailwind::Palette; 4] = [    tailwind::BLUE,    tailwind::EMERALD,    tailwind::INDIGO,    tailwind::RED,];const INFO_TEXT: &str =    "(Esc) quit | (↑) move up | (↓) move down | (→) next color | (←) previous color";
const ITEM_HEIGHT: usize = 4;
fn main() -> Result<()> {    color_eyre::install()?;    let terminal = ratatui::init();    let app_result = App::new().run(terminal);    ratatui::restore();    app_result}struct TableColors {    buffer_bg: Color,    header_bg: Color,    header_fg: Color,    row_fg: Color,    selected_style_fg: Color,    normal_row_color: Color,    alt_row_color: Color,    footer_border_color: Color,}
impl TableColors {    const fn new(color: &tailwind::Palette) -> Self {        Self {            buffer_bg: tailwind::SLATE.c950,            header_bg: color.c900,            header_fg: tailwind::SLATE.c200,            row_fg: tailwind::SLATE.c200,            selected_style_fg: color.c400,            normal_row_color: tailwind::SLATE.c950,            alt_row_color: tailwind::SLATE.c900,            footer_border_color: color.c400,        }    }}
struct Data {    name: String,    address: String,    email: String,}
impl Data {    const fn ref_array(&self) -> [&String; 3] {        [&self.name, &self.address, &self.email]    }
    fn name(&self) -> &str {        &self.name    }
    fn address(&self) -> &str {        &self.address    }
    fn email(&self) -> &str {        &self.email    }}
struct App {    state: TableState,    items: Vec<Data>,    longest_item_lens: (u16, u16, u16), // order is (name, address, email)    scroll_state: ScrollbarState,    colors: TableColors,    color_index: usize,}
impl App {    fn new() -> Self {        let data_vec = generate_fake_names();        Self {            state: TableState::default().with_selected(0),            longest_item_lens: constraint_len_calculator(&data_vec),            scroll_state: ScrollbarState::new((data_vec.len() - 1) * ITEM_HEIGHT),            colors: TableColors::new(&PALETTES[0]),            color_index: 0,            items: data_vec,        }    }
    pub fn next(&mut self) {        let i = match self.state.selected() {            Some(i) => {                if i >= self.items.len() - 1 {                    0                } else {                    i + 1                }            }            None => 0,        };        self.state.select(Some(i));        self.scroll_state = self.scroll_state.position(i * ITEM_HEIGHT);    }
    pub fn previous(&mut self) {        let i = match self.state.selected() {            Some(i) => {                if i == 0 {                    self.items.len() - 1                } else {                    i - 1                }            }            None => 0,        };        self.state.select(Some(i));        self.scroll_state = self.scroll_state.position(i * ITEM_HEIGHT);    }
    pub fn next_color(&mut self) {        self.color_index = (self.color_index + 1) % PALETTES.len();    }
    pub fn previous_color(&mut self) {        let count = PALETTES.len();        self.color_index = (self.color_index + count - 1) % count;    }
    pub fn set_colors(&mut self) {        self.colors = TableColors::new(&PALETTES[self.color_index]);    }
    fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {        loop {            terminal.draw(|frame| self.draw(frame))?;
            if let Event::Key(key) = event::read()? {                if key.kind == KeyEventKind::Press {                    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(),                        _ => {}                    }                }            }        }    }
    fn draw(&mut self, frame: &mut Frame) {        let vertical = &Layout::vertical([Constraint::Min(5), Constraint::Length(3)]);        let rects = vertical.split(frame.area());
        self.set_colors();
        self.render_table(frame, rects[0]);        self.render_scrollbar(frame, rects[0]);        self.render_footer(frame, rects[1]);    }
    fn render_table(&mut self, frame: &mut Frame, area: Rect) {        let header_style = Style::default()            .fg(self.colors.header_fg)            .bg(self.colors.header_bg);        let selected_style = Style::default()            .add_modifier(Modifier::REVERSED)            .fg(self.colors.selected_style_fg);
        let header = ["Name", "Address", "Email"]            .into_iter()            .map(Cell::from)            .collect::<Row>()            .style(header_style)            .height(1);        let rows = self.items.iter().enumerate().map(|(i, data)| {            let color = match i % 2 {                0 => self.colors.normal_row_color,                _ => self.colors.alt_row_color,            };            let item = data.ref_array();            item.into_iter()                .map(|content| Cell::from(Text::from(format!("\n{content}\n"))))                .collect::<Row>()                .style(Style::new().fg(self.colors.row_fg).bg(color))                .height(4)        });        let bar = " █ ";        let t = Table::new(            rows,            [                // + 1 is for padding.                Constraint::Length(self.longest_item_lens.0 + 1),                Constraint::Min(self.longest_item_lens.1 + 1),                Constraint::Min(self.longest_item_lens.2),            ],        )        .header(header)        .highlight_style(selected_style)        .highlight_symbol(Text::from(vec![            "".into(),            bar.into(),            bar.into(),            "".into(),        ]))        .bg(self.colors.buffer_bg)        .highlight_spacing(HighlightSpacing::Always);        frame.render_stateful_widget(t, area, &mut self.state);    }
    fn render_scrollbar(&mut self, frame: &mut Frame, area: Rect) {        frame.render_stateful_widget(            Scrollbar::default()                .orientation(ScrollbarOrientation::VerticalRight)                .begin_symbol(None)                .end_symbol(None),            area.inner(Margin {                vertical: 1,                horizontal: 1,            }),            &mut self.scroll_state,        );    }
    fn render_footer(&self, frame: &mut Frame, area: Rect) {        let info_footer = Paragraph::new(Line::from(INFO_TEXT))            .style(                Style::new()                    .fg(self.colors.row_fg)                    .bg(self.colors.buffer_bg),            )            .centered()            .block(                Block::bordered()                    .border_type(BorderType::Double)                    .border_style(Style::new().fg(self.colors.footer_border_color)),            );        frame.render_widget(info_footer, area);    }}
fn generate_fake_names() -> Vec<Data> {    use fakeit::{address, contact, name};
    (0..20)        .map(|_| {            let name = name::full();            let address = format!(                "{}\n{}, {} {}",                address::street(),                address::city(),                address::state(),                address::zip()            );            let email = contact::email();
            Data {                name,                address,                email,            }        })        .sorted_by(|a, b| a.name.cmp(&b.name))        .collect_vec()}
fn constraint_len_calculator(items: &[Data]) -> (u16, u16, u16) {    let name_len = items        .iter()        .map(Data::name)        .map(UnicodeWidthStr::width)        .max()        .unwrap_or(0);    let address_len = items        .iter()        .map(Data::address)        .flat_map(str::lines)        .map(UnicodeWidthStr::width)        .max()        .unwrap_or(0);    let email_len = items        .iter()        .map(Data::email)        .map(UnicodeWidthStr::width)        .max()        .unwrap_or(0);
    #[allow(clippy::cast_possible_truncation)]    (name_len as u16, address_len as u16, email_len as u16)}
#[cfg(test)]mod tests {    use crate::Data;
    #[test]    fn constraint_len_calculator() {        let test_data = vec![            Data {                name: "Emirhan Tala".to_string(),                address: "Cambridgelaan 6XX\n3584 XX Utrecht".to_string(),                email: "tala.emirhan@gmail.com".to_string(),            },            Data {                name: "thistextis26characterslong".to_string(),                address: "this line is 31 characters long\nbottom line is 33 characters long"                    .to_string(),                email: "thisemailis40caharacterslong@ratatui.com".to_string(),            },        ];        let (longest_name_len, longest_address_len, longest_email_len) =            crate::constraint_len_calculator(&test_data);
        assert_eq!(26, longest_name_len);        assert_eq!(33, longest_address_len);        assert_eq!(40, longest_email_len);    }}