[go: up one dir, main page]

tabs/
tabs.rs

1//! # [Ratatui] Tabs example
2//!
3//! The latest version of this example is available in the [examples] folder in the repository.
4//!
5//! Please note that the examples are designed to be run against the `main` branch of the Github
6//! repository. This means that you may not be able to compile with the latest release version on
7//! crates.io, or the one that you have installed locally.
8//!
9//! See the [examples readme] for more information on finding examples that match the version of the
10//! library you are using.
11//!
12//! [Ratatui]: https://github.com/ratatui/ratatui
13//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
14//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
15
16use color_eyre::Result;
17use ratatui::{
18    buffer::Buffer,
19    crossterm::event::{self, Event, KeyCode, KeyEventKind},
20    layout::{Constraint, Layout, Rect},
21    style::{palette::tailwind, Color, Stylize},
22    symbols,
23    text::Line,
24    widgets::{Block, Padding, Paragraph, Tabs, Widget},
25    DefaultTerminal,
26};
27use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
28
29fn main() -> Result<()> {
30    color_eyre::install()?;
31    let terminal = ratatui::init();
32    let app_result = App::default().run(terminal);
33    ratatui::restore();
34    app_result
35}
36
37#[derive(Default)]
38struct App {
39    state: AppState,
40    selected_tab: SelectedTab,
41}
42
43#[derive(Default, Clone, Copy, PartialEq, Eq)]
44enum AppState {
45    #[default]
46    Running,
47    Quitting,
48}
49
50#[derive(Default, Clone, Copy, Display, FromRepr, EnumIter)]
51enum SelectedTab {
52    #[default]
53    #[strum(to_string = "Tab 1")]
54    Tab1,
55    #[strum(to_string = "Tab 2")]
56    Tab2,
57    #[strum(to_string = "Tab 3")]
58    Tab3,
59    #[strum(to_string = "Tab 4")]
60    Tab4,
61}
62
63impl App {
64    fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
65        while self.state == AppState::Running {
66            terminal.draw(|frame| frame.render_widget(&self, frame.area()))?;
67            self.handle_events()?;
68        }
69        Ok(())
70    }
71
72    fn handle_events(&mut self) -> std::io::Result<()> {
73        if let Event::Key(key) = event::read()? {
74            if key.kind == KeyEventKind::Press {
75                match key.code {
76                    KeyCode::Char('l') | KeyCode::Right => self.next_tab(),
77                    KeyCode::Char('h') | KeyCode::Left => self.previous_tab(),
78                    KeyCode::Char('q') | KeyCode::Esc => self.quit(),
79                    _ => {}
80                }
81            }
82        }
83        Ok(())
84    }
85
86    pub fn next_tab(&mut self) {
87        self.selected_tab = self.selected_tab.next();
88    }
89
90    pub fn previous_tab(&mut self) {
91        self.selected_tab = self.selected_tab.previous();
92    }
93
94    pub fn quit(&mut self) {
95        self.state = AppState::Quitting;
96    }
97}
98
99impl SelectedTab {
100    /// Get the previous tab, if there is no previous tab return the current tab.
101    fn previous(self) -> Self {
102        let current_index: usize = self as usize;
103        let previous_index = current_index.saturating_sub(1);
104        Self::from_repr(previous_index).unwrap_or(self)
105    }
106
107    /// Get the next tab, if there is no next tab return the current tab.
108    fn next(self) -> Self {
109        let current_index = self as usize;
110        let next_index = current_index.saturating_add(1);
111        Self::from_repr(next_index).unwrap_or(self)
112    }
113}
114
115impl Widget for &App {
116    fn render(self, area: Rect, buf: &mut Buffer) {
117        use Constraint::{Length, Min};
118        let vertical = Layout::vertical([Length(1), Min(0), Length(1)]);
119        let [header_area, inner_area, footer_area] = vertical.areas(area);
120
121        let horizontal = Layout::horizontal([Min(0), Length(20)]);
122        let [tabs_area, title_area] = horizontal.areas(header_area);
123
124        render_title(title_area, buf);
125        self.render_tabs(tabs_area, buf);
126        self.selected_tab.render(inner_area, buf);
127        render_footer(footer_area, buf);
128    }
129}
130
131impl App {
132    fn render_tabs(&self, area: Rect, buf: &mut Buffer) {
133        let titles = SelectedTab::iter().map(SelectedTab::title);
134        let highlight_style = (Color::default(), self.selected_tab.palette().c700);
135        let selected_tab_index = self.selected_tab as usize;
136        Tabs::new(titles)
137            .highlight_style(highlight_style)
138            .select(selected_tab_index)
139            .padding("", "")
140            .divider(" ")
141            .render(area, buf);
142    }
143}
144
145fn render_title(area: Rect, buf: &mut Buffer) {
146    "Ratatui Tabs Example".bold().render(area, buf);
147}
148
149fn render_footer(area: Rect, buf: &mut Buffer) {
150    Line::raw("◄ ► to change tab | Press q to quit")
151        .centered()
152        .render(area, buf);
153}
154
155impl Widget for SelectedTab {
156    fn render(self, area: Rect, buf: &mut Buffer) {
157        // in a real app these might be separate widgets
158        match self {
159            Self::Tab1 => self.render_tab0(area, buf),
160            Self::Tab2 => self.render_tab1(area, buf),
161            Self::Tab3 => self.render_tab2(area, buf),
162            Self::Tab4 => self.render_tab3(area, buf),
163        }
164    }
165}
166
167impl SelectedTab {
168    /// Return tab's name as a styled `Line`
169    fn title(self) -> Line<'static> {
170        format!("  {self}  ")
171            .fg(tailwind::SLATE.c200)
172            .bg(self.palette().c900)
173            .into()
174    }
175
176    fn render_tab0(self, area: Rect, buf: &mut Buffer) {
177        Paragraph::new("Hello, World!")
178            .block(self.block())
179            .render(area, buf);
180    }
181
182    fn render_tab1(self, area: Rect, buf: &mut Buffer) {
183        Paragraph::new("Welcome to the Ratatui tabs example!")
184            .block(self.block())
185            .render(area, buf);
186    }
187
188    fn render_tab2(self, area: Rect, buf: &mut Buffer) {
189        Paragraph::new("Look! I'm different than others!")
190            .block(self.block())
191            .render(area, buf);
192    }
193
194    fn render_tab3(self, area: Rect, buf: &mut Buffer) {
195        Paragraph::new("I know, these are some basic changes. But I think you got the main idea.")
196            .block(self.block())
197            .render(area, buf);
198    }
199
200    /// A block surrounding the tab's content
201    fn block(self) -> Block<'static> {
202        Block::bordered()
203            .border_set(symbols::border::PROPORTIONAL_TALL)
204            .padding(Padding::horizontal(1))
205            .border_style(self.palette().c700)
206    }
207
208    const fn palette(self) -> tailwind::Palette {
209        match self {
210            Self::Tab1 => tailwind::BLUE,
211            Self::Tab2 => tailwind::EMERALD,
212            Self::Tab3 => tailwind::INDIGO,
213            Self::Tab4 => tailwind::RED,
214        }
215    }
216}