diff --git a/ui/src/main.rs b/ui/src/main.rs index 46da03b..0dd41f8 100644 --- a/ui/src/main.rs +++ b/ui/src/main.rs @@ -14,14 +14,13 @@ use uuid::Uuid; #[tokio::main] async fn main() -> anyhow::Result<(), eframe::Error> { - // Set up logging let env = Env::default().filter_or("MY_LOG_LEVEL", "INFO"); env_logger::init_from_env(env); warn!("Initialized logger"); let options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default() - .with_fullscreen(false) + .with_fullscreen(true) .with_min_inner_size(egui::vec2(800.0, 800.0)) .with_inner_size(egui::vec2(1920.0, 1080.0)), ..Default::default() @@ -100,6 +99,7 @@ struct GameState { game_over: Option, available_moves: Option>, turn_player: Option, + move_history: Vec , } impl Default for GameState { @@ -115,6 +115,7 @@ impl Default for GameState { game_over: None, available_moves: Some(cuccfck), turn_player: Some("white".to_string()), + move_history: Vec::new(), } } } @@ -127,6 +128,7 @@ enum AppState { FindingMatch, InGame, GameOver, + Settings, } struct ChessApp { @@ -139,14 +141,36 @@ struct ChessApp { // Channels for communication with network tasks tx_to_network: Option>, rx_from_network: Option>, - // UI state selected_square: Option<(usize, usize)>, + //Settings + fullscreen: bool, + resolutions: Vec<(u32, u32)>, + pending_settings: PendingSettings, + selected_resolution: usize, + dark_mode: bool, +} + +#[derive(Default)] +struct PendingSettings { + fullscreen: bool, + selected_resolution: usize, } impl Default for ChessApp { fn default() -> Self { Self { + fullscreen: false, + resolutions: vec![ + (1280, 720), + (1600, 900), + (1920, 1080), + (2560, 1440), + (3840, 2160), + ], + pending_settings: PendingSettings::default(), + selected_resolution: 2, + dark_mode: false, state: AppState::MainMenu, game_state: Arc::new(Mutex::new(GameState::default())), server_port: "9001".to_string(), @@ -155,6 +179,7 @@ impl Default for ChessApp { rx_from_network: None, selected_square: None, server_ip: "127.0.0.1".to_string(), + // TODO: for the online server (reverse proxy?) start_local_server_instance: false, } @@ -193,7 +218,18 @@ impl ChessApp { } }); } - + fn apply_settings(&mut self, ctx: &egui::Context) { + self.fullscreen = self.pending_settings.fullscreen; + self.selected_resolution = self.pending_settings.selected_resolution; + + if let Some(resolution) = self.resolutions.get(self.selected_resolution) { + ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize( + egui::Vec2::new(resolution.0 as f32, resolution.1 as f32) + )); + } + + ctx.send_viewport_cmd(egui::ViewportCommand::Fullscreen(self.fullscreen)); + } async fn network_handler( server_port: String, server_ip: String, @@ -485,221 +521,473 @@ impl eframe::App for ChessApp { // Get current game state let game_state = self.game_state.lock().unwrap().clone(); + let screen_size = ctx.screen_rect().size(); + let base_size = screen_size.x.min(screen_size.y); + + + // Determine background color based on dark mode setting + let background_color = if self.dark_mode { + egui::Color32::from_rgb(27, 27, 27) // Dark mode + } else { + egui::Color32::from_rgb(235, 235, 235) // Light mode + }; + // Also adjust text colors if needed + let text_color = if self.dark_mode { + egui::Color32::WHITE + } else { + egui::Color32::BLACK + }; + + // Update the visual style based on dark mode + let mut visuals = ctx.style().visuals.clone(); + + if self.dark_mode { + // Dark mode visuals + visuals = egui::Visuals::dark(); + // Adjust specific colors if needed + visuals.widgets.noninteractive.bg_fill = egui::Color32::from_rgb(40, 40, 40); + visuals.widgets.inactive.bg_fill = egui::Color32::from_rgb(60, 60, 60); + visuals.widgets.hovered.bg_fill = egui::Color32::from_rgb(70, 70, 70); + visuals.widgets.active.bg_fill = egui::Color32::from_rgb(80, 80, 80); + visuals.faint_bg_color = egui::Color32::from_rgb(50, 50, 50); + visuals.extreme_bg_color = egui::Color32::from_rgb(20, 20, 20); + visuals.code_bg_color = egui::Color32::from_rgb(40, 40, 40); + visuals.panel_fill = background_color; + } else { + // Light mode visuals + visuals = egui::Visuals::light(); + visuals.widgets.noninteractive.bg_fill = egui::Color32::from_rgb(210, 210, 210); + visuals.widgets.inactive.bg_fill = egui::Color32::from_rgb(190,190,190); + visuals.widgets.hovered.bg_fill = egui::Color32::from_rgb(180,180,180); + visuals.widgets.active.bg_fill = egui::Color32::from_rgb(170,170,170); + visuals.faint_bg_color = egui::Color32::from_rgb(200,200,200); + visuals.extreme_bg_color = egui::Color32::from_rgb(230,230,230); + visuals.code_bg_color = egui::Color32::from_rgb(210,210,210); + visuals.panel_fill = background_color; + } + + // Apply the updated visuals + ctx.set_visuals(visuals); + + match self.state { AppState::MainMenu => { - egui::CentralPanel::default().show(ctx, |ui| { + // Proportional sizing + let button_width = base_size*0.4; + let button_height = base_size*0.1; + let font_size = base_size*0.025; + let heading_size=base_size*0.1; + let spacing_size = base_size*0.07; + + // Set background color for the entire panel + egui::CentralPanel::default() + .frame(egui::Frame::default().fill(background_color)) + .show(ctx, |ui| { + ui.vertical_centered(|ui| { + // Style the heading based on dark mode + let heading_color = if self.dark_mode { + egui::Color32::WHITE + } else { + egui::Color32::BLACK + }; + + ui.heading(egui::RichText::new("♞ Knightly ♞").color(heading_color)); + ui.add_space(30.0); + + ui.horizontal(|ui| { + ui.label(egui::RichText::new("Username:").color(heading_color)); + ui.text_edit_singleline(&mut self.username); + }); + + ui.add_space(20.0); + + // Create styled button + let button_text_color = if self.dark_mode { + egui::Color32::WHITE + } else { + egui::Color32::BLACK + }; + + if ui.add_sized( + egui::Vec2::new(button_width, button_height), + egui::Button::new( + egui::RichText::new("Online Play") + .size(font_size) + .color(button_text_color) + ) + ).clicked() { + self.server_ip = "127.0.0.1".to_string(); + self.connect_to_server(); + } + + ui.add_space(20.0); + + if ui.add_sized( + egui::Vec2::new(button_width, button_height), + egui::Button::new( + egui::RichText::new("Private Play") + .size(font_size) + .color(button_text_color) + ) + ).clicked() { + self.state = AppState::PrivatePlayConnect; + } + + ui.add_space(20.0); + + if ui.add_sized( + egui::Vec2::new(button_width, button_height), + egui::Button::new( + egui::RichText::new("Settings") + .size(font_size) + .color(button_text_color) + ) + ).clicked() { + self.state = AppState::Settings; + } + + ui.add_space(20.0); + + if ui.add_sized( + egui::Vec2::new(button_width, button_height), + egui::Button::new( + egui::RichText::new("Quit") + .size(font_size) + .color(button_text_color) + ) + ).clicked() { + std::process::exit(0); + } + }); + }); + } + AppState::Settings => { + egui::CentralPanel::default() + .frame(egui::Frame::default().fill(background_color)) + .show(ctx, |ui| { ui.vertical_centered(|ui| { - ui.heading("♞ Knightly ♞"); + ui.heading("Settings"); ui.add_space(30.0); + // Fullscreen toggle ui.horizontal(|ui| { - ui.label("Username:"); - ui.text_edit_singleline(&mut self.username); - }); - - ui.add_space(20.0); - - if ui.button("Online Play").clicked() { - // TODO: change to dns - self.server_ip = "127.0.0.1".to_string(); - self.connect_to_server(); - } - - if ui.button("Private Play").clicked() { - self.state = AppState::PrivatePlayConnect; - } - - if ui.button("Quit").clicked() { - std::process::exit(0); - } - }); - }); - } - - AppState::PrivatePlayConnect => { - egui::CentralPanel::default().show(ctx, |ui| { - ui.vertical_centered(|ui| { - ui.horizontal(|ui| { - ui.label("Server ip address"); - ui.text_edit_singleline(&mut self.server_ip); - }); - - ui.horizontal(|ui| { - ui.label("Server Port:"); - ui.text_edit_singleline(&mut self.server_port); - }); - - ui.horizontal(|ui| { - ui.checkbox(&mut self.start_local_server_instance, "Host Server"); - }); - - ui.add_space(20.0); - - if ui.button("Play").clicked() { - if self.start_local_server_instance == true { - let path = if cfg!(windows) { - "./server.exe" - } else { - "./server" - }; - - if !Path::new(path).exists() { - error!("Server binary does not exist, cfg: {}", path); - } else { - let _ = Command::new(path).spawn(); - std::thread::sleep(std::time::Duration::from_secs(1)); - } + ui.label("Fullscreen:"); + if ui.checkbox(&mut self.pending_settings.fullscreen, "").changed() { + // If enabling fullscreen, we might want to disable resolution selection } - self.connect_to_server(); - } - }) - }); - } + }); + ui.add_space(10.0); - AppState::Connecting => { - egui::CentralPanel::default().show(ctx, |ui| { - ui.vertical_centered(|ui| { - ui.heading("Connecting to Server..."); - ui.add_space(20.0); - ui.spinner(); - }); - - if ui.button("Cancel").clicked() { - info!("Returning to menu from before connecting to the server"); - self.state = AppState::MainMenu; - } - }); - } - - AppState::FindingMatch => { - egui::CentralPanel::default().show(ctx, |ui| { - ui.vertical_centered(|ui| { - ui.heading("Finding Match..."); - ui.add_space(20.0); - ui.label("Waiting for an opponent..."); - ui.spinner(); - - ui.add_space(20.0); - if ui.button("cancel").clicked() { - if let Some(tx) = &self.tx_to_network { - warn!("Closing connection to server, cancelled match findig!"); - let _ = tx.send(ClientEvent::CloseConnection); - self.state = AppState::MainMenu; - } - } - }); - }); - } - - AppState::InGame => { - // Draw menu bar - egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| { - ui.horizontal(|ui| { - if ui.button("Main Menu").clicked() { - *self = ChessApp::default(); - } - - if ui.button("Resign").clicked() { - if let Some(tx) = &self.tx_to_network { - let _ = tx.send(ClientEvent::Resign); - } - } - - ui.separator(); - - if let Some(color) = &game_state.player_color { - ui.label(format!("You are: {}", color)); - } - - if let Some(opponent) = &game_state.opponent_name { - ui.label(format!("vs: {}", opponent)); - } - }); - }); - - // Draw chess board - egui::CentralPanel::default().show(ctx, |ui| { - ui.vertical_centered(|ui| { - let board = self.fen_to_board(&game_state.fen); - let is_white = game_state - .player_color - .as_ref() - .map_or(true, |c| c == "white"); - - let available_size = ui.available_size(); - let board_size = available_size.x.min(available_size.y) * 0.9; - let tile_size = board_size / 8.0; - - let (response, painter) = ui.allocate_painter( - egui::Vec2::new(board_size, board_size), - egui::Sense::click(), - ); - - let board_top_left = response.rect.left_top(); - - // Draw board and pieces - for row in 0..8 { - for col in 0..8 { - let (display_row, display_col) = if is_white { - (row, col) - } else { - (7 - row, 7 - col) - }; - - let color = if (row + col) % 2 == 0 { - egui::Color32::from_rgb(240, 217, 181) // Light - } else { - egui::Color32::from_rgb(181, 136, 99) // Dark - }; - - let rect = egui::Rect::from_min_size( - egui::Pos2::new( - board_top_left.x + col as f32 * tile_size, - board_top_left.y + row as f32 * tile_size, - ), - egui::Vec2::new(tile_size, tile_size), - ); - - painter.rect_filled(rect, 0.0, color); - - // Draw piece - let piece_char = board[display_row][display_col]; - if piece_char != ' ' { - let symbol = self.chess_char_to_piece(piece_char); - let font_id = egui::FontId::proportional(tile_size * 0.8); - let text_color = if piece_char.is_uppercase() { - egui::Color32::WHITE - } else { - egui::Color32::BLACK - }; - - painter.text( - rect.center(), - egui::Align2::CENTER_CENTER, - symbol, - font_id, - text_color, - ); - } - - // Draw selection - if let Some((sel_row, sel_col)) = self.selected_square { - if sel_row == display_row && sel_col == display_col { - painter.rect_stroke( - rect, - 0.0, - egui::Stroke::new(3.0, egui::Color32::RED), - egui::StrokeKind::Middle, + // Resolution dropdown + ui.horizontal(|ui| { + ui.label("Resolution:"); + egui::ComboBox::new("resolution_combo", "") + .selected_text(format!( + "{}x{}", + self.resolutions[self.pending_settings.selected_resolution].0, + self.resolutions[self.pending_settings.selected_resolution].1 + )) + .show_ui(ui, |ui| { + for (i, &(width, height)) in self.resolutions.iter().enumerate() { + ui.selectable_value( + &mut self.pending_settings.selected_resolution, + i, + format!("{}x{}", width, height), ); } + }); + }); + ui.add_space(10.0); + //dark mode toggle + ui.horizontal(|ui| { + ui.label("Dark mode"); + if ui.checkbox(&mut self.dark_mode, "").changed() { + info!("Dark mode changed to: {}", self.dark_mode); } + }); + // Apply and Cancel buttons + ui.horizontal(|ui| { + if ui.add_sized([140.0, 40.0], egui::Button::new("Apply")).clicked() { + self.apply_settings(ctx); + self.state = AppState::MainMenu; + } + + if ui.add_sized([140.0, 40.0], egui::Button::new("Cancel")).clicked() { + self.state = AppState::MainMenu; + } + }); + }); + }); + } + AppState::PrivatePlayConnect => { + let button_width = base_size*0.4; + let button_height = base_size*0.1; + let font_size = base_size*0.025; + + let text_color = if self.dark_mode { + egui::Color32::WHITE + } else { + egui::Color32::BLACK + }; + + egui::CentralPanel::default() + .frame(egui::Frame::default().fill(background_color)) + .show(ctx, |ui| { + ui.vertical_centered(|ui| { + ui.horizontal(|ui| { + ui.label(egui::RichText::new("Server ip address").color(text_color)); + ui.text_edit_singleline(&mut self.server_ip); + }); - // Handle clicks - if response.clicked() { - if let Some(click_pos) = ui.ctx().pointer_interact_pos() { - if rect.contains(click_pos) { - let res = self.handle_click(display_row, display_col); + ui.horizontal(|ui| { + ui.label(egui::RichText::new("Server Port:").color(text_color)); + ui.text_edit_singleline(&mut self.server_port); + }); + + ui.horizontal(|ui| { + ui.label(egui::RichText::new("Host Server").color(text_color)); + ui.checkbox(&mut self.start_local_server_instance, ""); + }); + + ui.add_space(20.0); + if ui.add_sized( + egui::Vec2::new(button_width, button_height), + egui::Button::new( + egui::RichText::new("Play") + .size(font_size) + .color(text_color) + ) + ).clicked() { + if self.start_local_server_instance == true { + let path = if cfg!(windows) { + "./server.exe" + } else { + "./server" + }; + + if !Path::new(path).exists() { + error!("Server binary does not exist, cfg: {}", path); + } else { + let _ = Command::new(path).spawn(); + std::thread::sleep(std::time::Duration::from_secs(1)); + } + } + self.connect_to_server(); + } + ui.add_space(20.0); + if ui.add_sized( + egui::Vec2::new(button_width, button_height), + egui::Button::new( + egui::RichText::new("Return to main menu") + .size(font_size) + .color(text_color) + ) + ).clicked(){ + self.state=AppState::MainMenu; + } + }) + }); +} + + AppState::Connecting => { + let text_color = if self.dark_mode { + egui::Color32::WHITE + } else { + egui::Color32::BLACK + }; + + egui::CentralPanel::default() + .frame(egui::Frame::default().fill(background_color)) + .show(ctx, |ui| { + ui.vertical_centered(|ui| { + ui.heading(egui::RichText::new("Connecting to Server...").color(text_color)); + ui.add_space(20.0); + ui.spinner(); + + ui.add_space(20.0); + if ui.button( + egui::RichText::new("Cancel").color(text_color) + ).clicked() { + info!("Returning to menu from before connecting to the server"); + self.state = AppState::MainMenu; + } + }); + }); +} + + AppState::FindingMatch => { + let text_color = if self.dark_mode { + egui::Color32::WHITE + } else { + egui::Color32::BLACK + }; + + egui::CentralPanel::default() + .frame(egui::Frame::default().fill(background_color)) + .show(ctx, |ui| { + ui.vertical_centered(|ui| { + ui.heading(egui::RichText::new("Finding Match...").color(text_color)); + ui.add_space(20.0); + ui.label(egui::RichText::new("Waiting for an opponent...").color(text_color)); + ui.spinner(); + + ui.add_space(20.0); + if ui.button( + egui::RichText::new("cancel").color(text_color) + ).clicked() { + if let Some(tx) = &self.tx_to_network { + warn!("Closing connection to server, cancelled match finding!"); + let _ = tx.send(ClientEvent::CloseConnection); + self.state = AppState::MainMenu; + } + } + }); + }); +} + + AppState::InGame => { + let text_color = if self.dark_mode { + egui::Color32::WHITE + } else { + egui::Color32::BLACK + }; + + // Draw menu bar + egui::TopBottomPanel::top("menu_bar") + .frame(egui::Frame::default().fill(background_color)) + .show(ctx, |ui| { + ui.horizontal(|ui| { + if ui.button( + egui::RichText::new("Main Menu").color(text_color) + ).clicked() { + *self = ChessApp::default(); + } + + if ui.button( + egui::RichText::new("Resign").color(text_color) + ).clicked() { + if let Some(tx) = &self.tx_to_network { + let _ = tx.send(ClientEvent::Resign); + } + } + + ui.separator(); + + if let Some(color) = &game_state.player_color { + ui.label(egui::RichText::new(format!("You are: {}", color)).color(text_color)); + } + + if let Some(opponent) = &game_state.opponent_name { + ui.label(egui::RichText::new(format!("vs: {}", opponent)).color(text_color)); + } + }); + }); + + // Main content area with chess board and move history + egui::CentralPanel::default() + .frame(egui::Frame::default().fill(background_color)) + .show(ctx, |ui| { + let total_width = ui.available_width(); + let total_height = ui.available_height(); + + // Calculate sizes + let board_max_width = total_width * 0.75; + let board_max_height = total_height * 0.95; + let board_size = board_max_width.min(board_max_height); + let history_width = total_width * 0.20; + + // Add margin around the board (20 pixels on each side) + let board_margin = 20.0; + let effective_board_size = board_size - 2.0 * board_margin; + + // Center the entire content horizontally and vertically + ui.vertical_centered(|ui| { + ui.horizontal_centered(|ui| { + // Chess board with margin (left side) + ui.vertical(|ui| { + // Add vertical spacing above the board + ui.add_space(board_margin); + + ui.horizontal(|ui| { + // Add horizontal spacing to the left of the board + ui.add_space(board_margin); + + let (board_response, board_painter) = ui.allocate_painter( + egui::Vec2::new(effective_board_size, effective_board_size), + egui::Sense::click(), + ); + + let board = self.fen_to_board(&game_state.fen); + let is_white = game_state + .player_color + .as_ref() + .map_or(true, |c| c == "white"); + let tile_size = effective_board_size / 8.0; + let board_top_left = board_response.rect.left_top(); + + // Draw board and pieces + for row in 0..8 { + for col in 0..8 { + let (display_row, display_col) = if is_white { + (row, col) + } else { + (7 - row, 7 - col) + }; + + let color = if (row + col) % 2 == 0 { + egui::Color32::from_rgb(240, 217, 181) // Light + } else { + egui::Color32::from_rgb(181, 136, 99) // Dark + }; + + let rect = egui::Rect::from_min_size( + egui::Pos2::new( + board_top_left.x + col as f32 * tile_size, + board_top_left.y + row as f32 * tile_size, + ), + egui::Vec2::new(tile_size, tile_size), + ); + + board_painter.rect_filled(rect, 0.0, color); + + // Draw piece + let piece_char = board[display_row][display_col]; + if piece_char != ' ' { + let symbol = self.chess_char_to_piece(piece_char); + let font_id = egui::FontId::proportional(tile_size * 0.8); + let text_color = if piece_char.is_uppercase() { + egui::Color32::WHITE + } else { + egui::Color32::BLACK + }; + + board_painter.text( + rect.center(), + egui::Align2::CENTER_CENTER, + symbol, + font_id, + text_color, + ); + } + + // Draw selection + if let Some((sel_row, sel_col)) = self.selected_square { + if sel_row == display_row && sel_col == display_col { + board_painter.rect_stroke( + rect, + 0.0, + egui::Stroke::new(3.0, egui::Color32::RED), + egui::StrokeKind::Inside, + ); + } + } + + // Handle clicks + if board_response.clicked() { + if let Some(click_pos) = ui.ctx().pointer_interact_pos() { + if rect.contains(click_pos) { + let res = self.handle_click(display_row, display_col); match res { Ok(_) => { if let Some(tx) = &self.tx_to_network { @@ -720,33 +1008,123 @@ impl eframe::App for ChessApp { error!("{}", e); } } + } } } } } - } + + // Add horizontal spacing to the right of the board + ui.add_space(board_margin); + }); + + // Add vertical spacing below the board + ui.add_space(board_margin); + }); + + // Add spacing between board and move history + ui.add_space(15.0); + + // Move History (right side) - match the board height including margins + ui.vertical(|ui| { + egui::Frame::default() + .fill(if self.dark_mode { + egui::Color32::from_rgb(60, 60, 60) + } else { + egui::Color32::from_rgb(240, 240, 240) + }) + .stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(200, 200, 200))) + .inner_margin(egui::Margin::same(8)) + .show(ui, |ui| { + ui.set_width(history_width); + ui.set_height(board_size); // Match total board height including margins + + ui.vertical_centered(|ui| { + ui.heading(egui::RichText::new("Move History").color(text_color)); + ui.separator(); + + // Scroll area for move history + egui::ScrollArea::vertical() + .max_height(board_size - 50.0) // Based on board height + .show(ui, |ui| { + // Use actual move history from game_state + if let Ok(game_state) = self.game_state.lock() { + for (i, move_text) in game_state.move_history.iter().enumerate() { + ui.horizontal(|ui| { + // Alternate background + if i % 2 == 0 { + ui.visuals_mut().widgets.noninteractive.bg_fill = + if self.dark_mode { + egui::Color32::from_rgb(70, 70, 70) + } else { + egui::Color32::from_rgb(250, 250, 250) + }; + } else { + ui.visuals_mut().widgets.noninteractive.bg_fill = + if self.dark_mode { + egui::Color32::from_rgb(50, 50, 50) + } else { + egui::Color32::from_rgb(230, 230, 230) + }; + } + + ui.label(egui::RichText::new(move_text.to_string()).size(16.0).color(text_color)); + + if ui.small_button("📋").clicked() { + info!("Copy move: {}", move_text); + } + }); + + if i < game_state.move_history.len() - 1 { + ui.add_space(2.0); + } + } + + if game_state.move_history.is_empty() { + ui.vertical_centered(|ui| { + ui.add_space(20.0); + ui.label(egui::RichText::new("No moves yet").size(16.0).color(text_color)); + ui.label(egui::RichText::new("Game will start soon...").size(14.0).color(text_color)); + }); + } + } + }); + }); + }); }); }); - } + }); + }); +} AppState::GameOver => { - egui::CentralPanel::default().show(ctx, |ui| { - ui.vertical_centered(|ui| { - ui.heading("Game Over"); - ui.add_space(20.0); + let text_color = if self.dark_mode { + egui::Color32::WHITE + } else { + egui::Color32::BLACK + }; + + egui::CentralPanel::default() + .frame(egui::Frame::default().fill(background_color)) + .show(ctx, |ui| { + ui.vertical_centered(|ui| { + ui.heading(egui::RichText::new("Game Over").color(text_color)); + ui.add_space(20.0); - if let Some(reason) = &game_state.game_over { - ui.label(format!("Result: {}", reason)); - } + if let Some(reason) = &game_state.game_over { + ui.label(egui::RichText::new(format!("Result: {}", reason)).color(text_color)); + } - ui.add_space(20.0); + ui.add_space(20.0); - if ui.button("Back to Main Menu").clicked() { - *self = ChessApp::default(); - } - }); - }); - } + if ui.button( + egui::RichText::new("Back to Main Menu").color(text_color) + ).clicked() { + *self = ChessApp::default(); + } + }); + }); +} } // Request repaint to keep UI responsive @@ -968,6 +1346,7 @@ mod tests { let new_fen = "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1".to_string(); let message = ServerMessage2::UIUpdate { fen: new_fen.clone(), + turn_player: "white".to_string(), }; tx.send(message).unwrap(); @@ -1051,7 +1430,7 @@ mod tests { to_square: BoardSquare { x: 2, y: 2 }, promotion_piece: None, }; - let move_event = ClientEvent::Move { step: chess_move }; + let move_event = ClientEvent::Move { step: chess_move, turn_player:"white".to_string() }; let serialized = serde_json::to_string(&move_event).unwrap(); assert!(serialized.contains("Move")); @@ -1093,7 +1472,7 @@ mod tests { r#"{"UIUpdate":{"fen":"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"}}"#; let message: ServerMessage2 = serde_json::from_str(ui_update_json).unwrap(); match message { - ServerMessage2::UIUpdate { fen } => { + ServerMessage2::UIUpdate { fen , turn_player} => { assert!(fen.contains("rnbqkbnr")); } _ => panic!("Expected UIUpdate message"),