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] }
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 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 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 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()); 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 let popup_chunks = Layout::default()
189 .direction(Direction::Vertical)
190 .margin(1)
191 .constraints([
192 Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), ])
196 .split(main_area);
197
198 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 let button_chunks = Layout::default()
206 .direction(Direction::Horizontal)
207 .constraints([
208 Constraint::Min(1), Constraint::Length(6), Constraint::Length(2), Constraint::Length(6), Constraint::Min(1), ])
214 .split(popup_chunks[2]);
215
216 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 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 frame.render_widget(popup_block, main_area);
240
241 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()); 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 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}