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 const MENU_ITEMS: u8 = {
312 #[cfg(feature = "sound")]
313 {
314 8 }
316 #[cfg(not(feature = "sound"))]
317 {
318 7 }
320 };
321
322 match key_event.code {
323 KeyCode::Up | KeyCode::Char('k') => app.menu_cursor_up(MENU_ITEMS),
324 KeyCode::Down | KeyCode::Char('j') => app.menu_cursor_down(MENU_ITEMS),
325 KeyCode::Left | KeyCode::Char('h') if app.menu_cursor == 3 => {
327 app.cycle_skin_backward();
328 app.update_config();
329 }
330 KeyCode::Right | KeyCode::Char('l') if app.menu_cursor == 3 => {
331 app.cycle_skin();
332 app.update_config();
333 }
334 KeyCode::Char(' ') | KeyCode::Enter => app.menu_select(),
335 KeyCode::Char('?') => app.toggle_help_popup(),
336 _ => fallback_key_handler(app, key_event),
337 }
338}
339
340fn handle_solo_page_events(app: &mut App, key_event: KeyEvent) {
342 match key_event.code {
343 KeyCode::Char('r') => app.restart(), KeyCode::Char('b') => {
345 app.reset_home();
347 }
348 KeyCode::Char('n' | 'N') => {
349 if app.puzzle_game.is_none()
352 && app.game.logic.game_state != GameState::Checkmate
353 && app.game.logic.game_state != GameState::Draw
354 {
355 app.navigate_history_next();
356 }
357 }
358 KeyCode::Char('p' | 'P') => {
359 if app.puzzle_game.is_none()
361 && app.game.logic.game_state != GameState::Checkmate
362 && app.game.logic.game_state != GameState::Draw
363 {
364 app.navigate_history_previous();
365 }
366 }
367 KeyCode::Char('t' | 'T') if app.puzzle_game.is_some() && app.current_popup.is_none() => {
368 app.show_puzzle_hint();
370 }
371 _ => chess_inputs(app, key_event), }
373}
374
375fn chess_inputs(app: &mut App, key_event: KeyEvent) {
380 let is_playing = app.game.logic.game_state == GameState::Playing;
381
382 match key_event.code {
383 KeyCode::Up | KeyCode::Char('k') if is_playing => app.go_up_in_game(),
385 KeyCode::Down | KeyCode::Char('j') if is_playing => app.go_down_in_game(),
386
387 KeyCode::Right | KeyCode::Char('l') => match app.game.logic.game_state {
389 GameState::Promotion => {
390 app.game.ui.cursor_right_promotion();
392 }
393 GameState::Playing => {
394 if app.current_page == Pages::Lichess {
396 if let Some(my_color) = app.selected_color {
397 if app.game.logic.player_turn == my_color {
398 app.go_right_in_game();
399 }
400 }
401 } else {
402 app.go_right_in_game();
403 }
404 }
405 _ => (),
406 },
407 KeyCode::Left | KeyCode::Char('h') => match app.game.logic.game_state {
408 GameState::Promotion => {
409 app.game.ui.cursor_left_promotion();
411 }
412 GameState::Playing => {
413 if app.current_page == Pages::Lichess {
415 if let Some(my_color) = app.selected_color {
416 if app.game.logic.player_turn == my_color {
417 app.go_left_in_game();
418 }
419 }
420 } else {
421 app.go_left_in_game();
422 }
423 }
424 GameState::Checkmate | GameState::Draw => {
425 if app.current_popup == Some(Popups::EndScreen) {
428 app.current_popup = None;
430 app.end_screen_dismissed = true;
431 } else if app.end_screen_dismissed {
432 app.end_screen_dismissed = false;
434 app.show_end_screen();
435 } else {
436 app.show_end_screen();
438 }
439 }
440 },
441 KeyCode::Char(' ') | KeyCode::Enter => {
443 app.process_cell_click();
445 }
446 KeyCode::Char('?') => app.toggle_help_popup(), KeyCode::Char('s' | 'S') => {
448 app.cycle_skin(); app.update_config();
450 }
451 KeyCode::Esc => app.game.ui.unselect_cell(), _ => fallback_key_handler(app, key_event),
453 }
454}
455
456fn handle_multiplayer_page_events(app: &mut App, key_event: KeyEvent) {
459 match key_event.code {
460 KeyCode::Char('b') => {
461 app.reset_home();
463 }
464
465 _ => chess_inputs(app, key_event), }
467}
468
469fn handle_bot_page_events(app: &mut App, key_event: KeyEvent) {
472 match key_event.code {
473 KeyCode::Char('r') => app.restart(), KeyCode::Char('b') => {
475 app.reset_home();
477 }
478 _ => chess_inputs(app, key_event), }
480}
481
482fn handle_credit_page_events(app: &mut App, key_event: KeyEvent) {
484 match key_event.code {
485 KeyCode::Char(' ') | KeyCode::Esc | KeyCode::Enter => app.toggle_credit_popup(),
486 _ => fallback_key_handler(app, key_event),
487 }
488}
489
490fn fallback_key_handler(app: &mut App, key_event: KeyEvent) {
493 match key_event.code {
494 KeyCode::Char('q') => app.quit(), KeyCode::Char('c' | 'C') if key_event.modifiers == KeyModifiers::CONTROL => app.quit(), KeyCode::Char('s') => app.cycle_skin(), _ => (), }
499}
500
501pub fn handle_mouse_events(mouse_event: MouseEvent, app: &mut App) -> AppResult<()> {
506 if app.current_page == Pages::Home || app.current_page == Pages::Credit {
508 return Ok(());
509 }
510
511 if mouse_event.kind == MouseEventKind::Down(MouseButton::Left) {
513 if app.game.logic.game_state == GameState::Checkmate
515 || app.game.logic.game_state == GameState::Draw
516 {
517 return Ok(());
518 }
519 if app.current_popup.is_some() {
521 return Ok(());
522 }
523
524 if app.game.logic.game_state == GameState::Promotion {
528 let x = (mouse_event.column - app.game.ui.top_x) / app.game.ui.width;
530 let y = (mouse_event.row - app.game.ui.top_y) / app.game.ui.height;
531 if x > 3 || y > 0 {
532 return Ok(()); }
534 app.game.ui.promotion_cursor = x as i8;
535
536 let mut move_was_correct = true;
538
539 if let Some((from, to)) = app.pending_promotion_move.take() {
541 let promotion_char = match app.game.ui.promotion_cursor {
543 0 => 'q', 1 => 'r', 2 => 'b', 3 => 'n', _ => 'q', };
549
550 let move_uci = format!("{}{}{}", from, to, promotion_char);
552
553 if app.puzzle_game.is_some() {
555 if let Some(mut puzzle_game) = app.puzzle_game.take() {
556 let (is_correct, message) = puzzle_game.validate_move(
557 move_uci,
558 &mut app.game,
559 app.lichess_token.clone(),
560 );
561
562 move_was_correct = is_correct;
563 app.puzzle_game = Some(puzzle_game);
564
565 if let Some(msg) = message {
566 if is_correct {
567 app.error_message = Some(msg);
568 app.current_popup = Some(Popups::PuzzleEndScreen);
569 } else {
570 app.error_message = Some(msg);
571 app.current_popup = Some(Popups::Error);
572 }
573 }
574 }
575 }
576 }
577
578 if move_was_correct || app.puzzle_game.is_none() {
581 let should_flip = app.puzzle_game.is_none();
583 app.game.handle_promotion(should_flip);
584 } else {
585 if app.game.logic.game_state == GameState::Promotion {
588 app.game.logic.game_state = GameState::Playing;
589 }
590 }
591 if app.game.logic.opponent.is_some() {
593 app.game.logic.handle_multiplayer_promotion();
594 }
595 return Ok(());
596 }
597
598 if app.current_page == Pages::Lichess {
600 if let Some(my_color) = app.selected_color {
601 if app.game.logic.player_turn != my_color {
602 return Ok(());
603 }
604 }
605 }
606
607 if mouse_event.column < app.game.ui.top_x || mouse_event.row < app.game.ui.top_y {
609 return Ok(());
610 }
611 if app.game.ui.width == 0 || app.game.ui.height == 0 {
612 return Ok(());
613 }
614
615 let x = (mouse_event.column - app.game.ui.top_x) / app.game.ui.width;
617 let y = (mouse_event.row - app.game.ui.top_y) / app.game.ui.height;
618 if x > 7 || y > 7 {
619 return Ok(()); }
621
622 app.game.ui.mouse_used = true;
624 let coords: Coord = Coord::new(y as u8, x as u8);
625
626 let square = match coords.try_to_square() {
628 Some(s) => s,
629 None => return Ok(()), };
631
632 let piece_color =
634 app.game
635 .logic
636 .game_board
637 .get_piece_color_at_square(&flip_square_if_needed(
638 square,
639 app.game.logic.game_board.is_flipped,
640 ));
641
642 if piece_color.is_none() {
644 if app.game.ui.selected_square.is_none() {
646 return Ok(());
647 } else {
648 app.try_mouse_move(square, coords);
650 }
651 }
652 else if piece_color == Some(app.game.logic.player_turn) {
654 if let Some(selected_square) = app.game.ui.selected_square {
657 let actual_selected =
659 flip_square_if_needed(selected_square, app.game.logic.game_board.is_flipped);
660 let actual_clicked =
661 flip_square_if_needed(square, app.game.logic.game_board.is_flipped);
662
663 let selected_role = app
664 .game
665 .logic
666 .game_board
667 .get_role_at_square(&actual_selected);
668 let clicked_role = app
669 .game
670 .logic
671 .game_board
672 .get_role_at_square(&actual_clicked);
673
674 if selected_role == Some(Role::King) && clicked_role == Some(Role::Rook) {
676 let castling_dest = match actual_clicked {
678 Square::H1 => Square::G1, Square::A1 => Square::C1, Square::H8 => Square::G8, Square::A8 => Square::C8, _ => {
683 if app.try_mouse_move(square, coords) {
685 return Ok(());
686 }
687 app.game.ui.selected_square = Some(square);
688 return Ok(());
689 }
690 };
691
692 let authorized_positions = app
694 .game
695 .logic
696 .game_board
697 .get_authorized_positions(app.game.logic.player_turn, &actual_selected);
698
699 if authorized_positions.contains(&castling_dest) {
700 let castling_coords = Coord::from_square(flip_square_if_needed(
702 castling_dest,
703 app.game.logic.game_board.is_flipped,
704 ));
705 if app.try_mouse_move(
706 flip_square_if_needed(
707 castling_dest,
708 app.game.logic.game_board.is_flipped,
709 ),
710 castling_coords,
711 ) {
712 return Ok(());
713 }
714 }
715 }
716
717 if app.try_mouse_move(square, coords) {
719 return Ok(());
721 }
722 }
723 app.game.ui.selected_square = Some(square);
725 } else {
726 if app.game.ui.selected_square.is_some() {
728 app.try_mouse_move(square, coords);
729 }
730 }
732 }
733 Ok(())
734}
735
736fn handle_lichess_menu_page_events(app: &mut App, key_event: KeyEvent) {
739 match key_event.code {
740 KeyCode::Up | KeyCode::Char('k') => app.menu_cursor_up(5), KeyCode::Down | KeyCode::Char('j') => app.menu_cursor_down(5),
742 KeyCode::Char(' ') | KeyCode::Enter => {
743 match app.menu_cursor {
745 0 => {
746 if app.lichess_token.is_none()
748 || app
749 .lichess_token
750 .as_ref()
751 .map(|t| t.is_empty())
752 .unwrap_or(true)
753 {
754 app.current_popup = Some(Popups::EnterLichessToken);
756 app.game.ui.prompt.reset();
757 app.game.ui.prompt.message = "Enter your Lichess API token:".to_string();
758 return;
759 }
760 app.menu_cursor = 0;
761 app.current_page = Pages::Lichess;
762 app.create_lichess_opponent();
763 }
764 1 => {
765 if app.lichess_token.is_none()
767 || app
768 .lichess_token
769 .as_ref()
770 .map(|t| t.is_empty())
771 .unwrap_or(true)
772 {
773 app.current_popup = Some(Popups::EnterLichessToken);
775 app.game.ui.prompt.reset();
776 app.game.ui.prompt.message = "Enter your Lichess API token:".to_string();
777 return;
778 }
779 app.start_puzzle_mode();
780 }
781 2 => {
782 if app.lichess_token.is_none()
784 || app
785 .lichess_token
786 .as_ref()
787 .map(|t| t.is_empty())
788 .unwrap_or(true)
789 {
790 app.current_popup = Some(Popups::EnterLichessToken);
792 app.game.ui.prompt.reset();
793 app.game.ui.prompt.message = "Enter your Lichess API token:".to_string();
794 return;
795 }
796 app.fetch_ongoing_games();
797 }
798 3 => {
799 if app.lichess_token.is_none()
801 || app
802 .lichess_token
803 .as_ref()
804 .map(|t| t.is_empty())
805 .unwrap_or(true)
806 {
807 app.current_popup = Some(Popups::EnterLichessToken);
809 app.game.ui.prompt.reset();
810 app.game.ui.prompt.message = "Enter your Lichess API token:".to_string();
811 return;
812 }
813 app.current_popup = Some(Popups::EnterGameCode);
814 app.game.ui.prompt.reset();
815 }
816 4 => {
817 app.disconnect_lichess();
819 }
820 _ => {}
821 }
822 }
823 KeyCode::Esc | KeyCode::Char('b') => {
824 app.menu_cursor = 0;
826 app.current_page = Pages::Home;
827 }
828 KeyCode::Char('?') => app.toggle_help_popup(),
829 _ => fallback_key_handler(app, key_event),
830 }
831}
832
833fn handle_ongoing_games_page_events(app: &mut App, key_event: KeyEvent) {
834 match key_event.code {
835 KeyCode::Up | KeyCode::Char('k') => {
836 if app.menu_cursor > 0 {
837 app.menu_cursor -= 1;
838 }
839 }
840 KeyCode::Down | KeyCode::Char('j') => {
841 if (app.menu_cursor as usize) < app.ongoing_games.len().saturating_sub(1) {
842 app.menu_cursor += 1;
843 }
844 }
845 KeyCode::Enter | KeyCode::Char(' ') => {
846 app.select_ongoing_game();
847 }
848 KeyCode::Char('r') | KeyCode::Char('R') => {
849 app.show_resign_confirmation();
851 }
852 KeyCode::Esc | KeyCode::Char('b') => {
853 app.menu_cursor = 0;
854 app.current_page = Pages::LichessMenu;
855 }
856 KeyCode::Char('?') => app.toggle_help_popup(),
857 _ => fallback_key_handler(app, key_event),
858 }
859}