1use 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 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 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 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 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 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}