docs(examples): refactor Tabs example (#861)

- Used a few new techniques from the 0.26 features (ref widgets, text rendering,
  dividers / padding etc.)
- Updated the app to a simpler application approach
- Use color_eyre
- Make it look pretty (colors, new proportional borders)

![Made with VHS](https://vhs.charm.sh/vhs-4WW21XTtepDhUSq4ZShO56.gif)

---------
Fixes https://github.com/ratatui-org/ratatui/issues/819
Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
This commit is contained in:
Emirhan TALA 2024-01-27 11:39:40 +01:00 committed by GitHub
parent 5b7ad2ad82
commit 4b8e54e811
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 187 additions and 119 deletions

View File

@ -13,37 +13,91 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples //! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md //! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::{error::Error, io}; use std::{io, io::stdout};
use color_eyre::{config::HookBuilder, Result};
use crossterm::{ use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind}, event::{self, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
}; };
use ratatui::{prelude::*, style::palette::tailwind, widgets::*}; use ratatui::{prelude::*, style::palette::tailwind, widgets::*};
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator}; use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
const PALETTES: &[tailwind::Palette] = &[ #[derive(Default)]
tailwind::BLUE, struct App {
tailwind::EMERALD, state: AppState,
tailwind::INDIGO, selected_tab: SelectedTab,
tailwind::RED, }
];
const BORDER_TYPES: &[BorderType] = &[ #[derive(Default, Clone, Copy, PartialEq, Eq)]
BorderType::Rounded, enum AppState {
BorderType::Plain, #[default]
BorderType::Double, Running,
BorderType::Thick, Quitting,
]; }
#[derive(Default, Clone, Copy, Display, FromRepr, EnumIter)] #[derive(Default, Clone, Copy, Display, FromRepr, EnumIter)]
enum SelectedTab { enum SelectedTab {
#[default] #[default]
Tab0, #[strum(to_string = "Tab 1")]
Tab1, Tab1,
#[strum(to_string = "Tab 2")]
Tab2, Tab2,
#[strum(to_string = "Tab 3")]
Tab3, Tab3,
#[strum(to_string = "Tab 4")]
Tab4,
}
fn main() -> Result<()> {
init_error_hooks()?;
let mut terminal = init_terminal()?;
App::default().run(&mut terminal)?;
restore_terminal()?;
Ok(())
}
impl App {
fn run(&mut self, terminal: &mut Terminal<impl Backend>) -> Result<()> {
while self.state == AppState::Running {
self.draw(terminal)?;
self.handle_events()?;
}
Ok(())
}
fn draw(&self, terminal: &mut Terminal<impl Backend>) -> Result<()> {
terminal.draw(|frame| frame.render_widget(self, frame.size()))?;
Ok(())
}
fn handle_events(&mut self) -> Result<(), io::Error> {
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
use KeyCode::*;
match key.code {
Char('l') | Right => self.next_tab(),
Char('h') | Left => self.previous_tab(),
Char('q') | Esc => self.quit(),
_ => {}
}
}
}
Ok(())
}
pub fn next_tab(&mut self) {
self.selected_tab = self.selected_tab.next();
}
pub fn previous_tab(&mut self) {
self.selected_tab = self.selected_tab.previous();
}
pub fn quit(&mut self) {
self.state = AppState::Quitting;
}
} }
impl SelectedTab { impl SelectedTab {
@ -62,121 +116,135 @@ impl SelectedTab {
} }
} }
impl From<SelectedTab> for Line<'_> { impl Widget for &App {
/// Return enum name as a styled `Line` with two spaces both left and right. fn render(self, area: Rect, buf: &mut Buffer) {
fn from(value: SelectedTab) -> Self { use Constraint::*;
format!(" {value} ") let vertical = Layout::vertical([Length(1), Min(0), Length(1)]);
.fg(tailwind::SLATE.c200) let [header_area, inner_area, footer_area] = area.split(&vertical);
.bg(PALETTES[value as usize].c900)
.into()
}
}
struct App { let horizontal = Layout::horizontal([Min(0), Length(20)]);
pub selected_tab: SelectedTab, let [tabs_area, title_area] = header_area.split(&horizontal);
self.render_title(title_area, buf);
self.render_tabs(tabs_area, buf);
self.selected_tab.render(inner_area, buf);
self.render_footer(footer_area, buf);
}
} }
impl App { impl App {
fn new() -> App { fn render_title(&self, area: Rect, buf: &mut Buffer) {
App { "Ratatui Tabs Example".bold().render(area, buf);
selected_tab: SelectedTab::default(), }
fn render_tabs(&self, area: Rect, buf: &mut Buffer) {
let titles = SelectedTab::iter().map(|tab| tab.title());
let highlight_style = (Color::default(), self.selected_tab.palette().c700);
let selected_tab_index = self.selected_tab as usize;
Tabs::new(titles)
.highlight_style(highlight_style)
.select(selected_tab_index)
.padding("", "")
.divider(" ")
.render(area, buf);
}
fn render_footer(&self, area: Rect, buf: &mut Buffer) {
Line::raw("◄ ► to change tab | Press q to quit")
.centered()
.render(area, buf);
} }
} }
pub fn next(&mut self) { impl Widget for SelectedTab {
self.selected_tab = self.selected_tab.next(); fn render(self, area: Rect, buf: &mut Buffer) {
// in a real app these might be separate widgets
match self {
SelectedTab::Tab1 => self.render_tab0(area, buf),
SelectedTab::Tab2 => self.render_tab1(area, buf),
SelectedTab::Tab3 => self.render_tab2(area, buf),
SelectedTab::Tab4 => self.render_tab3(area, buf),
} }
pub fn previous(&mut self) {
self.selected_tab = self.selected_tab.previous();
} }
} }
fn main() -> Result<(), Box<dyn Error>> { impl SelectedTab {
// setup terminal /// Return tab's name as a styled `Line`
enable_raw_mode()?; fn title(&self) -> Line<'static> {
let mut stdout = io::stdout(); format!(" {self} ")
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; .fg(tailwind::SLATE.c200)
let backend = CrosstermBackend::new(stdout); .bg(self.palette().c900)
let mut terminal = Terminal::new(backend)?; .into()
// create app and run it
let app = App::new();
let res = run_app(&mut terminal, app);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
} }
fn render_tab0(&self, area: Rect, buf: &mut Buffer) {
Paragraph::new("Hello, World!")
.block(self.block())
.render(area, buf)
}
fn render_tab1(&self, area: Rect, buf: &mut Buffer) {
Paragraph::new("Welcome to the Ratatui tabs example!")
.block(self.block())
.render(area, buf)
}
fn render_tab2(&self, area: Rect, buf: &mut Buffer) {
Paragraph::new("Look! I'm different than others!")
.block(self.block())
.render(area, buf)
}
fn render_tab3(&self, area: Rect, buf: &mut Buffer) {
Paragraph::new("I know, these are some basic changes. But I think you got the main idea.")
.block(self.block())
.render(area, buf)
}
/// A block surrounding the tab's content
fn block(&self) -> Block<'static> {
Block::default()
.borders(Borders::ALL)
.border_set(symbols::border::PROPORTIONAL_TALL)
.padding(Padding::horizontal(1))
.border_style(self.palette().c700)
}
fn palette(&self) -> tailwind::Palette {
match self {
SelectedTab::Tab1 => tailwind::BLUE,
SelectedTab::Tab2 => tailwind::EMERALD,
SelectedTab::Tab3 => tailwind::INDIGO,
SelectedTab::Tab4 => tailwind::RED,
}
}
}
fn init_error_hooks() -> color_eyre::Result<()> {
let (panic, error) = HookBuilder::default().into_hooks();
let panic = panic.into_panic_hook();
let error = error.into_eyre_hook();
color_eyre::eyre::set_hook(Box::new(move |e| {
let _ = restore_terminal();
error(e)
}))?;
std::panic::set_hook(Box::new(move |info| {
let _ = restore_terminal();
panic(info)
}));
Ok(()) Ok(())
} }
fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> { fn init_terminal() -> color_eyre::Result<Terminal<impl Backend>> {
loop { enable_raw_mode()?;
terminal.draw(|f| ui(f, &app))?; stdout().execute(EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout());
if let Event::Key(key) = event::read()? { let terminal = Terminal::new(backend)?;
if key.kind == KeyEventKind::Press { Ok(terminal)
match key.code {
KeyCode::Char('q') | KeyCode::Esc => return Ok(()),
KeyCode::Char('l') | KeyCode::Right => app.next(),
KeyCode::Char('h') | KeyCode::Left => app.previous(),
_ => {}
}
}
}
}
} }
fn ui(f: &mut Frame, app: &App) { fn restore_terminal() -> color_eyre::Result<()> {
let area = f.size(); disable_raw_mode()?;
let vertical = Layout::vertical([Constraint::Length(4), Constraint::Min(3)]); stdout().execute(LeaveAlternateScreen)?;
let [tabs_area, inner_area] = area.split(&vertical); Ok(())
render_tabs(f, app, tabs_area);
render_inner(f, app, inner_area);
}
fn render_tabs(f: &mut Frame, app: &App, area: Rect) {
let block = Block::new()
.title("Tabs Example".bold())
.title("Use h l or ◄ ► to change tab")
.title_alignment(Alignment::Center)
.padding(Padding::top(1)); // padding to separate tabs from block title.
let selected_tab_index = app.selected_tab as usize;
// Gets tab titles from `SelectedTab::iter()`
let tabs = Tabs::new(SelectedTab::iter())
.block(block)
.highlight_style(
Style::new()
.bg(PALETTES[selected_tab_index].c600)
.underlined(),
)
.select(selected_tab_index)
.padding("", "")
.divider(" | ");
f.render_widget(tabs, area);
}
fn render_inner(f: &mut Frame, app: &App, area: Rect) {
let index = app.selected_tab as usize;
let inner_block = Block::default()
.title(format!("Inner {index}"))
.borders(Borders::ALL)
.border_type(BORDER_TYPES[index])
.border_style(Style::new().fg(PALETTES[index].c600));
f.render_widget(inner_block, area);
} }

View File

@ -3,13 +3,13 @@
Output "target/tabs.gif" Output "target/tabs.gif"
Set Theme "Aardvark Blue" Set Theme "Aardvark Blue"
Set Width 1200 Set Width 1200
Set Height 332 Set Height 368
Hide Hide
Type "cargo run --example=tabs --features=crossterm" Type "cargo run --example=tabs --features=crossterm"
Enter Enter
Sleep 1s Sleep 2s
Show Show
Sleep 1s Sleep 1s
Right@1s 3 Right@2.5s 3
Left@1s 3 Left@2.5s 3
Sleep 1s Sleep 2s