Merge branch 'UI/Appearance' into UI/event
This commit is contained in:
827
ui/src/main.rs
827
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<String>,
|
||||
available_moves: Option<Vec<ChessMove>>,
|
||||
turn_player: Option<String>,
|
||||
move_history: Vec <String>,
|
||||
}
|
||||
|
||||
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<mpsc::UnboundedSender<ClientEvent>>,
|
||||
rx_from_network: Option<mpsc::UnboundedReceiver<ServerMessage2>>,
|
||||
|
||||
// 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"),
|
||||
|
||||
Reference in New Issue
Block a user