demo2/
app.rs

1use std::time::Duration;
2
3use color_eyre::{eyre::Context, Result};
4use crossterm::event;
5use itertools::Itertools;
6use ratatui::{
7    buffer::Buffer,
8    crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind},
9    layout::{Constraint, Layout, Rect},
10    style::Color,
11    text::{Line, Span},
12    widgets::{Block, Tabs, Widget},
13    DefaultTerminal, Frame,
14};
15use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
16
17use crate::{
18    destroy,
19    tabs::{AboutTab, EmailTab, RecipeTab, TracerouteTab, WeatherTab},
20    THEME,
21};
22
23#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
24pub struct App {
25    mode: Mode,
26    tab: Tab,
27    about_tab: AboutTab,
28    recipe_tab: RecipeTab,
29    email_tab: EmailTab,
30    traceroute_tab: TracerouteTab,
31    weather_tab: WeatherTab,
32}
33
34#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
35enum Mode {
36    #[default]
37    Running,
38    Destroy,
39    Quit,
40}
41
42#[derive(Debug, Clone, Copy, Default, Display, EnumIter, FromRepr, PartialEq, Eq)]
43enum Tab {
44    #[default]
45    About,
46    Recipe,
47    Email,
48    Traceroute,
49    Weather,
50}
51
52impl App {
53    /// Run the app until the user quits.
54    pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
55        while self.is_running() {
56            terminal
57                .draw(|frame| self.draw(frame))
58                .wrap_err("terminal.draw")?;
59            self.handle_events()?;
60        }
61        Ok(())
62    }
63
64    fn is_running(&self) -> bool {
65        self.mode != Mode::Quit
66    }
67
68    /// Draw a single frame of the app.
69    fn draw(&self, frame: &mut Frame) {
70        frame.render_widget(self, frame.area());
71        if self.mode == Mode::Destroy {
72            destroy::destroy(frame);
73        }
74    }
75
76    /// Handle events from the terminal.
77    ///
78    /// This function is called once per frame, The events are polled from the stdin with timeout of
79    /// 1/50th of a second. This was chosen to try to match the default frame rate of a GIF in VHS.
80    fn handle_events(&mut self) -> Result<()> {
81        let timeout = Duration::from_secs_f64(1.0 / 50.0);
82        if !event::poll(timeout)? {
83            return Ok(());
84        }
85        match event::read()? {
86            Event::Key(key) if key.kind == KeyEventKind::Press => self.handle_key_press(key),
87            _ => {}
88        }
89        Ok(())
90    }
91
92    fn handle_key_press(&mut self, key: KeyEvent) {
93        match key.code {
94            KeyCode::Char('q') | KeyCode::Esc => self.mode = Mode::Quit,
95            KeyCode::Char('h') | KeyCode::Left => self.prev_tab(),
96            KeyCode::Char('l') | KeyCode::Right => self.next_tab(),
97            KeyCode::Char('k') | KeyCode::Up => self.prev(),
98            KeyCode::Char('j') | KeyCode::Down => self.next(),
99            KeyCode::Char('d') | KeyCode::Delete => self.destroy(),
100            _ => {}
101        };
102    }
103
104    fn prev(&mut self) {
105        match self.tab {
106            Tab::About => self.about_tab.prev_row(),
107            Tab::Recipe => self.recipe_tab.prev(),
108            Tab::Email => self.email_tab.prev(),
109            Tab::Traceroute => self.traceroute_tab.prev_row(),
110            Tab::Weather => self.weather_tab.prev(),
111        }
112    }
113
114    fn next(&mut self) {
115        match self.tab {
116            Tab::About => self.about_tab.next_row(),
117            Tab::Recipe => self.recipe_tab.next(),
118            Tab::Email => self.email_tab.next(),
119            Tab::Traceroute => self.traceroute_tab.next_row(),
120            Tab::Weather => self.weather_tab.next(),
121        }
122    }
123
124    fn prev_tab(&mut self) {
125        self.tab = self.tab.prev();
126    }
127
128    fn next_tab(&mut self) {
129        self.tab = self.tab.next();
130    }
131
132    fn destroy(&mut self) {
133        self.mode = Mode::Destroy;
134    }
135}
136
137/// Implement Widget for &App rather than for App as we would otherwise have to clone or copy the
138/// entire app state on every frame. For this example, the app state is small enough that it doesn't
139/// matter, but for larger apps this can be a significant performance improvement.
140impl Widget for &App {
141    fn render(self, area: Rect, buf: &mut Buffer) {
142        let vertical = Layout::vertical([
143            Constraint::Length(1),
144            Constraint::Min(0),
145            Constraint::Length(1),
146        ]);
147        let [title_bar, tab, bottom_bar] = vertical.areas(area);
148
149        Block::new().style(THEME.root).render(area, buf);
150        self.render_title_bar(title_bar, buf);
151        self.render_selected_tab(tab, buf);
152        App::render_bottom_bar(bottom_bar, buf);
153    }
154}
155
156impl App {
157    fn render_title_bar(&self, area: Rect, buf: &mut Buffer) {
158        let layout = Layout::horizontal([Constraint::Min(0), Constraint::Length(43)]);
159        let [title, tabs] = layout.areas(area);
160
161        Span::styled("Ratatui", THEME.app_title).render(title, buf);
162        let titles = Tab::iter().map(Tab::title);
163        Tabs::new(titles)
164            .style(THEME.tabs)
165            .highlight_style(THEME.tabs_selected)
166            .select(self.tab as usize)
167            .divider("")
168            .padding("", "")
169            .render(tabs, buf);
170    }
171
172    fn render_selected_tab(&self, area: Rect, buf: &mut Buffer) {
173        match self.tab {
174            Tab::About => self.about_tab.render(area, buf),
175            Tab::Recipe => self.recipe_tab.render(area, buf),
176            Tab::Email => self.email_tab.render(area, buf),
177            Tab::Traceroute => self.traceroute_tab.render(area, buf),
178            Tab::Weather => self.weather_tab.render(area, buf),
179        };
180    }
181
182    fn render_bottom_bar(area: Rect, buf: &mut Buffer) {
183        let keys = [
184            ("H/←", "Left"),
185            ("L/→", "Right"),
186            ("K/↑", "Up"),
187            ("J/↓", "Down"),
188            ("D/Del", "Destroy"),
189            ("Q/Esc", "Quit"),
190        ];
191        let spans = keys
192            .iter()
193            .flat_map(|(key, desc)| {
194                let key = Span::styled(format!(" {key} "), THEME.key_binding.key);
195                let desc = Span::styled(format!(" {desc} "), THEME.key_binding.description);
196                [key, desc]
197            })
198            .collect_vec();
199        Line::from(spans)
200            .centered()
201            .style((Color::Indexed(236), Color::Indexed(232)))
202            .render(area, buf);
203    }
204}
205
206impl Tab {
207    fn next(self) -> Self {
208        let current_index = self as usize;
209        let next_index = current_index.saturating_add(1);
210        Self::from_repr(next_index).unwrap_or(self)
211    }
212
213    fn prev(self) -> Self {
214        let current_index = self as usize;
215        let prev_index = current_index.saturating_sub(1);
216        Self::from_repr(prev_index).unwrap_or(self)
217    }
218
219    fn title(self) -> String {
220        match self {
221            Self::About => String::new(),
222            tab => format!(" {tab} "),
223        }
224    }
225}