1use crate::constants::Popups;
2use crate::game_logic::coord::Coord;
3use crate::game_logic::game::GameState;
4use crate::utils::{flip_square_if_needed, get_coord_from_square};
5use crate::{
6 app::{App, AppResult},
7 constants::Pages,
8};
9use ratatui::crossterm::event::{
10 KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseButton, MouseEvent, MouseEventKind,
11};
12use shakmaty::{Role, Square};
13
14pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
20 if key_event.kind != KeyEventKind::Press {
23 return Ok(());
24 }
25
26 if app.game.ui.mouse_used {
29 app.game.ui.mouse_used = false;
30 if let Some(selected_square) = app.game.ui.selected_square {
31 app.game.ui.cursor_coordinates =
33 get_coord_from_square(Some(selected_square), app.game.logic.game_board.is_flipped);
34 app.game.ui.selected_square = None;
35 } else {
36 app.game.ui.cursor_coordinates.col = 4;
38 app.game.ui.cursor_coordinates.row = 4;
39 }
40 }
41
42 match app.current_popup {
45 Some(popup) => handle_popup_input(app, key_event, popup),
46 None => handle_page_input(app, key_event),
47 }
48
49 Ok(())
50}
51
52fn handle_popup_input(app: &mut App, key_event: KeyEvent, popup: Popups) {
57 match popup {
58 Popups::EnterHostIP => match key_event.code {
60 KeyCode::Enter => {
61 app.game.ui.prompt.submit_message();
63 if app.current_page == Pages::Multiplayer {
64 app.host_ip = Some(app.game.ui.prompt.message.clone());
65 }
66 app.current_popup = None;
67 }
68 KeyCode::Char(to_insert) => app.game.ui.prompt.enter_char(to_insert),
69 KeyCode::Backspace => app.game.ui.prompt.delete_char(),
70 KeyCode::Left => app.game.ui.prompt.move_cursor_left(),
71 KeyCode::Right => app.game.ui.prompt.move_cursor_right(),
72 KeyCode::Esc => {
73 app.current_popup = None;
75 if app.current_page == Pages::Multiplayer {
76 app.hosting = None;
77 app.selected_color = None;
78 app.menu_cursor = 0;
79 }
80 app.current_page = Pages::Home;
81 }
82 _ => fallback_key_handler(app, key_event),
83 },
84 Popups::Help => match key_event.code {
86 KeyCode::Char('?') => app.toggle_help_popup(),
87 KeyCode::Esc => app.toggle_help_popup(),
88 _ => fallback_key_handler(app, key_event),
89 },
90 Popups::ColorSelection => match key_event.code {
92 KeyCode::Esc => app.close_popup_and_go_home(),
93 KeyCode::Right | KeyCode::Char('l') => app.menu_cursor_right(2),
94 KeyCode::Left | KeyCode::Char('h') => app.menu_cursor_left(2),
95 KeyCode::Char(' ') | KeyCode::Enter => app.color_selection(),
96 _ => fallback_key_handler(app, key_event),
97 },
98 Popups::MultiplayerSelection => match key_event.code {
100 KeyCode::Esc => app.close_popup_and_go_home(),
101 KeyCode::Right | KeyCode::Char('l') => app.menu_cursor_right(2),
102 KeyCode::Left | KeyCode::Char('h') => app.menu_cursor_left(2),
103 KeyCode::Char(' ') | KeyCode::Enter => app.hosting_selection(),
104 _ => fallback_key_handler(app, key_event),
105 },
106 Popups::EnginePathError => match key_event.code {
107 KeyCode::Esc | KeyCode::Enter | KeyCode::Char(' ') => app.close_popup_and_go_home(),
108 _ => fallback_key_handler(app, key_event),
109 },
110 Popups::WaitingForOpponentToJoin => match key_event.code {
111 KeyCode::Esc | KeyCode::Enter | KeyCode::Char(' ') => {
112 app.close_popup_and_go_home();
113 app.cancel_hosting_cleanup();
114 }
115 _ => fallback_key_handler(app, key_event),
116 },
117 Popups::EndScreen => match key_event.code {
119 KeyCode::Char('h') | KeyCode::Char('H') => {
120 app.current_popup = None;
122 app.end_screen_dismissed = true;
123 }
124 KeyCode::Esc => {
125 app.current_popup = None;
127 app.end_screen_dismissed = true;
128 }
129 KeyCode::Char('r') | KeyCode::Char('R') => {
130 if app.game.logic.opponent.is_none() {
132 app.restart();
133 app.current_popup = None;
134 }
135 }
136 KeyCode::Char('b') | KeyCode::Char('B') => {
137 app.reset_home();
139 }
140 _ => fallback_key_handler(app, key_event),
141 },
142 Popups::PuzzleEndScreen => match key_event.code {
144 KeyCode::Char('h') | KeyCode::Char('H') => {
145 app.current_popup = None;
147 }
148 KeyCode::Esc => {
149 app.current_popup = None;
151 }
152 KeyCode::Char('n') | KeyCode::Char('N') => {
153 app.current_popup = None;
155 app.start_puzzle_mode();
156 }
157 KeyCode::Char('b') | KeyCode::Char('B') => {
158 app.reset_home();
160 }
161 _ => fallback_key_handler(app, key_event),
162 },
163 Popups::Error => match key_event.code {
165 KeyCode::Esc | KeyCode::Enter | KeyCode::Char(' ') => {
166 app.current_popup = None;
167 app.error_message = None;
168 match app.current_page {
170 Pages::Lichess | Pages::OngoingGames => {
171 app.current_page = Pages::LichessMenu;
173 }
174 Pages::Multiplayer | Pages::Bot => {
175 app.current_page = Pages::Home;
177 }
178 _ => {
179 if app.current_page == Pages::Solo && app.game.logic.opponent.is_some() {
182 app.current_page = Pages::Home;
184 }
185 }
186 }
187 }
188 _ => fallback_key_handler(app, key_event),
189 },
190 Popups::Success => match key_event.code {
192 KeyCode::Esc | KeyCode::Enter | KeyCode::Char(' ') => {
193 app.current_popup = None;
194 app.error_message = None;
195 match app.current_page {
197 Pages::Lichess | Pages::OngoingGames => {
198 app.current_page = Pages::LichessMenu;
200 }
201 Pages::Multiplayer | Pages::Bot => {
202 app.current_page = Pages::Home;
204 }
205 _ => {
206 if app.current_page == Pages::Solo && app.game.logic.opponent.is_some() {
209 app.current_page = Pages::Home;
211 }
212 }
213 }
214 }
215 _ => fallback_key_handler(app, key_event),
216 },
217 Popups::EnterGameCode => match key_event.code {
218 KeyCode::Enter => {
219 app.game.ui.prompt.submit_message();
221 let game_code = app.game.ui.prompt.message.clone();
222
223 if !game_code.is_empty() {
224 app.current_page = Pages::Lichess;
226 app.join_lichess_game_by_code(game_code);
227 } else {
228 app.current_popup = None;
230 app.current_page = Pages::LichessMenu;
231 }
232 }
233 KeyCode::Char(to_insert) => app.game.ui.prompt.enter_char(to_insert),
234 KeyCode::Backspace => app.game.ui.prompt.delete_char(),
235 KeyCode::Left => app.game.ui.prompt.move_cursor_left(),
236 KeyCode::Right => app.game.ui.prompt.move_cursor_right(),
237 KeyCode::Esc => {
238 app.current_popup = None;
240 app.current_page = Pages::LichessMenu;
241 }
242 _ => fallback_key_handler(app, key_event),
243 },
244 Popups::EnterLichessToken => match key_event.code {
245 KeyCode::Enter => {
246 app.game.ui.prompt.submit_message();
248 let token = app.game.ui.prompt.message.clone().trim().to_string();
249
250 if !token.is_empty() {
251 app.save_and_validate_lichess_token(token);
253 } else {
254 app.current_popup = None;
256 }
257 }
258 KeyCode::Char(to_insert) => app.game.ui.prompt.enter_char(to_insert),
259 KeyCode::Backspace => app.game.ui.prompt.delete_char(),
260 KeyCode::Left => app.game.ui.prompt.move_cursor_left(),
261 KeyCode::Right => app.game.ui.prompt.move_cursor_right(),
262 KeyCode::Esc => {
263 app.current_popup = None;
265 }
266 _ => fallback_key_handler(app, key_event),
267 },
268 Popups::SeekingLichessGame => match key_event.code {
269 KeyCode::Esc => {
270 if let Some(token) = &app.lichess_cancellation_token {
271 token.store(true, std::sync::atomic::Ordering::Relaxed);
272 }
273 app.lichess_seek_receiver = None; app.lichess_cancellation_token = None;
275 app.current_popup = None;
276 app.current_page = Pages::Home; }
278 _ => fallback_key_handler(app, key_event),
279 },
280 Popups::ResignConfirmation => match key_event.code {
281 KeyCode::Char('y') | KeyCode::Char('Y') | KeyCode::Enter => {
282 app.confirm_resign_game();
283 }
285 KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
286 app.current_popup = None;
287 }
288 _ => fallback_key_handler(app, key_event),
289 },
290 };
291}
292
293fn handle_page_input(app: &mut App, key_event: KeyEvent) {
295 match &app.current_page {
296 Pages::Home => handle_home_page_events(app, key_event),
297 Pages::Solo => handle_solo_page_events(app, key_event),
298 Pages::Multiplayer => handle_multiplayer_page_events(app, key_event),
299 Pages::Lichess => handle_multiplayer_page_events(app, key_event),
300 Pages::LichessMenu => handle_lichess_menu_page_events(app, key_event),
301 Pages::OngoingGames => handle_ongoing_games_page_events(app, key_event),
302 Pages::Bot => handle_bot_page_events(app, key_event),
303 Pages::Credit => handle_credit_page_events(app, key_event),
304 }
305}
306
307fn handle_home_page_events(app: &mut App, key_event: KeyEvent) {
310 match key_event.code {
311 KeyCode::Up | KeyCode::Char('k') => app.menu_cursor_up(8), KeyCode::Down | KeyCode::Char('j') => app.menu_cursor_down(8), KeyCode::Left | KeyCode::Char('h') if app.menu_cursor == 3 => {
315 app.cycle_skin_backward();
316 app.update_config();
317 }
318 KeyCode::Right | KeyCode::Char('l') if app.menu_cursor == 3 => {
319 app.cycle_skin();
320 app.update_config();
321 }
322 KeyCode::Char(' ') | KeyCode::Enter => app.menu_select(),
323 KeyCode::Char('?') => app.toggle_help_popup(),
324 _ => fallback_key_handler(app, key_event),
325 }
326}
327
328fn handle_solo_page_events(app: &mut App, key_event: KeyEvent) {
330 match key_event.code {
331 KeyCode::Char('r') => app.restart(), KeyCode::Char('b') => {
333 app.reset_home();
335 }
336 KeyCode::Char('n' | 'N') => {
337 if app.puzzle_game.is_none()
340 && app.game.logic.game_state != GameState::Checkmate
341 && app.game.logic.game_state != GameState::Draw
342 {
343 app.navigate_history_next();
344 }
345 }
346 KeyCode::Char('p' | 'P') => {
347 if app.puzzle_game.is_none()
349 && app.game.logic.game_state != GameState::Checkmate
350 && app.game.logic.game_state != GameState::Draw
351 {
352 app.navigate_history_previous();
353 }
354 }
355 KeyCode::Char('t' | 'T') if app.puzzle_game.is_some() && app.current_popup.is_none() => {
356 app.show_puzzle_hint();
358 }
359 _ => chess_inputs(app, key_event), }
361}
362
363fn chess_inputs(app: &mut App, key_event: KeyEvent) {
368 let is_playing = app.game.logic.game_state == GameState::Playing;
369
370 match key_event.code {
371 KeyCode::Up | KeyCode::Char('k') if is_playing => app.go_up_in_game(),
373 KeyCode::Down | KeyCode::Char('j') if is_playing => app.go_down_in_game(),
374
375 KeyCode::Right | KeyCode::Char('l') => match app.game.logic.game_state {
377 GameState::Promotion => {
378 app.game.ui.cursor_right_promotion();
380 }
381 GameState::Playing => {
382 if app.current_page == Pages::Lichess {
384 if let Some(my_color) = app.selected_color {
385 if app.game.logic.player_turn == my_color {
386 app.go_right_in_game();
387 }
388 }
389 } else {
390 app.go_right_in_game();
391 }
392 }
393 _ => (),
394 },
395 KeyCode::Left | KeyCode::Char('h') => match app.game.logic.game_state {
396 GameState::Promotion => {
397 app.game.ui.cursor_left_promotion();
399 }
400 GameState::Playing => {
401 if app.current_page == Pages::Lichess {
403 if let Some(my_color) = app.selected_color {
404 if app.game.logic.player_turn == my_color {
405 app.go_left_in_game();
406 }
407 }
408 } else {
409 app.go_left_in_game();
410 }
411 }
412 GameState::Checkmate | GameState::Draw => {
413 if app.current_popup == Some(Popups::EndScreen) {
416 app.current_popup = None;
418 app.end_screen_dismissed = true;
419 } else if app.end_screen_dismissed {
420 app.end_screen_dismissed = false;
422 app.show_end_screen();
423 } else {
424 app.show_end_screen();
426 }
427 }
428 },
429 KeyCode::Char(' ') | KeyCode::Enter => {
431 app.process_cell_click();
433 }
434 KeyCode::Char('?') => app.toggle_help_popup(), KeyCode::Char('s' | 'S') => {
436 app.cycle_skin(); app.update_config();
438 }
439 KeyCode::Esc => app.game.ui.unselect_cell(), _ => fallback_key_handler(app, key_event),
441 }
442}
443
444fn handle_multiplayer_page_events(app: &mut App, key_event: KeyEvent) {
447 match key_event.code {
448 KeyCode::Char('b') => {
449 app.reset_home();
451 }
452
453 _ => chess_inputs(app, key_event), }
455}
456
457fn handle_bot_page_events(app: &mut App, key_event: KeyEvent) {
460 match key_event.code {
461 KeyCode::Char('r') => app.restart(), KeyCode::Char('b') => {
463 app.reset_home();
465 }
466 _ => chess_inputs(app, key_event), }
468}
469
470fn handle_credit_page_events(app: &mut App, key_event: KeyEvent) {
472 match key_event.code {
473 KeyCode::Char(' ') | KeyCode::Esc | KeyCode::Enter => app.toggle_credit_popup(),
474 _ => fallback_key_handler(app, key_event),
475 }
476}
477
478fn fallback_key_handler(app: &mut App, key_event: KeyEvent) {
481 match key_event.code {
482 KeyCode::Char('q') => app.quit(), KeyCode::Char('c' | 'C') if key_event.modifiers == KeyModifiers::CONTROL => app.quit(), KeyCode::Char('s') => app.cycle_skin(), _ => (), }
487}
488
489pub fn handle_mouse_events(mouse_event: MouseEvent, app: &mut App) -> AppResult<()> {
494 if app.current_page == Pages::Home || app.current_page == Pages::Credit {
496 return Ok(());
497 }
498
499 if mouse_event.kind == MouseEventKind::Down(MouseButton::Left) {
501 if app.game.logic.game_state == GameState::Checkmate
503 || app.game.logic.game_state == GameState::Draw
504 {
505 return Ok(());
506 }
507 if app.current_popup.is_some() {
509 return Ok(());
510 }
511
512 if app.game.logic.game_state == GameState::Promotion {
516 let x = (mouse_event.column - app.game.ui.top_x) / app.game.ui.width;
518 let y = (mouse_event.row - app.game.ui.top_y) / app.game.ui.height;
519 if x > 3 || y > 0 {
520 return Ok(()); }
522 app.game.ui.promotion_cursor = x as i8;
523
524 let mut move_was_correct = true;
526
527 if let Some((from, to)) = app.pending_promotion_move.take() {
529 let promotion_char = match app.game.ui.promotion_cursor {
531 0 => 'q', 1 => 'r', 2 => 'b', 3 => 'n', _ => 'q', };
537
538 let move_uci = format!("{}{}{}", from, to, promotion_char);
540
541 if app.puzzle_game.is_some() {
543 if let Some(mut puzzle_game) = app.puzzle_game.take() {
544 let (is_correct, message) = puzzle_game.validate_move(
545 move_uci,
546 &mut app.game,
547 app.lichess_token.clone(),
548 );
549
550 move_was_correct = is_correct;
551 app.puzzle_game = Some(puzzle_game);
552
553 if let Some(msg) = message {
554 if is_correct {
555 app.error_message = Some(msg);
556 app.current_popup = Some(Popups::PuzzleEndScreen);
557 } else {
558 app.error_message = Some(msg);
559 app.current_popup = Some(Popups::Error);
560 }
561 }
562 }
563 }
564 }
565
566 if move_was_correct || app.puzzle_game.is_none() {
569 let should_flip = app.puzzle_game.is_none();
571 app.game.handle_promotion(should_flip);
572 } else {
573 if app.game.logic.game_state == GameState::Promotion {
576 app.game.logic.game_state = GameState::Playing;
577 }
578 }
579 if app.game.logic.opponent.is_some() {
581 app.game.logic.handle_multiplayer_promotion();
582 }
583 return Ok(());
584 }
585
586 if app.current_page == Pages::Lichess {
588 if let Some(my_color) = app.selected_color {
589 if app.game.logic.player_turn != my_color {
590 return Ok(());
591 }
592 }
593 }
594
595 if mouse_event.column < app.game.ui.top_x || mouse_event.row < app.game.ui.top_y {
597 return Ok(());
598 }
599 if app.game.ui.width == 0 || app.game.ui.height == 0 {
600 return Ok(());
601 }
602
603 let x = (mouse_event.column - app.game.ui.top_x) / app.game.ui.width;
605 let y = (mouse_event.row - app.game.ui.top_y) / app.game.ui.height;
606 if x > 7 || y > 7 {
607 return Ok(()); }
609
610 app.game.ui.mouse_used = true;
612 let coords: Coord = Coord::new(y as u8, x as u8);
613
614 let square = match coords.try_to_square() {
616 Some(s) => s,
617 None => return Ok(()), };
619
620 let piece_color =
622 app.game
623 .logic
624 .game_board
625 .get_piece_color_at_square(&flip_square_if_needed(
626 square,
627 app.game.logic.game_board.is_flipped,
628 ));
629
630 if piece_color.is_none() {
632 if app.game.ui.selected_square.is_none() {
634 return Ok(());
635 } else {
636 app.try_mouse_move(square, coords);
638 }
639 }
640 else if piece_color == Some(app.game.logic.player_turn) {
642 if let Some(selected_square) = app.game.ui.selected_square {
645 let actual_selected =
647 flip_square_if_needed(selected_square, app.game.logic.game_board.is_flipped);
648 let actual_clicked =
649 flip_square_if_needed(square, app.game.logic.game_board.is_flipped);
650
651 let selected_role = app
652 .game
653 .logic
654 .game_board
655 .get_role_at_square(&actual_selected);
656 let clicked_role = app
657 .game
658 .logic
659 .game_board
660 .get_role_at_square(&actual_clicked);
661
662 if selected_role == Some(Role::King) && clicked_role == Some(Role::Rook) {
664 let castling_dest = match actual_clicked {
666 Square::H1 => Square::G1, Square::A1 => Square::C1, Square::H8 => Square::G8, Square::A8 => Square::C8, _ => {
671 if app.try_mouse_move(square, coords) {
673 return Ok(());
674 }
675 app.game.ui.selected_square = Some(square);
676 return Ok(());
677 }
678 };
679
680 let authorized_positions = app
682 .game
683 .logic
684 .game_board
685 .get_authorized_positions(app.game.logic.player_turn, &actual_selected);
686
687 if authorized_positions.contains(&castling_dest) {
688 let castling_coords = Coord::from_square(flip_square_if_needed(
690 castling_dest,
691 app.game.logic.game_board.is_flipped,
692 ));
693 if app.try_mouse_move(
694 flip_square_if_needed(
695 castling_dest,
696 app.game.logic.game_board.is_flipped,
697 ),
698 castling_coords,
699 ) {
700 return Ok(());
701 }
702 }
703 }
704
705 if app.try_mouse_move(square, coords) {
707 return Ok(());
709 }
710 }
711 app.game.ui.selected_square = Some(square);
713 } else {
714 if app.game.ui.selected_square.is_some() {
716 app.try_mouse_move(square, coords);
717 }
718 }
720 }
721 Ok(())
722}
723
724fn handle_lichess_menu_page_events(app: &mut App, key_event: KeyEvent) {
727 match key_event.code {
728 KeyCode::Up | KeyCode::Char('k') => app.menu_cursor_up(5), KeyCode::Down | KeyCode::Char('j') => app.menu_cursor_down(5),
730 KeyCode::Char(' ') | KeyCode::Enter => {
731 match app.menu_cursor {
733 0 => {
734 if app.lichess_token.is_none()
736 || app
737 .lichess_token
738 .as_ref()
739 .map(|t| t.is_empty())
740 .unwrap_or(true)
741 {
742 app.current_popup = Some(Popups::EnterLichessToken);
744 app.game.ui.prompt.reset();
745 app.game.ui.prompt.message = "Enter your Lichess API token:".to_string();
746 return;
747 }
748 app.menu_cursor = 0;
749 app.current_page = Pages::Lichess;
750 app.create_lichess_opponent();
751 }
752 1 => {
753 if app.lichess_token.is_none()
755 || app
756 .lichess_token
757 .as_ref()
758 .map(|t| t.is_empty())
759 .unwrap_or(true)
760 {
761 app.current_popup = Some(Popups::EnterLichessToken);
763 app.game.ui.prompt.reset();
764 app.game.ui.prompt.message = "Enter your Lichess API token:".to_string();
765 return;
766 }
767 app.start_puzzle_mode();
768 }
769 2 => {
770 if app.lichess_token.is_none()
772 || app
773 .lichess_token
774 .as_ref()
775 .map(|t| t.is_empty())
776 .unwrap_or(true)
777 {
778 app.current_popup = Some(Popups::EnterLichessToken);
780 app.game.ui.prompt.reset();
781 app.game.ui.prompt.message = "Enter your Lichess API token:".to_string();
782 return;
783 }
784 app.fetch_ongoing_games();
785 }
786 3 => {
787 if app.lichess_token.is_none()
789 || app
790 .lichess_token
791 .as_ref()
792 .map(|t| t.is_empty())
793 .unwrap_or(true)
794 {
795 app.current_popup = Some(Popups::EnterLichessToken);
797 app.game.ui.prompt.reset();
798 app.game.ui.prompt.message = "Enter your Lichess API token:".to_string();
799 return;
800 }
801 app.current_popup = Some(Popups::EnterGameCode);
802 app.game.ui.prompt.reset();
803 }
804 4 => {
805 app.disconnect_lichess();
807 }
808 _ => {}
809 }
810 }
811 KeyCode::Esc | KeyCode::Char('b') => {
812 app.menu_cursor = 0;
814 app.current_page = Pages::Home;
815 }
816 KeyCode::Char('?') => app.toggle_help_popup(),
817 _ => fallback_key_handler(app, key_event),
818 }
819}
820
821fn handle_ongoing_games_page_events(app: &mut App, key_event: KeyEvent) {
822 match key_event.code {
823 KeyCode::Up | KeyCode::Char('k') => {
824 if app.menu_cursor > 0 {
825 app.menu_cursor -= 1;
826 }
827 }
828 KeyCode::Down | KeyCode::Char('j') => {
829 if (app.menu_cursor as usize) < app.ongoing_games.len().saturating_sub(1) {
830 app.menu_cursor += 1;
831 }
832 }
833 KeyCode::Enter | KeyCode::Char(' ') => {
834 app.select_ongoing_game();
835 }
836 KeyCode::Char('r') | KeyCode::Char('R') => {
837 app.show_resign_confirmation();
839 }
840 KeyCode::Esc | KeyCode::Char('b') => {
841 app.menu_cursor = 0;
842 app.current_page = Pages::LichessMenu;
843 }
844 KeyCode::Char('?') => app.toggle_help_popup(),
845 _ => fallback_key_handler(app, key_event),
846 }
847}