//! # [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/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 ratatui::{ buffer::Buffer, crossterm::event::{self, Event, KeyCode, KeyEventKind}, layout::{Constraint, Layout, Rect}, style::{palette::tailwind, Color, Stylize}, symbols, text::Line, widgets::{Block, Padding, Paragraph, Tabs, Widget}, DefaultTerminal, }; use strum::{Display, EnumIter, FromRepr, IntoEnumIterator}; fn main() -> Result<()> { color_eyre::install()?; let terminal = ratatui::init(); let app_result = App::default().run(terminal); ratatui::restore(); app_result } #[derive(Default)] struct App { state: AppState, selected_tab: SelectedTab, } #[derive(Default, Clone, Copy, PartialEq, Eq)] enum AppState { #[default] Running, Quitting, } #[derive(Default, Clone, Copy, Display, FromRepr, EnumIter)] enum SelectedTab { #[default] #[strum(to_string = "Tab 1")] Tab1, #[strum(to_string = "Tab 2")] Tab2, #[strum(to_string = "Tab 3")] Tab3, #[strum(to_string = "Tab 4")] Tab4, } impl App { fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { while self.state == AppState::Running { terminal.draw(|frame| frame.render_widget(&self, frame.area()))?; self.handle_events()?; } Ok(()) } fn handle_events(&mut self) -> std::io::Result<()> { if let Event::Key(key) = event::read()? { if key.kind == KeyEventKind::Press { match key.code { KeyCode::Char('l') | KeyCode::Right => self.next_tab(), KeyCode::Char('h') | KeyCode::Left => self.previous_tab(), KeyCode::Char('q') | KeyCode::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 { /// 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) } } impl Widget for &App { fn render(self, area: Rect, buf: &mut Buffer) { use Constraint::{Length, Min}; let vertical = Layout::vertical([Length(1), Min(0), Length(1)]); let [header_area, inner_area, footer_area] = vertical.areas(area); let horizontal = Layout::horizontal([Min(0), Length(20)]); let [tabs_area, title_area] = horizontal.areas(header_area); render_title(title_area, buf); self.render_tabs(tabs_area, buf); self.selected_tab.render(inner_area, buf); render_footer(footer_area, buf); } } impl App { fn render_tabs(&self, area: Rect, buf: &mut Buffer) { let titles = SelectedTab::iter().map(SelectedTab::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_title(area: Rect, buf: &mut Buffer) { "Ratatui Tabs Example".bold().render(area, buf); } fn render_footer(area: Rect, buf: &mut Buffer) { Line::raw("◄ ► to change tab | Press q to quit") .centered() .render(area, buf); } impl Widget for SelectedTab { fn render(self, area: Rect, buf: &mut Buffer) { // in a real app these might be separate widgets match self { Self::Tab1 => self.render_tab0(area, buf), Self::Tab2 => self.render_tab1(area, buf), Self::Tab3 => self.render_tab2(area, buf), Self::Tab4 => self.render_tab3(area, buf), } } } 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() } 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::bordered() .border_set(symbols::border::PROPORTIONAL_TALL) .padding(Padding::horizontal(1)) .border_style(self.palette().c700) } const fn palette(self) -> tailwind::Palette { match self { Self::Tab1 => tailwind::BLUE, Self::Tab2 => tailwind::EMERALD, Self::Tab3 => tailwind::INDIGO, Self::Tab4 => tailwind::RED, } } }