Repository: thomas-mauran/chess-tui (language: rust)

Issue:
Mouse support? Is it technically possible?
## Feature Request

A question: Mouse support? Is it technically possible?



Discussion / hints:
Yes - Crossterm supports click events. You'd need to map these to the screen location of the board positions however.
That could be very nice do you want to make a pr @franzmondlichtmann ?
Oh sorry, I was just curious, I don't actually know how to implement that. :)
Also I'm not very good in tech. Maybe I should have done a feature request instead, and not call it a problem.
No problem thanks for the issue it's a great idea 
This sounds interesting to me. I will jump into it when I can.
> This sounds interesting to me. I will jump into it when I can.

that could be very nice mate looking forward to it !

Fix patch:
diff --git a/src/board.rs b/src/board.rs
index 6d2c2c4..e7b3f55 100644
--- a/src/board.rs
+++ b/src/board.rs
@@ -132,6 +132,14 @@ pub struct Board {
     pub is_game_against_bot: bool,
     // the display mode
     pub display_mode: DisplayMode,
+    // coordinates of the interactable part of the screen (either normal chess board or promotion screen)
+    pub top_x: u16,
+    pub top_y: u16,
+    // dimension of a selectable cell (either 1 of the 64 cells, or 1 of the 4 promotion options)
+    pub width: u16,
+    pub height: u16,
+    // last move was with a mouse
+    pub mouse_used: bool,
     // if the bot is starting, meaning the player is black
     pub is_bot_starting: bool,
     // The white piece that got taken
@@ -139,7 +147,6 @@ pub struct Board {
     // The black piece that got taken
     pub black_taken_pieces: Vec<PieceType>,
 }
-
 fn init_board() -> GameBoard {
     [
         [
@@ -208,9 +215,14 @@ impl Default for Board {
             engine: None,
             is_game_against_bot: false,
             display_mode: DisplayMode::DEFAULT,
+            top_x: 0,
+            top_y: 0,
+            width: 0,
+            height: 0,
             is_bot_starting: false,
             white_taken_pieces: vec![],
             black_taken_pieces: vec![],
+            mouse_used: false,
         }
     }
 }
@@ -234,9 +246,14 @@ impl Board {
             engine: None,
             is_game_against_bot: false,
             display_mode: DisplayMode::DEFAULT,
+            top_x: 0,
+            top_y: 0,
+            width: 0,
+            height: 0,
             is_bot_starting: false,
             white_taken_pieces: vec![],
             black_taken_pieces: vec![],
+            mouse_used: false,
         }
     }
 
@@ -392,6 +409,25 @@ impl Board {
         }
     }
 
+    pub fn move_selected_piece_cursor_mouse(&mut self, coordinates: Coord) {
+        let piece_color = get_piece_color(self.board, &self.selected_coordinates);
+        let piece_type = get_piece_type(self.board, &self.selected_coordinates);
+
+        let authorized_positions =
+            self.get_authorized_positions(piece_type, piece_color, self.selected_coordinates);
+        if authorized_positions.contains(&coordinates)
+            && match piece_color {
+                Some(piece) => piece == self.player_turn,
+                None => false,
+            }
+        {
+            self.cursor_coordinates = coordinates;
+            self.select_cell();
+        } else {
+            self.selected_coordinates = coordinates;
+        }
+    }
+
     // Methods to select a cell on the board
     pub fn select_cell(&mut self) {
         // If we are doing a promotion the cursor is used for the popup
@@ -871,11 +907,17 @@ impl Board {
     }
 
     // Method to render the board
-    pub fn board_render(&self, area: Rect, frame: &mut Frame) {
+    pub fn board_render(&mut self, area: Rect, frame: &mut Frame) {
         let width = area.width / 8;
         let height = area.height / 8;
         let border_height = area.height / 2 - (4 * height);
         let border_width = area.width / 2 - (4 * width);
+
+        // we update the starting coordinates
+        self.top_x = area.x + border_width;
+        self.top_y = area.y + border_height;
+        self.width = width;
+        self.height = height;
         // We have 8 vertical lines
         let columns = Layout::default()
             .direction(Direction::Vertical)
@@ -945,16 +987,22 @@ impl Board {
                         get_piece_type(self.board, &self.selected_coordinates);
                     let selected_piece_color: Option<PieceColor> =
                         get_piece_color(self.board, &self.selected_coordinates);
-                    positions = self.get_authorized_positions(
-                        selected_piece_type,
-                        selected_piece_color,
-                        self.selected_coordinates,
-                    );
-
-                    // Draw grey if the color is in the authorized positions
-                    for coords in positions.clone() {
-                        if i == coords.row && j == coords.col {
-                            // cell_color = Color::Rgb(100, 100, 100);
+                    // only draw available moves if it is the right players turn
+                    if match selected_piece_color {
+                        Some(color) => color == self.player_turn,
+                        None => false,
+                    } {
+                        positions = self.get_authorized_positions(
+                            selected_piece_type,
+                            selected_piece_color,
+                            self.selected_coordinates,
+                        );
+
+                        // Draw grey if the color is in the authorized positions
+                        for coords in positions.clone() {
+                            if i == coords.row && j == coords.col {
+                                // cell_color = Color::Rgb(100, 100, 100);
+                            }
                         }
                     }
                 }
@@ -968,7 +1016,10 @@ impl Board {
                 // - last move cell: green
                 // - default cell: white or black
                 // Draw the cell blue if this is the current cursor cell
-                if i == self.cursor_coordinates.row && j == self.cursor_coordinates.col {
+                if i == self.cursor_coordinates.row
+                    && j == self.cursor_coordinates.col
+                    && !self.mouse_used
+                {
                     Board::render_cell(frame, square, Color::LightBlue, None);
                 }
                 // Draw the cell magenta if the king is getting checked
diff --git a/src/handler.rs b/src/handler.rs
index 128417e..24bcd7e 100644
--- a/src/handler.rs
+++ b/src/handler.rs
@@ -1,8 +1,11 @@
 use crate::{
     app::{App, AppResult},
+    board::Coord,
     constants::Pages,
 };
-use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
+use ratatui::crossterm::event::{
+    KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseButton, MouseEvent, MouseEventKind,
+};
 
 /// Handles the key events and updates the state of [`App`].
 pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
@@ -10,6 +13,17 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
         // crossterm on Windows sends Release and Repeat events as well, which we ignore.
         return Ok(());
     }
+    if app.board.mouse_used {
+        app.board.mouse_used = false;
+        if app.board.selected_coordinates != Coord::undefined() {
+            app.board.cursor_coordinates = app.board.selected_coordinates;
+            app.board.selected_coordinates = Coord::undefined();
+        } else {
+            app.board.cursor_coordinates.col = 4;
+            app.board.cursor_coordinates.row = 4;
+        }
+    }
+
     match key_event.code {
         // Exit application on `q`
         KeyCode::Char('q') => {
@@ -87,3 +101,43 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
     }
     Ok(())
 }
+
+pub fn handle_mouse_events(mouse_event: MouseEvent, app: &mut App) -> AppResult<()> {
+    // Mouse control only implemented for actual game
+    if app.current_page == Pages::Home {
+        return Ok(());
+    }
+    if mouse_event.kind == MouseEventKind::Down(MouseButton::Left) {
+        if app.board.is_checkmate || app.board.is_draw {
+            return Ok(());
+        }
+
+        if app.show_color_popup || app.show_help_popup {
+            return Ok(());
+        }
+
+        // If there is a promotion to be done the top_x, top_y, width and height
+        // values are updated accordingly
+        if app.board.is_promotion {
+            let x = (mouse_event.column - app.board.top_x) / app.board.width;
+            let y = (mouse_event.row - app.board.top_y) / app.board.height;
+            if x > 3 || y > 0 {
+                return Ok(());
+            }
+            app.board.promotion_cursor = x as i8;
+            app.board.promote_piece();
+        }
+        if mouse_event.column < app.board.top_x || mouse_event.row < app.board.top_y {
+            return Ok(());
+        }
+        let x = (mouse_event.column - app.board.top_x) / app.board.width;
+        let y = (mouse_event.row - app.board.top_y) / app.board.height;
+        if x > 7 || y > 7 {
+            return Ok(());
+        }
+        app.board.mouse_used = true;
+        let coords: Coord = Coord::new(y as u8, x as u8);
+        app.board.move_selected_piece_cursor_mouse(coords);
+    }
+    Ok(())
+}
diff --git a/src/main.rs b/src/main.rs
index 62fb800..0feef37 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -4,7 +4,7 @@ extern crate chess_tui;
 use chess_tui::app::{App, AppResult};
 use chess_tui::constants::{home_dir, DisplayMode};
 use chess_tui::event::{Event, EventHandler};
-use chess_tui::handler::handle_key_events;
+use chess_tui::handler::{handle_key_events, handle_mouse_events};
 use chess_tui::tui::Tui;
 use clap::Parser;
 use std::fs::{self, File};
@@ -22,6 +22,11 @@ struct Args {
 }
 
 fn main() -> AppResult<()> {
+    // Used to enable mouse capture
+    ratatui::crossterm::execute!(
+        std::io::stdout(),
+        ratatui::crossterm::event::EnableMouseCapture
+    )?;
     // Parse the cli arguments
     let args = Args::parse();
 
@@ -69,12 +74,18 @@ fn main() -> AppResult<()> {
         match tui.events.next()? {
             Event::Tick => app.tick(),
             Event::Key(key_event) => handle_key_events(key_event, &mut app)?,
-            Event::Mouse(_) | Event::Resize(_, _) => {}
+            Event::Mouse(mouse_event) => handle_mouse_events(mouse_event, &mut app)?,
+            Event::Resize(_, _) => {}
         }
     }
 
     // Exit the user interface.
     ratatui::try_restore()?;
+    // Free up the mouse, otherwise it will remain linked to the terminal
+    ratatui::crossterm::execute!(
+        std::io::stdout(),
+        ratatui::crossterm::event::DisableMouseCapture
+    )?;
 
     Ok(())
 }
diff --git a/src/popups.rs b/src/popups.rs
index 5da22c8..dad01fd 100644
--- a/src/popups.rs
+++ b/src/popups.rs
@@ -68,7 +68,7 @@ pub fn render_end_popup(frame: &mut Frame, sentence: &str) {
     frame.render_widget(paragraph, area);
 }
 
-pub fn render_promotion_popup(frame: &mut Frame, app: &App) {
+pub fn render_promotion_popup(frame: &mut Frame, app: &mut App) {
     let block = Block::default()
         .title("Pawn promotion")
         .borders(Borders::ALL)
@@ -116,6 +116,12 @@ pub fn render_promotion_popup(frame: &mut Frame, app: &App) {
         )
         .split(inner_popup_layout_vertical[1]);
 
+    // When a promotion is happening, the mouse should be able to know where the icons are
+    app.board.top_x = inner_popup_layout_horizontal[0].x;
+    app.board.top_y = inner_popup_layout_horizontal[0].y;
+    app.board.width = inner_popup_layout_horizontal[0].width;
+    app.board.height = inner_popup_layout_horizontal[0].height;
+
     let display_mode = &app.board.display_mode;
 
     let queen_p = Paragraph::new(Queen::to_string(display_mode))
diff --git a/src/ui.rs b/src/ui.rs
index a7d1bf0..14f50f2 100644
--- a/src/ui.rs
+++ b/src/ui.rs
@@ -143,7 +143,7 @@ pub fn render_menu_ui(frame: &mut Frame, app: &App, main_area: Rect) {
 }
 
 // Method to render the game board and handle game popups
-pub fn render_game_ui(frame: &mut Frame, app: &App, main_area: Rect) {
+pub fn render_game_ui(frame: &mut Frame, app: &mut App, main_area: Rect) {
     let main_layout_horizontal = Layout::default()
         .direction(Direction::Vertical)
         .constraints(
@@ -186,7 +186,7 @@ pub fn render_game_ui(frame: &mut Frame, app: &App, main_area: Rect) {
     // We render the board_block in the center layout made above
     frame.render_widget(board_block.clone(), main_layout_vertical[1]);
 
-    // We make the inside of the board
+    // We make the inside of the board and update its starting coordinates
     app.board
         .board_render(board_block.inner(main_layout_vertical[1]), frame);
 


Test patch: