Skip to main content

time_rs/lib/
ui.rs

1use crate::lib::app;
2use crate::lib::app::{App, CurrentScreen, CurrentlyEditing};
3use crate::lib::throbber::Throbber;
4use ratatui::Frame;
5use ratatui::layout::{Constraint, Layout, Rect};
6use ratatui::prelude::Direction;
7use ratatui::style::{Color, Modifier, Style};
8use ratatui::text::{Line, Span, Text};
9use ratatui::widgets::{Block, Borders, Cell, Clear, Paragraph, Row, Table};
10
11fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
12    let popup_layout = Layout::default()
13        .direction(Direction::Vertical)
14        .constraints([
15            Constraint::Percentage((100 - percent_y) / 2),
16            Constraint::Percentage(percent_y),
17            Constraint::Percentage((100 - percent_y) / 2),
18        ])
19        .split(r);
20
21    Layout::default()
22        .direction(Direction::Horizontal)
23        .constraints([
24            Constraint::Percentage((100 - percent_x) / 2),
25            Constraint::Percentage(percent_x),
26            Constraint::Percentage((100 - percent_x) / 2),
27        ])
28        .split(popup_layout[1])[1] // Return the middle chunk
29}
30
31fn create_rows_with_subheaders(
32    timers: &Vec<app::Timer>,
33    throbber: &Throbber,
34) -> (Vec<Row<'static>>, Vec<bool>) {
35    let mut rows = Vec::new();
36    let mut selectable_rows = Vec::new();
37
38    if timers.is_empty() {
39        return (rows, selectable_rows);
40    }
41
42    let mut current_date = timers.first().unwrap().formatted_date();
43    rows.push(create_row_for_date(current_date.clone()));
44    selectable_rows.push(false);
45
46    for (i, timer) in timers.iter().enumerate() {
47        let is_last = i == timers.len() - 1;
48        if current_date != timer.formatted_date() {
49            current_date = timer.formatted_date();
50            rows.push(create_row_for_date(current_date.clone()));
51            selectable_rows.push(false);
52            rows.push(create_row_for_timer(timer, is_last, throbber));
53            selectable_rows.push(true);
54        } else {
55            rows.push(create_row_for_timer(timer, is_last, throbber));
56            selectable_rows.push(true);
57        }
58    }
59
60    (rows, selectable_rows)
61}
62
63fn create_row_for_date(date: String) -> Row<'static> {
64    Row::new(vec![
65        Cell::from(date),
66        Cell::from(""),
67        Cell::from(""),
68        Cell::from(""),
69    ])
70    .style(
71        Style::default()
72            .bg(Color::Gray)
73            .fg(Color::White)
74            .add_modifier(Modifier::BOLD),
75    )
76}
77
78fn create_row_for_timer(timer: &app::Timer, is_last: bool, throbber: &Throbber) -> Row<'static> {
79    Row::new(vec![
80        Cell::from(timer.name.clone()),
81        Cell::from(timer.description.clone()),
82        Cell::from(timer.formatted_duration().clone()),
83        Cell::from(if is_last {
84            Span::from(throbber.get_state_string().to_string() + " ")
85        } else {
86            Span::from("")
87        }),
88    ])
89}
90
91pub fn ui(frame: &mut Frame, app: &mut App) {
92    // Create the layout sections.
93    let chunks = Layout::default()
94        .direction(Direction::Vertical)
95        .constraints([
96            Constraint::Length(3),
97            Constraint::Min(1),
98            Constraint::Length(3),
99        ])
100        .split(frame.area());
101
102    let title_block = Block::default()
103        .borders(Borders::ALL)
104        .style(Style::default());
105
106    let title = Paragraph::new(Text::styled("Time.rs", Style::default())).block(title_block);
107
108    frame.render_widget(title, chunks[0]);
109
110    // render table in chunk[1]
111
112    let (rows, selectable_rows) = create_rows_with_subheaders(&app.timers, &app.throbber);
113    app.selectable_rows = selectable_rows;
114
115    let selected_row_style = Style::default().add_modifier(Modifier::REVERSED);
116
117    let table = Table::new(
118        rows,
119        &[
120            Constraint::Percentage(10),
121            Constraint::Fill(1),
122            Constraint::Length(8),
123            Constraint::Length(2),
124        ],
125    )
126    .header(
127        Row::new(vec![
128            Cell::from("Name"),
129            Cell::from("Description"),
130            Cell::from("Duration"),
131            Cell::from(""),
132        ])
133        .style(Style::default())
134        .bottom_margin(1),
135    )
136    .row_highlight_style(selected_row_style)
137    .block(
138        Block::default()
139            .borders(Borders::ALL)
140            .style(Style::default())
141            .title("Timers"),
142    );
143
144    frame.render_widget(Clear, chunks[1]);
145    frame.render_stateful_widget(table, chunks[1], &mut app.state);
146    // Footer
147
148    let current_keys_hint = {
149        match &app.current_screen {
150            CurrentScreen::Main => Span::styled(
151                "<space> Start/Stop timer | <Alt + i> Add timer | <e> Edit timer | <dd> Delete timer | <j> Down | <k> Up | <Esc> Exit",
152                Style::default(),
153            ),
154            CurrentScreen::Exit => Span::styled("<y> Yes | <n> No", Style::default()),
155            CurrentScreen::Add | CurrentScreen::Edit => {
156                Span::styled("<Tab> Next field | <Enter> Submit", Style::default())
157            }
158        }
159    };
160
161    let key_notes_footer =
162        Paragraph::new(Line::from(current_keys_hint)).block(Block::default().borders(Borders::ALL));
163
164    frame.render_widget(key_notes_footer, chunks[2]);
165
166    if let CurrentScreen::Exit = app.current_screen {
167        frame.render_widget(Clear, frame.area()); //this clears the entire screen and anything already drawn
168        let popup_block = Block::default()
169            .title("Exit?")
170            .borders(Borders::ALL)
171            .style(Style::default());
172
173        let main_area = Rect {
174            x: (frame.area().width.saturating_sub(40)) / 2,
175            y: (frame.area().height.saturating_sub(10)) / 2,
176            width: 40,
177            height: 7,
178        };
179
180        let help_area = Rect {
181            x: (frame.area().width.saturating_sub(40)) / 2,
182            y: (frame.area().height.saturating_sub(15)) / 2 + 9,
183            width: 40,
184            height: 3,
185        };
186
187        // Create layout for the popup content
188        let popup_chunks = Layout::default()
189            .direction(Direction::Vertical)
190            .margin(1)
191            .constraints([
192                Constraint::Length(1), // Question text
193                Constraint::Length(1), // Spacing
194                Constraint::Length(1), // Buttons
195            ])
196            .split(main_area);
197
198        // Question text
199        let question_text = Paragraph::new("Would you like to exit?")
200            .alignment(ratatui::layout::Alignment::Center)
201            .style(Style::default());
202        frame.render_widget(question_text, popup_chunks[0]);
203
204        // Button layout
205        let button_chunks = Layout::default()
206            .direction(Direction::Horizontal)
207            .constraints([
208                Constraint::Min(1),    // Left flexible spacing
209                Constraint::Length(6), // Fixed width for Yes button
210                Constraint::Length(2), // Fixed gap between buttons
211                Constraint::Length(6), // Fixed width for No button
212                Constraint::Min(1),    // Right flexible spacing
213            ])
214            .split(popup_chunks[2]);
215
216        // Yes button
217        let yes_style = if app.exit_button_selected {
218            Style::default().add_modifier(Modifier::REVERSED)
219        } else {
220            Style::default()
221        };
222        let yes_button = Paragraph::new("[Y]es")
223            .alignment(ratatui::layout::Alignment::Center)
224            .style(yes_style);
225        frame.render_widget(yes_button, button_chunks[1]);
226
227        // No button
228        let no_style = if !app.exit_button_selected {
229            Style::default().add_modifier(Modifier::REVERSED)
230        } else {
231            Style::default()
232        };
233        let no_button = Paragraph::new("[N]o")
234            .alignment(ratatui::layout::Alignment::Center)
235            .style(no_style);
236        frame.render_widget(no_button, button_chunks[3]);
237
238        // Render the popup block border
239        frame.render_widget(popup_block, main_area);
240
241        // Create help box below main popup
242        let help_block = Block::default()
243            .borders(Borders::ALL)
244            .style(Style::default());
245
246        let help_text = Paragraph::new("<Tab> Switch | <Enter> Confirm")
247            .alignment(ratatui::layout::Alignment::Center)
248            .block(help_block)
249            .style(Style::default());
250
251        frame.render_widget(help_text, help_area);
252    }
253
254    if let CurrentScreen::Edit | CurrentScreen::Add = app.current_screen {
255        frame.render_widget(Clear, frame.area()); //this clears the entire screen and anything already drawn
256        let popup_block = Block::default()
257            .borders(Borders::NONE)
258            .style(Style::default());
259
260        let area = centered_rect(60, 35, frame.area());
261        frame.render_widget(popup_block, area);
262
263        // Split the area into main content and help box
264        let main_chunks = Layout::default()
265            .direction(Direction::Vertical)
266            .margin(1)
267            .constraints([Constraint::Percentage(70), Constraint::Length(3)])
268            .split(area);
269
270        let popup_chunks = Layout::default()
271            .direction(Direction::Horizontal)
272            .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
273            .split(main_chunks[0]);
274
275        let mut name_block = Block::default().title("Name").borders(Borders::ALL);
276        let mut desc_block = Block::default().title("Description").borders(Borders::ALL);
277
278        let active_style = Style::default().add_modifier(Modifier::REVERSED);
279
280        match app.currently_editing {
281            Some(CurrentlyEditing::Name) => name_block = name_block.style(active_style),
282            Some(CurrentlyEditing::Description) => desc_block = desc_block.style(active_style),
283            None => {
284                name_block = name_block.style(active_style);
285            }
286        };
287
288        let key_text = Paragraph::new(app.name_input.clone()).block(name_block);
289        frame.render_widget(key_text, popup_chunks[0]);
290
291        let value_text = Paragraph::new(app.description_input.clone()).block(desc_block);
292        frame.render_widget(value_text, popup_chunks[1]);
293
294        let help_block = Block::default()
295            .borders(Borders::ALL)
296            .style(Style::default());
297
298        let help_paragraph =
299            Paragraph::new("<Enter> Save | <Tab> Switch field | <Esc> Back").block(help_block);
300        frame.render_widget(help_paragraph, main_chunks[1]);
301    }
302}