1use crate::{
2 app::{
3 state::{AppStatus, Focus, KeyBindingEnum},
4 App,
5 },
6 constants::{HIDDEN_PASSWORD_SYMBOL, MIN_TIME_BETWEEN_SENDING_RESET_LINK},
7 ui::{
8 rendering::{
9 common::{
10 draw_crab_pattern, draw_title, render_blank_styled_canvas_with_margin,
11 render_close_button,
12 },
13 utils::{
14 calculate_viewport_corrected_cursor_position, centered_rect_with_length,
15 check_if_active_and_get_style, check_if_mouse_is_in_area,
16 get_mouse_focusable_field_style,
17 },
18 view::ResetPassword,
19 },
20 Renderable,
21 },
22};
23use ratatui::{
24 layout::{Alignment, Constraint, Direction, Layout},
25 text::{Line, Span},
26 widgets::{Block, BorderType, Borders, Clear, Paragraph},
27 Frame,
28};
29use std::time::Duration;
30
31impl Renderable for ResetPassword {
32 fn render(rect: &mut Frame, app: &mut App, is_active: bool) {
33 if is_active {
34 if app.state.focus == Focus::EmailIDField
35 || app.state.focus == Focus::ResetPasswordLinkField
36 || app.state.focus == Focus::PasswordField
37 || app.state.focus == Focus::ConfirmPasswordField
38 {
39 if app.state.app_status != AppStatus::UserInput {
40 app.state.app_status = AppStatus::UserInput;
41 }
42 } else if app.state.app_status != AppStatus::Initialized {
43 app.state.app_status = AppStatus::Initialized;
44 }
45 }
46
47 let main_chunks = Layout::default()
48 .direction(Direction::Vertical)
49 .constraints([Constraint::Length(3), Constraint::Fill(1)].as_ref())
50 .split(rect.area());
51
52 let chunks = Layout::default()
53 .direction(Direction::Horizontal)
54 .constraints([
55 Constraint::Fill(1),
56 Constraint::Length(2),
57 Constraint::Length(50),
58 ])
59 .split(main_chunks[1]);
60
61 let info_box = centered_rect_with_length(54, 13, chunks[0]);
62
63 let info_chunks = Layout::default()
64 .direction(Direction::Vertical)
65 .constraints(
66 [
67 Constraint::Length(1),
68 Constraint::Length(1),
69 Constraint::Fill(1),
70 ]
71 .as_ref(),
72 )
73 .margin(1)
74 .split(info_box);
75
76 let form_chunks = Layout::default()
77 .direction(Direction::Vertical)
78 .constraints([
79 Constraint::Length((chunks[2].height - 24) / 2),
80 Constraint::Length(3),
81 Constraint::Length(3),
82 Constraint::Length(1),
83 Constraint::Length(3),
84 Constraint::Length(3),
85 Constraint::Length(3),
86 Constraint::Length(3),
87 Constraint::Length(3),
88 Constraint::Length((chunks[2].height - 24) / 2),
89 ])
90 .margin(1)
91 .split(chunks[2]);
92
93 let email_id_chunk = form_chunks[1];
94 let send_reset_link_button_chunk = form_chunks[2];
95 let reset_link_chunk = form_chunks[4];
96 let new_password_chunk = form_chunks[5];
97 let confirm_new_password_chunk = form_chunks[6];
98 let show_password_main_chunk = form_chunks[7];
99 let submit_button_chunk = form_chunks[8];
100
101 let show_password_chunks = Layout::default()
102 .direction(Direction::Horizontal)
103 .constraints([
104 Constraint::Length(show_password_main_chunk.width - 7),
105 Constraint::Length(5),
106 ])
107 .margin(1)
108 .split(show_password_main_chunk);
109
110 let submit_button_chunks = Layout::default()
111 .direction(Direction::Horizontal)
112 .constraints([
113 Constraint::Length((submit_button_chunk.width - 12) / 2),
114 Constraint::Length(12),
115 Constraint::Length((submit_button_chunk.width - 12) / 2),
116 ])
117 .split(submit_button_chunk);
118
119 let email_id_field_style = get_mouse_focusable_field_style(
120 app,
121 Focus::EmailIDField,
122 &email_id_chunk,
123 is_active,
124 true,
125 );
126
127 let send_reset_link_button_style = if !is_active {
128 app.current_theme.inactive_text_style
129 } else if let Some(last_reset_password_link_sent_time) =
130 app.state.last_reset_password_link_sent_time
131 {
132 if last_reset_password_link_sent_time.elapsed()
133 < Duration::from_secs(MIN_TIME_BETWEEN_SENDING_RESET_LINK)
134 {
135 app.current_theme.inactive_text_style
136 } else if check_if_mouse_is_in_area(
137 &app.state.current_mouse_coordinates,
138 &send_reset_link_button_chunk,
139 ) {
140 if app.state.mouse_focus != Some(Focus::SendResetPasswordLinkButton) {
141 app.state.app_status = AppStatus::Initialized;
142 } else {
143 app.state.app_status = AppStatus::UserInput;
144 }
145 app.state.mouse_focus = Some(Focus::SendResetPasswordLinkButton);
146 app.state.set_focus(Focus::SendResetPasswordLinkButton);
147 app.current_theme.mouse_focus_style
148 } else if app.state.focus == Focus::SendResetPasswordLinkButton {
149 app.current_theme.keyboard_focus_style
150 } else {
151 app.current_theme.general_style
152 }
153 } else if check_if_mouse_is_in_area(
154 &app.state.current_mouse_coordinates,
155 &send_reset_link_button_chunk,
156 ) {
157 if app.state.mouse_focus != Some(Focus::SendResetPasswordLinkButton) {
158 app.state.app_status = AppStatus::Initialized;
159 } else {
160 app.state.app_status = AppStatus::UserInput;
161 }
162 app.state.mouse_focus = Some(Focus::SendResetPasswordLinkButton);
163 app.state.set_focus(Focus::SendResetPasswordLinkButton);
164 app.current_theme.mouse_focus_style
165 } else if app.state.focus == Focus::SendResetPasswordLinkButton {
166 app.current_theme.keyboard_focus_style
167 } else {
168 app.current_theme.general_style
169 };
170
171 let separator_style = check_if_active_and_get_style(
172 is_active,
173 app.current_theme.inactive_text_style,
174 app.current_theme.general_style,
175 );
176
177 let reset_link_field_style = get_mouse_focusable_field_style(
178 app,
179 Focus::ResetPasswordLinkField,
180 &reset_link_chunk,
181 is_active,
182 true,
183 );
184
185 let new_password_field_style = get_mouse_focusable_field_style(
186 app,
187 Focus::PasswordField,
188 &new_password_chunk,
189 is_active,
190 true,
191 );
192
193 let confirm_new_password_field_style = get_mouse_focusable_field_style(
194 app,
195 Focus::ConfirmPasswordField,
196 &confirm_new_password_chunk,
197 is_active,
198 true,
199 );
200
201 let show_password_style = get_mouse_focusable_field_style(
202 app,
203 Focus::ExtraFocus,
204 &show_password_main_chunk,
205 is_active,
206 false,
207 );
208
209 let submit_button_style = get_mouse_focusable_field_style(
210 app,
211 Focus::SubmitButton,
212 &submit_button_chunks[1],
213 is_active,
214 false,
215 );
216
217 let general_style = check_if_active_and_get_style(
218 is_active,
219 app.current_theme.inactive_text_style,
220 app.current_theme.general_style,
221 );
222 let help_key_style = check_if_active_and_get_style(
223 is_active,
224 app.current_theme.inactive_text_style,
225 app.current_theme.help_key_style,
226 );
227 let help_text_style = check_if_active_and_get_style(
228 is_active,
229 app.current_theme.inactive_text_style,
230 app.current_theme.help_text_style,
231 );
232
233 let crab_paragraph = draw_crab_pattern(
234 chunks[0],
235 app.current_theme.inactive_text_style,
236 is_active,
237 app.config.disable_animations,
238 );
239
240 let info_border = Block::default()
241 .borders(Borders::ALL)
242 .border_type(BorderType::Rounded)
243 .border_style(separator_style);
244
245 let info_header = Paragraph::new("Reset Password")
246 .style(general_style)
247 .block(Block::default())
248 .alignment(Alignment::Center);
249
250 let accept_key = app
251 .get_first_keybinding(KeyBindingEnum::Accept)
252 .unwrap_or("".to_string());
253 let next_focus_key = app
254 .get_first_keybinding(KeyBindingEnum::NextFocus)
255 .unwrap_or("".to_string());
256 let prv_focus_key = app
257 .get_first_keybinding(KeyBindingEnum::PrvFocus)
258 .unwrap_or("".to_string());
259
260 let help_lines = vec![
261 Line::from(Span::styled(
262 "1) Enter your email and send reset link first.",
263 help_text_style,
264 )),
265 Line::from(Span::styled(
266 "2) Copy the reset link from your email and then paste the reset link.",
267 help_text_style,
268 )),
269 Line::from(Span::styled(
270 "3) Enter new password and confirm the new password.",
271 help_text_style,
272 )),
273 Line::from(""),
274 Line::from(Span::styled(
275 "### Check Spam folder if you don't see the email ###",
276 help_text_style,
277 )),
278 Line::from(""),
279 Line::from(vec![
280 Span::styled("Press ", help_text_style),
281 Span::styled(next_focus_key, help_key_style),
282 Span::styled(" or ", help_text_style),
283 Span::styled(prv_focus_key, help_key_style),
284 Span::styled(" to change focus. Press ", help_text_style),
285 Span::styled(accept_key, help_key_style),
286 Span::styled(" to submit.", help_text_style),
287 ]),
288 ];
289
290 let help_paragraph = Paragraph::new(help_lines)
291 .style(general_style)
292 .block(Block::default())
293 .wrap(ratatui::widgets::Wrap { trim: true });
294
295 let separator = Block::default()
296 .borders(Borders::ALL)
297 .border_type(BorderType::Rounded)
298 .border_style(separator_style);
299
300 let send_reset_link_button_text = if let Some(last_reset_password_link_sent_time) =
301 app.state.last_reset_password_link_sent_time
302 {
303 if last_reset_password_link_sent_time.elapsed()
304 < Duration::from_secs(MIN_TIME_BETWEEN_SENDING_RESET_LINK)
305 {
306 let remaining_time = Duration::from_secs(MIN_TIME_BETWEEN_SENDING_RESET_LINK)
307 .checked_sub(last_reset_password_link_sent_time.elapsed())
308 .unwrap();
309 format!("Please wait for {} seconds", remaining_time.as_secs())
310 } else {
311 "Send Reset Link".to_string()
312 }
313 } else {
314 "Send Reset Link".to_string()
315 };
316
317 let email_id_block = Block::default()
318 .style(email_id_field_style)
319 .borders(Borders::ALL)
320 .border_type(BorderType::Rounded);
321
322 let send_reset_link_button = Paragraph::new(send_reset_link_button_text)
323 .style(send_reset_link_button_style)
324 .block(
325 Block::default()
326 .borders(Borders::ALL)
327 .border_type(BorderType::Rounded),
328 )
329 .alignment(Alignment::Center);
330
331 let reset_link_block = Block::default()
332 .style(reset_link_field_style)
333 .borders(Borders::ALL)
334 .border_type(BorderType::Rounded);
335
336 let new_password_block = Block::default()
337 .style(new_password_field_style)
338 .borders(Borders::ALL)
339 .border_type(BorderType::Rounded);
340
341 let confirm_new_password_block = Block::default()
342 .style(confirm_new_password_field_style)
343 .borders(Borders::ALL)
344 .border_type(BorderType::Rounded);
345
346 app.state
347 .text_buffers
348 .email_id
349 .set_placeholder_text("Email ID");
350
351 app.state.text_buffers.email_id.set_block(email_id_block);
352
353 app.state
354 .text_buffers
355 .reset_password_link
356 .set_placeholder_text("Reset Link");
357
358 app.state
359 .text_buffers
360 .reset_password_link
361 .set_block(reset_link_block);
362
363 app.state
364 .text_buffers
365 .password
366 .set_placeholder_text("New Password");
367
368 app.state
369 .text_buffers
370 .password
371 .set_block(new_password_block);
372
373 app.state
374 .text_buffers
375 .confirm_password
376 .set_placeholder_text("Confirm New Password");
377
378 app.state
379 .text_buffers
380 .confirm_password
381 .set_block(confirm_new_password_block);
382
383 let show_password_paragraph = Paragraph::new("Show Password")
384 .style(show_password_style)
385 .block(Block::default())
386 .alignment(Alignment::Right);
387
388 let show_password_checkbox_value = if app.state.show_password {
389 "[X]"
390 } else {
391 "[ ]"
392 };
393
394 let show_password_checkbox_paragraph = Paragraph::new(show_password_checkbox_value)
395 .style(show_password_style)
396 .block(Block::default())
397 .alignment(Alignment::Center);
398
399 let submit_button = Paragraph::new("Submit")
400 .style(submit_button_style)
401 .block(
402 Block::default()
403 .borders(Borders::ALL)
405 .border_type(BorderType::Rounded),
406 )
407 .alignment(Alignment::Center);
408
409 rect.render_widget(draw_title(app, main_chunks[0], is_active), main_chunks[0]);
410 rect.render_widget(crab_paragraph, chunks[0]);
411 rect.render_widget(Clear, info_box);
412 render_blank_styled_canvas_with_margin(rect, app, info_box, is_active, -1);
413 rect.render_widget(info_border, info_box);
414 rect.render_widget(info_header, info_chunks[0]);
415 rect.render_widget(help_paragraph, info_chunks[2]);
416 rect.render_widget(separator, chunks[1]);
417 rect.render_widget(app.state.text_buffers.email_id.widget(), email_id_chunk);
418 rect.render_widget(send_reset_link_button, send_reset_link_button_chunk);
419 rect.render_widget(
420 app.state.text_buffers.reset_password_link.widget(),
421 reset_link_chunk,
422 );
423 if app.state.show_password {
424 rect.render_widget(app.state.text_buffers.password.widget(), new_password_chunk);
425 rect.render_widget(
426 app.state.text_buffers.confirm_password.widget(),
427 confirm_new_password_chunk,
428 );
429 } else {
430 if app.state.text_buffers.password.is_empty() {
431 rect.render_widget(app.state.text_buffers.password.widget(), new_password_chunk);
432 } else {
433 let hidden_text = HIDDEN_PASSWORD_SYMBOL
434 .to_string()
435 .repeat(app.state.text_buffers.password.get_joined_lines().len());
436 let hidden_paragraph = Paragraph::new(hidden_text)
437 .style(new_password_field_style)
438 .block(
439 Block::default()
440 .borders(Borders::ALL)
441 .border_type(BorderType::Rounded),
442 );
443 rect.render_widget(hidden_paragraph, new_password_chunk);
444 }
445 if app.state.text_buffers.confirm_password.is_empty() {
446 rect.render_widget(
447 app.state.text_buffers.confirm_password.widget(),
448 confirm_new_password_chunk,
449 );
450 } else {
451 let hidden_text = HIDDEN_PASSWORD_SYMBOL.to_string().repeat(
452 app.state
453 .text_buffers
454 .confirm_password
455 .get_joined_lines()
456 .len(),
457 );
458 let hidden_paragraph = Paragraph::new(hidden_text)
459 .style(confirm_new_password_field_style)
460 .block(
461 Block::default()
462 .borders(Borders::ALL)
463 .border_type(BorderType::Rounded),
464 );
465 rect.render_widget(hidden_paragraph, confirm_new_password_chunk);
466 }
467 }
468 rect.render_widget(show_password_paragraph, show_password_chunks[0]);
469 rect.render_widget(show_password_checkbox_paragraph, show_password_chunks[1]);
470 rect.render_widget(submit_button, submit_button_chunks[1]);
471 if app.config.enable_mouse_support {
472 render_close_button(rect, app, is_active)
473 }
474
475 if app.state.app_status == AppStatus::UserInput {
476 match app.state.focus {
477 Focus::EmailIDField => {
478 let (x_pos, y_pos) = calculate_viewport_corrected_cursor_position(
479 &app.state.text_buffers.email_id,
480 &app.config.show_line_numbers,
481 &email_id_chunk,
482 );
483 rect.set_cursor_position((x_pos, y_pos));
484 }
485 Focus::ResetPasswordLinkField => {
486 let (x_pos, y_pos) = calculate_viewport_corrected_cursor_position(
487 &app.state.text_buffers.reset_password_link,
488 &app.config.show_line_numbers,
489 &reset_link_chunk,
490 );
491 rect.set_cursor_position((x_pos, y_pos));
492 }
493 Focus::PasswordField => {
494 let (x_pos, y_pos) = calculate_viewport_corrected_cursor_position(
495 &app.state.text_buffers.password,
496 &app.config.show_line_numbers,
497 &new_password_chunk,
498 );
499 rect.set_cursor_position((x_pos, y_pos));
500 }
501 Focus::ConfirmPasswordField => {
502 let (x_pos, y_pos) = calculate_viewport_corrected_cursor_position(
503 &app.state.text_buffers.confirm_password,
504 &app.config.show_line_numbers,
505 &confirm_new_password_chunk,
506 );
507 rect.set_cursor_position((x_pos, y_pos));
508 }
509 _ => {}
510 }
511 }
512 }
513}