mirror of
https://github.com/ratatui-org/ratatui
synced 2024-11-25 14:10:31 +00:00
docs(examples): update table example and table.tape (#840)
In table.rs - added scrollbar to the table - colors changed to use style::palette::tailwind - now colors can be changed with keys (l or →) for the next color, (h or ←) for the previous color - added a footer for key info For table.tape - typing speed changed to 0.75s from 0.5s - screen size changed to fit - pushed keys changed to show the current example better Fixes: https://github.com/ratatui-org/ratatui/issues/800
This commit is contained in:
parent
405a125c82
commit
330a899eac
3 changed files with 227 additions and 34 deletions
|
@ -353,7 +353,7 @@ examples/generate.bash
|
||||||
[ratatui-logo.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/ratatui-logo.gif?raw=true
|
[ratatui-logo.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/ratatui-logo.gif?raw=true
|
||||||
[scrollbar.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/scrollbar.gif?raw=true
|
[scrollbar.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/scrollbar.gif?raw=true
|
||||||
[sparkline.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/sparkline.gif?raw=true
|
[sparkline.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/sparkline.gif?raw=true
|
||||||
[table.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/table.gif?raw=true
|
[table.gif]: https://vhs.charm.sh/vhs-6njXBytDf0rwPufUtmSSpI.gif
|
||||||
[tabs.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/tabs.gif?raw=true
|
[tabs.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/tabs.gif?raw=true
|
||||||
[user_input.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/user_input.gif?raw=true
|
[user_input.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/user_input.gif?raw=true
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use std::{error::Error, io, str::FromStr};
|
use std::{error::Error, io};
|
||||||
|
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||||
|
@ -7,17 +7,89 @@ use crossterm::{
|
||||||
};
|
};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use ratatui::{prelude::*, widgets::*};
|
use ratatui::{prelude::*, widgets::*};
|
||||||
|
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;
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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 {
|
||||||
|
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 {
|
struct App {
|
||||||
state: TableState,
|
state: TableState,
|
||||||
items: Vec<Vec<String>>,
|
items: Vec<Data>,
|
||||||
|
longest_item_lens: (u16, u16, u16), // order is (name, address, email)
|
||||||
|
scroll_state: ScrollbarState,
|
||||||
|
colors: TableColors,
|
||||||
|
color_index: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
fn new() -> App {
|
fn new() -> App {
|
||||||
|
let data_vec = generate_fake_names();
|
||||||
App {
|
App {
|
||||||
state: TableState::default().with_selected(0),
|
state: TableState::default().with_selected(0),
|
||||||
items: generate_fake_names(),
|
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) {
|
pub fn next(&mut self) {
|
||||||
|
@ -32,6 +104,7 @@ impl App {
|
||||||
None => 0,
|
None => 0,
|
||||||
};
|
};
|
||||||
self.state.select(Some(i));
|
self.state.select(Some(i));
|
||||||
|
self.scroll_state = self.scroll_state.position(i * ITEM_HEIGHT);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn previous(&mut self) {
|
pub fn previous(&mut self) {
|
||||||
|
@ -46,10 +119,24 @@ impl App {
|
||||||
None => 0,
|
None => 0,
|
||||||
};
|
};
|
||||||
self.state.select(Some(i));
|
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 generate_fake_names() -> Vec<Vec<String>> {
|
fn generate_fake_names() -> Vec<Data> {
|
||||||
use fakeit::{address, contact, name};
|
use fakeit::{address, contact, name};
|
||||||
|
|
||||||
(0..20)
|
(0..20)
|
||||||
|
@ -63,9 +150,14 @@ fn generate_fake_names() -> Vec<Vec<String>> {
|
||||||
address::zip()
|
address::zip()
|
||||||
);
|
);
|
||||||
let email = contact::email();
|
let email = contact::email();
|
||||||
vec![name, address, email]
|
|
||||||
|
Data {
|
||||||
|
name,
|
||||||
|
address,
|
||||||
|
email,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.sorted_by(|a, b| a[0].cmp(&b[0]))
|
.sorted_by(|a, b| a.name.cmp(&b.name))
|
||||||
.collect_vec()
|
.collect_vec()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,10 +195,13 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
|
||||||
|
|
||||||
if let Event::Key(key) = event::read()? {
|
if let Event::Key(key) = event::read()? {
|
||||||
if key.kind == KeyEventKind::Press {
|
if key.kind == KeyEventKind::Press {
|
||||||
|
use KeyCode::*;
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Char('q') => return Ok(()),
|
Char('q') | Esc => return Ok(()),
|
||||||
KeyCode::Down | KeyCode::Char('j') => app.next(),
|
Char('j') | Down => app.next(),
|
||||||
KeyCode::Up | KeyCode::Char('k') => app.previous(),
|
Char('k') | Up => app.previous(),
|
||||||
|
Char('l') | Right => app.next_color(),
|
||||||
|
Char('h') | Left => app.previous_color(),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -115,15 +210,24 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ui(f: &mut Frame, app: &mut App) {
|
fn ui(f: &mut Frame, app: &mut App) {
|
||||||
let rects = Layout::vertical([Constraint::Percentage(100)]).split(f.size());
|
let rects = Layout::vertical([Constraint::Min(5), Constraint::Length(3)]).split(f.size());
|
||||||
|
|
||||||
// colors from https://tailwindcss.com/docs/customizing-colors
|
app.set_colors();
|
||||||
let header_bg = Color::from_str("#1e3a8a").unwrap();
|
|
||||||
let header_fg = Color::from_str("#eff6ff").unwrap();
|
render_table(f, app, rects[0]);
|
||||||
let header_style = Style::default().fg(header_fg).bg(header_bg);
|
|
||||||
let normal_row_color = Color::from_str("#1e293b").unwrap();
|
render_scrollbar(f, app, rects[0]);
|
||||||
let alt_row_color = Color::from_str("#0f172a").unwrap();
|
|
||||||
let selected_style = Style::default().add_modifier(Modifier::REVERSED);
|
render_footer(f, app, rects[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_table(f: &mut Frame, app: &mut App, area: Rect) {
|
||||||
|
let header_style = Style::default()
|
||||||
|
.fg(app.colors.header_fg)
|
||||||
|
.bg(app.colors.header_bg);
|
||||||
|
let selected_style = Style::default()
|
||||||
|
.add_modifier(Modifier::REVERSED)
|
||||||
|
.fg(app.colors.selected_style_fg);
|
||||||
|
|
||||||
let header = ["Name", "Address", "Email"]
|
let header = ["Name", "Address", "Email"]
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -132,25 +236,27 @@ fn ui(f: &mut Frame, app: &mut App) {
|
||||||
.collect::<Row>()
|
.collect::<Row>()
|
||||||
.style(header_style)
|
.style(header_style)
|
||||||
.height(1);
|
.height(1);
|
||||||
let rows = app.items.iter().enumerate().map(|(i, item)| {
|
let rows = app.items.iter().enumerate().map(|(i, data)| {
|
||||||
let color = match i % 2 {
|
let color = match i % 2 {
|
||||||
0 => normal_row_color,
|
0 => app.colors.normal_row_color,
|
||||||
_ => alt_row_color,
|
_ => app.colors.alt_row_color,
|
||||||
};
|
};
|
||||||
|
let item = data.ref_array();
|
||||||
item.iter()
|
item.iter()
|
||||||
.cloned()
|
.cloned()
|
||||||
.map(|content| Cell::from(Text::from(format!("\n{}\n", content))))
|
.map(|content| Cell::from(Text::from(format!("\n{}\n", content))))
|
||||||
.collect::<Row>()
|
.collect::<Row>()
|
||||||
.style(Style::new().bg(color))
|
.style(Style::new().fg(app.colors.row_fg).bg(color))
|
||||||
.height(4)
|
.height(4)
|
||||||
});
|
});
|
||||||
let bar = " █ ";
|
let bar = " █ ";
|
||||||
let t = Table::new(
|
let t = Table::new(
|
||||||
rows,
|
rows,
|
||||||
[
|
[
|
||||||
Constraint::Length(15),
|
// + 1 is for padding.
|
||||||
Constraint::Min(30),
|
Constraint::Length(app.longest_item_lens.0 + 1),
|
||||||
Constraint::Min(25),
|
Constraint::Min(app.longest_item_lens.1 + 1),
|
||||||
|
Constraint::Min(app.longest_item_lens.2),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
.header(header)
|
.header(header)
|
||||||
|
@ -161,6 +267,86 @@ fn ui(f: &mut Frame, app: &mut App) {
|
||||||
bar.into(),
|
bar.into(),
|
||||||
"".into(),
|
"".into(),
|
||||||
]))
|
]))
|
||||||
|
.bg(app.colors.buffer_bg)
|
||||||
.highlight_spacing(HighlightSpacing::Always);
|
.highlight_spacing(HighlightSpacing::Always);
|
||||||
f.render_stateful_widget(t, rects[0], &mut app.state);
|
f.render_stateful_widget(t, area, &mut app.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
(name_len as u16, address_len as u16, email_len as u16)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_scrollbar(f: &mut Frame, app: &mut App, area: Rect) {
|
||||||
|
f.render_stateful_widget(
|
||||||
|
Scrollbar::default()
|
||||||
|
.orientation(ScrollbarOrientation::VerticalRight)
|
||||||
|
.begin_symbol(None)
|
||||||
|
.end_symbol(None),
|
||||||
|
area.inner(&Margin {
|
||||||
|
vertical: 1,
|
||||||
|
horizontal: 1,
|
||||||
|
}),
|
||||||
|
&mut app.scroll_state,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_footer(f: &mut Frame, app: &mut App, area: Rect) {
|
||||||
|
let info_footer = Paragraph::new(Line::from(INFO_TEXT))
|
||||||
|
.style(Style::new().fg(app.colors.row_fg).bg(app.colors.buffer_bg))
|
||||||
|
.alignment(Alignment::Center)
|
||||||
|
.block(
|
||||||
|
Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(Style::new().fg(app.colors.footer_border_color))
|
||||||
|
.border_type(BorderType::Double),
|
||||||
|
);
|
||||||
|
f.render_widget(info_footer, area);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,18 +2,25 @@
|
||||||
# To run this script, install vhs and run `vhs ./examples/table.tape`
|
# To run this script, install vhs and run `vhs ./examples/table.tape`
|
||||||
Output "target/table.gif"
|
Output "target/table.gif"
|
||||||
Set Theme "Aardvark Blue"
|
Set Theme "Aardvark Blue"
|
||||||
Set Width 1200
|
Set Width 1400
|
||||||
Set Height 800
|
Set Height 768
|
||||||
Hide
|
Hide
|
||||||
Type "cargo run --example=table --features=crossterm"
|
Type "cargo run --example=table --features=crossterm"
|
||||||
Enter
|
Enter
|
||||||
Sleep 1s
|
Sleep 1s
|
||||||
Show
|
Show
|
||||||
Sleep 2s
|
Sleep 2s
|
||||||
Set TypingSpeed 0.5s
|
Set TypingSpeed 1s
|
||||||
Down 2
|
Down 3
|
||||||
Up 2
|
Up 6
|
||||||
Down 8
|
Sleep 1s
|
||||||
Up 12
|
Down 3
|
||||||
Down 4
|
Sleep 1s
|
||||||
|
Right 1
|
||||||
|
Sleep 1s
|
||||||
|
Right 1
|
||||||
|
Sleep 1s
|
||||||
|
Right 1
|
||||||
|
Sleep 1s
|
||||||
|
Right 1
|
||||||
Sleep 2s
|
Sleep 2s
|
||||||
|
|
Loading…
Reference in a new issue