2024-01-24 19:50:18 +00:00
|
|
|
//! # [Ratatui] Tabs 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-org/ratatui
|
|
|
|
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
|
|
|
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
|
|
|
|
2024-01-27 10:39:40 +00:00
|
|
|
use std::{io, io::stdout};
|
2023-06-12 05:07:15 +00:00
|
|
|
|
2024-01-27 10:39:40 +00:00
|
|
|
use color_eyre::{config::HookBuilder, Result};
|
2021-11-01 21:27:46 +00:00
|
|
|
use crossterm::{
|
2024-01-27 10:39:40 +00:00
|
|
|
event::{self, Event, KeyCode, KeyEventKind},
|
2021-11-01 21:27:46 +00:00
|
|
|
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
2024-01-27 10:39:40 +00:00
|
|
|
ExecutableCommand,
|
2020-03-13 01:02:51 +00:00
|
|
|
};
|
2024-01-21 09:23:50 +00:00
|
|
|
use ratatui::{prelude::*, style::palette::tailwind, widgets::*};
|
|
|
|
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
|
2018-09-23 18:59:51 +00:00
|
|
|
|
2024-01-27 10:39:40 +00:00
|
|
|
#[derive(Default)]
|
|
|
|
struct App {
|
|
|
|
state: AppState,
|
|
|
|
selected_tab: SelectedTab,
|
|
|
|
}
|
2024-01-21 09:23:50 +00:00
|
|
|
|
2024-01-27 10:39:40 +00:00
|
|
|
#[derive(Default, Clone, Copy, PartialEq, Eq)]
|
|
|
|
enum AppState {
|
|
|
|
#[default]
|
|
|
|
Running,
|
|
|
|
Quitting,
|
|
|
|
}
|
2024-01-21 09:23:50 +00:00
|
|
|
|
|
|
|
#[derive(Default, Clone, Copy, Display, FromRepr, EnumIter)]
|
|
|
|
enum SelectedTab {
|
|
|
|
#[default]
|
2024-01-27 10:39:40 +00:00
|
|
|
#[strum(to_string = "Tab 1")]
|
2024-01-21 09:23:50 +00:00
|
|
|
Tab1,
|
2024-01-27 10:39:40 +00:00
|
|
|
#[strum(to_string = "Tab 2")]
|
2024-01-21 09:23:50 +00:00
|
|
|
Tab2,
|
2024-01-27 10:39:40 +00:00
|
|
|
#[strum(to_string = "Tab 3")]
|
2024-01-21 09:23:50 +00:00
|
|
|
Tab3,
|
2024-01-27 10:39:40 +00:00
|
|
|
#[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;
|
|
|
|
}
|
2016-11-07 23:35:46 +00:00
|
|
|
}
|
|
|
|
|
2024-01-21 09:23:50 +00:00
|
|
|
impl SelectedTab {
|
|
|
|
/// Get the previous tab, if there is no previous tab return the current tab.
|
|
|
|
fn previous(&self) -> Self {
|
|
|
|
let current_index: usize = *self as usize;
|
|
|
|
let previous_index = current_index.saturating_sub(1);
|
|
|
|
Self::from_repr(previous_index).unwrap_or(*self)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Get the next tab, if there is no next tab return the current tab.
|
|
|
|
fn next(&self) -> Self {
|
|
|
|
let current_index = *self as usize;
|
|
|
|
let next_index = current_index.saturating_add(1);
|
|
|
|
Self::from_repr(next_index).unwrap_or(*self)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-27 10:39:40 +00:00
|
|
|
impl Widget for &App {
|
|
|
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
|
|
|
use Constraint::*;
|
|
|
|
let vertical = Layout::vertical([Length(1), Min(0), Length(1)]);
|
2024-02-02 04:26:35 +00:00
|
|
|
let [header_area, inner_area, footer_area] = vertical.areas(area);
|
2024-01-21 09:23:50 +00:00
|
|
|
|
2024-01-27 10:39:40 +00:00
|
|
|
let horizontal = Layout::horizontal([Min(0), Length(20)]);
|
2024-02-02 04:26:35 +00:00
|
|
|
let [tabs_area, title_area] = horizontal.areas(header_area);
|
2024-01-27 10:39:40 +00:00
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
2024-01-21 09:23:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl App {
|
2024-01-27 10:39:40 +00:00
|
|
|
fn render_title(&self, area: Rect, buf: &mut Buffer) {
|
|
|
|
"Ratatui Tabs Example".bold().render(area, buf);
|
2021-11-11 14:56:37 +00:00
|
|
|
}
|
|
|
|
|
2024-01-27 10:39:40 +00:00
|
|
|
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);
|
2021-11-11 14:56:37 +00:00
|
|
|
}
|
|
|
|
|
2024-01-27 10:39:40 +00:00
|
|
|
fn render_footer(&self, area: Rect, buf: &mut Buffer) {
|
|
|
|
Line::raw("◄ ► to change tab | Press q to quit")
|
|
|
|
.centered()
|
|
|
|
.render(area, buf);
|
2021-11-01 21:27:46 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-27 10:39:40 +00:00
|
|
|
impl Widget for SelectedTab {
|
|
|
|
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),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-09-23 18:59:51 +00:00
|
|
|
|
2024-01-27 10:39:40 +00:00
|
|
|
impl SelectedTab {
|
|
|
|
/// Return tab's name as a styled `Line`
|
|
|
|
fn title(&self) -> Line<'static> {
|
|
|
|
format!(" {self} ")
|
|
|
|
.fg(tailwind::SLATE.c200)
|
|
|
|
.bg(self.palette().c900)
|
|
|
|
.into()
|
|
|
|
}
|
2016-11-07 23:35:46 +00:00
|
|
|
|
2024-01-27 10:39:40 +00:00
|
|
|
fn render_tab0(&self, area: Rect, buf: &mut Buffer) {
|
|
|
|
Paragraph::new("Hello, World!")
|
|
|
|
.block(self.block())
|
|
|
|
.render(area, buf)
|
|
|
|
}
|
2016-11-07 23:35:46 +00:00
|
|
|
|
2024-01-27 10:39:40 +00:00
|
|
|
fn render_tab1(&self, area: Rect, buf: &mut Buffer) {
|
|
|
|
Paragraph::new("Welcome to the Ratatui tabs example!")
|
|
|
|
.block(self.block())
|
|
|
|
.render(area, buf)
|
2021-11-01 21:27:46 +00:00
|
|
|
}
|
|
|
|
|
2024-01-27 10:39:40 +00:00
|
|
|
fn render_tab2(&self, area: Rect, buf: &mut Buffer) {
|
|
|
|
Paragraph::new("Look! I'm different than others!")
|
|
|
|
.block(self.block())
|
|
|
|
.render(area, buf)
|
|
|
|
}
|
2016-11-07 23:35:46 +00:00
|
|
|
|
2024-01-27 10:39:40 +00:00
|
|
|
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)
|
|
|
|
}
|
2018-09-23 18:59:51 +00:00
|
|
|
|
2024-01-27 10:39:40 +00:00
|
|
|
/// 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,
|
2018-08-12 17:44:52 +00:00
|
|
|
}
|
2018-09-23 18:59:51 +00:00
|
|
|
}
|
2021-11-01 21:27:46 +00:00
|
|
|
}
|
|
|
|
|
2024-01-27 10:39:40 +00:00
|
|
|
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(())
|
2024-01-21 09:23:50 +00:00
|
|
|
}
|
|
|
|
|
2024-01-27 10:39:40 +00:00
|
|
|
fn init_terminal() -> color_eyre::Result<Terminal<impl Backend>> {
|
|
|
|
enable_raw_mode()?;
|
|
|
|
stdout().execute(EnterAlternateScreen)?;
|
|
|
|
let backend = CrosstermBackend::new(stdout());
|
|
|
|
let terminal = Terminal::new(backend)?;
|
|
|
|
Ok(terminal)
|
2024-01-21 09:23:50 +00:00
|
|
|
}
|
|
|
|
|
2024-01-27 10:39:40 +00:00
|
|
|
fn restore_terminal() -> color_eyre::Result<()> {
|
|
|
|
disable_raw_mode()?;
|
|
|
|
stdout().execute(LeaveAlternateScreen)?;
|
|
|
|
Ok(())
|
2016-11-07 23:35:46 +00:00
|
|
|
}
|