1use std::cmp::max;
2
3use ggez::event::{KeyCode, KeyMods, MouseButton};
4use ggez::{event, graphics, Context, GameError, GameResult};
5use log::{debug, info};
6use unicode_segmentation::UnicodeSegmentation;
7
8use crate::{
9 Align, Button, Chess, GameState, Square, Theme, ALL_SQUARES, BOARD_CELL_PX_SIZE, BOARD_PX_SIZE,
10 BOARD_SIZE, INDEX_THEME, NUM_THEMES, SIDE_SCREEN_PX_SIZE, THEMES,
11};
12
13#[derive(Debug)]
15pub struct ChessGui {
16 pub(crate) chess: Chess,
17 theme: Theme,
18 buttons: Vec<Button>,
19}
20
21impl ChessGui {
22 pub fn new(chess: Chess, theme: Theme, buttons: Vec<Button>) -> Self {
24 ChessGui {
25 chess,
26 theme,
27 buttons,
28 }
29 }
30
31 pub fn reset(&mut self) {
33 self.chess.reset();
34 self.buttons.clear();
35 self.init_buttons();
36 }
37
38 pub fn set_theme(&mut self, theme: Theme) {
49 self.theme = theme;
50 }
51
52 pub unsafe fn next_theme(&mut self) {
58 INDEX_THEME = (INDEX_THEME + 1) % NUM_THEMES;
59 self.theme = THEMES[INDEX_THEME % 6];
60 }
61
62 pub fn add_button(&mut self, button: Button) {
64 self.buttons.push(button);
65 }
66
67 fn init_buttons(&mut self) {
69 self.buttons.push(
70 Button::new(
71 "theme",
72 true,
73 graphics::Rect::new(
74 BOARD_PX_SIZE.0 + SIDE_SCREEN_PX_SIZE.0 - 70.0,
75 20.0,
76 50.0,
77 50.0,
78 ),
79 graphics::Color::new(1.0, 1.0, 1.0, 1.0),
80 "Theme",
81 Align::Center,
82 Some(|chess_gui| unsafe {
83 chess_gui.next_theme();
84 }),
85 )
86 .set_image(self.theme.theme_icon_path),
87 );
88 self.buttons.push(Button::new(
89 "undo",
90 true,
91 graphics::Rect::new(
92 BOARD_PX_SIZE.0 + 20.0,
93 SIDE_SCREEN_PX_SIZE.1 - 210.0,
94 150.0,
95 50.0,
96 ),
97 graphics::Color::new(0.65, 0.44, 0.78, 1.0),
98 "Undo",
99 Align::Center,
100 Some(|chess_gui| {
101 chess_gui.chess.undo();
102 }),
103 ));
104 self.buttons.push(Button::new(
105 "declare-draw",
106 false,
107 graphics::Rect::new(
108 BOARD_PX_SIZE.0 + 190.0,
109 SIDE_SCREEN_PX_SIZE.1 - 210.0,
110 150.0,
111 50.0,
112 ),
113 graphics::Color::new(0.89, 0.8, 0.35, 1.0),
114 "Declare Draw",
115 Align::Center,
116 Some(|chess_gui| {
117 chess_gui.chess.declare_draw();
118 }),
119 ));
120 self.buttons.push(Button::new(
121 "offer-draw",
122 true,
123 graphics::Rect::new(
124 BOARD_PX_SIZE.0 + 20.0,
125 SIDE_SCREEN_PX_SIZE.1 - 140.0,
126 150.0,
127 50.0,
128 ),
129 graphics::Color::new(1.0, 0.64, 0.38, 1.0),
130 "Offer Draw",
131 Align::Center,
132 Some(|chess_gui| {
133 chess_gui.chess.offer_draw();
134 }),
135 ));
136 self.buttons.push(Button::new(
137 "accept-draw",
138 false,
139 graphics::Rect::new(
140 BOARD_PX_SIZE.0 + 190.0,
141 SIDE_SCREEN_PX_SIZE.1 - 140.0,
142 150.0,
143 50.0,
144 ),
145 graphics::Color::new(0.56, 0.78, 0.4, 1.0),
146 "Accept Draw",
147 Align::Center,
148 Some(|chess_gui| {
149 chess_gui.chess.accept_draw();
150 }),
151 ));
152 self.buttons.push(Button::new(
153 "reset",
154 true,
155 graphics::Rect::new(
156 BOARD_PX_SIZE.0 + 20.0,
157 SIDE_SCREEN_PX_SIZE.1 - 70.0,
158 150.0,
159 50.0,
160 ),
161 graphics::Color::new(0.65, 0.44, 0.78, 1.0),
162 "Reset",
163 Align::Center,
164 Some(|chess_gui| {
165 chess_gui.reset();
166 }),
167 ));
168 self.buttons.push(Button::new(
169 "resign",
170 true,
171 graphics::Rect::new(
172 BOARD_PX_SIZE.0 + 190.0,
173 SIDE_SCREEN_PX_SIZE.1 - 70.0,
174 150.0,
175 50.0,
176 ),
177 graphics::Color::new(0.98, 0.3, 0.3, 1.0),
178 "Resign",
179 Align::Center,
180 Some(|chess_gui| {
181 chess_gui.chess.resign(chess_gui.chess.board.side_to_move());
182 }),
183 ));
184 }
185
186 pub fn click(&mut self, x: f32, y: f32) {
188 if x < BOARD_PX_SIZE.0 && self.chess.state.is_ongoing() {
189 self.click_on_board(x, y);
190 } else {
191 self.click_on_side(x, y);
192 }
193 }
194
195 fn click_on_board(&mut self, x: f32, y: f32) {
199 let current_square = Square::from_screen(x, y);
200 debug!("Click at: ({x},{y}) -> on the square: {current_square}");
201 match self.chess.square_focused {
202 Some(square_selected) => self.chess.play(square_selected, current_square),
203 None => {
204 if self
205 .chess
206 .board
207 .color_on_is(current_square, self.chess.board.side_to_move())
208 {
209 self.chess.square_focused = Some(current_square);
210 }
211 }
212 }
213 }
214
215 fn click_on_side(&mut self, x: f32, y: f32) {
219 info!("Click at: ({x},{y}) -> on the side screen");
220 let buttons = self.buttons.clone();
221 for button in buttons.iter() {
222 if button.contains(x, y) {
223 button.clicked(self);
224 }
225 }
226 }
227
228 fn draw_board(&self, ctx: &mut Context) -> GameResult {
230 self.draw_empty_board(ctx)?;
231 self.draw_legal_moves(ctx)?;
232 self.draw_pinned_piece(ctx)?;
233 self.draw_content_board(ctx)?;
234 Ok(())
235 }
236
237 fn draw_empty_board(&self, ctx: &mut Context) -> GameResult {
239 for y in 0..BOARD_SIZE.1 {
240 for x in 0..BOARD_SIZE.0 {
241 let color_index = if (x % 2 == 1 && y % 2 == 1) || (x % 2 == 0 && y % 2 == 0) {
242 0
243 } else {
244 1
245 };
246 let mesh = graphics::MeshBuilder::new()
247 .rectangle(
248 graphics::DrawMode::fill(),
249 graphics::Rect::new(
250 x as f32 * BOARD_CELL_PX_SIZE.0,
251 y as f32 * BOARD_CELL_PX_SIZE.1,
252 BOARD_CELL_PX_SIZE.0,
253 BOARD_CELL_PX_SIZE.1,
254 ),
255 self.theme.board_color[color_index],
256 )?
257 .build(ctx)?;
258 graphics::draw(ctx, &mesh, graphics::DrawParam::default())?;
259 }
260 }
261 Ok(())
262 }
263
264 fn draw_content_board(&self, ctx: &mut Context) -> GameResult {
266 let mut path;
267 let mut image;
268 for square in ALL_SQUARES {
269 if let Some((piece, color)) = self.chess.board.on(square) {
270 path = self.theme.piece_path[color.to_index()][piece.to_index()];
271 image = graphics::Image::new(ctx, path).expect("Image load error");
272 let (x, y) = square.to_screen();
273 let dest_point = [x, y];
274 let image_scale = [0.5, 0.5];
275 let dp = graphics::DrawParam::new()
276 .dest(dest_point)
277 .scale(image_scale);
278 graphics::draw(ctx, &image, dp)?;
279 }
280 }
281 Ok(())
282 }
283
284 fn draw_legal_moves(&self, ctx: &mut Context) -> GameResult {
286 if self.theme.valid_moves_color.is_some() {
287 if let Some(square) = self.chess.square_focused {
288 for dest in self.chess.board.get_legal_moves(square) {
289 let (x, y) = dest.to_screen();
290 let mesh = graphics::MeshBuilder::new()
291 .rectangle(
292 graphics::DrawMode::fill(),
293 graphics::Rect::new(x, y, BOARD_CELL_PX_SIZE.0, BOARD_CELL_PX_SIZE.1),
294 self.theme.valid_moves_color.unwrap(),
295 )?
296 .build(ctx)?;
297 graphics::draw(ctx, &mesh, graphics::DrawParam::default())?;
298 }
299 }
300 }
301 Ok(())
302 }
303
304 fn draw_pinned_piece(&self, ctx: &mut Context) -> GameResult {
306 if self.theme.piece_pinned_path.is_some() {
307 let mut path;
308 let mut image;
309 for square in self.chess.board.pinned() {
310 path = self.theme.piece_pinned_path.unwrap();
311 image = graphics::Image::new(ctx, path).expect("Image load error");
312 let (x, y) = square.to_screen();
313 let dest_point = [x, y];
314 const SCALE: f32 = 1.0;
317 let image_scale = [
318 SCALE * (BOARD_CELL_PX_SIZE.0 / image.width() as f32),
319 SCALE * (BOARD_CELL_PX_SIZE.1 / image.height() as f32),
320 ];
321 let dp = graphics::DrawParam::new()
322 .dest(dest_point)
323 .scale(image_scale);
324 graphics::draw(ctx, &image, dp)?;
325 }
326 } else if self.theme.piece_pinned_color.is_some() {
327 for piece in self.chess.board.pinned() {
328 let (x, y) = piece.to_screen();
329 let mesh = graphics::MeshBuilder::new()
330 .rectangle(
331 graphics::DrawMode::fill(),
332 graphics::Rect::new(x, y, BOARD_CELL_PX_SIZE.0, BOARD_CELL_PX_SIZE.1),
333 self.theme.piece_pinned_color.unwrap(),
334 )?
335 .build(ctx)?;
336 graphics::draw(ctx, &mesh, graphics::DrawParam::default())?;
337 }
338 }
339 Ok(())
340 }
341
342 fn draw_side(&self, ctx: &mut Context) -> GameResult {
344 for button in self.buttons.iter() {
345 button.draw(ctx, self.theme.font_path, self.theme.font_scale)?;
346 }
347 self.draw_timers(ctx)?;
348 self.draw_winner(ctx)?;
349 Ok(())
350 }
351
352 fn draw_timers(&self, ctx: &mut Context) -> GameResult {
354 let bounds_white = graphics::Rect::new(BOARD_PX_SIZE.0 + 20.0, 20.0, 115.0, 50.0);
356 let bounds_black = graphics::Rect::new(BOARD_PX_SIZE.0 + 155.0, 20.0, 115.0, 50.0);
357 let background_mesh_white = graphics::MeshBuilder::new()
358 .rectangle(
359 graphics::DrawMode::fill(),
360 bounds_white,
361 graphics::Color::new(0.5, 0.5, 0.5, 1.0),
362 )?
363 .build(ctx)?;
364 graphics::draw(ctx, &background_mesh_white, graphics::DrawParam::default())?;
365 let background_mesh_black = graphics::MeshBuilder::new()
366 .rectangle(
367 graphics::DrawMode::fill(),
368 bounds_black,
369 graphics::Color::new(0.5, 0.5, 0.5, 1.0),
370 )?
371 .build(ctx)?;
372 graphics::draw(ctx, &background_mesh_black, graphics::DrawParam::default())?;
373
374 let text_white = format!("{}:{}", "--", "--");
376 let font = graphics::Font::new(ctx, self.theme.font_path)?;
377 let text_white = graphics::Text::new((text_white, font, self.theme.font_scale * 2.0));
378 let dest_point = [
379 bounds_white.x + (bounds_white.w - text_white.width(ctx)) / 2.0,
380 bounds_white.y + (bounds_white.h - text_white.height(ctx)) / 2.0,
381 ];
382 graphics::draw(ctx, &text_white, (dest_point,))?;
383 let text_black = format!("{}:{}", "--", "--");
384 let font = graphics::Font::new(ctx, self.theme.font_path)?;
385 let text_black = graphics::Text::new((text_black, font, self.theme.font_scale * 2.0));
386 let dest_point = [
387 bounds_black.x + (bounds_black.w - text_black.width(ctx)) / 2.0,
388 bounds_black.y + (bounds_black.h - text_black.height(ctx)) / 2.0,
389 ];
390 graphics::draw(ctx, &text_black, (dest_point, graphics::Color::BLACK))?;
391
392 Ok(())
393 }
394
395 fn draw_winner(&self, ctx: &mut Context) -> GameResult {
397 let bounds = graphics::Rect::new(
399 BOARD_PX_SIZE.0 + 20.0,
400 90.0,
401 320.0,
402 SIDE_SCREEN_PX_SIZE.1 - 250.0 - 70.0,
403 );
404 let background_mesh = graphics::MeshBuilder::new()
405 .rectangle(
406 graphics::DrawMode::stroke(3.0),
407 bounds,
408 graphics::Color::new(0.7, 0.7, 0.7, 1.0),
409 )?
410 .build(ctx)?;
411 graphics::draw(ctx, &background_mesh, graphics::DrawParam::default())?;
412
413 let text = match self.chess.state {
415 GameState::Ongoing => {
416 let line1 = "Ongoing:".to_string();
417 let line2 = format!("{:?} turn", self.chess.board.side_to_move());
418 let line1_size = line1.graphemes(true).count();
419 let line2_size = line2.graphemes(true).count();
420 let max_size = max(line1_size, line2_size);
421 format!("{: ^max_size$}\n{}", line1, line2)
422 }
423 GameState::Checkmates(color) => {
424 format!("{:?} is checkmate\n\n {:?} win !", color, !color)
425 }
426 GameState::Stalemate => "Draw: Stalemate".to_string(),
427 GameState::DrawAccepted => "Draw: Accepted".to_string(),
428 GameState::DrawDeclared => "Draw: Declared".to_string(),
429 GameState::Resigns(color) => format!("{:?} resigns\n\n {:?} win !", color, !color),
430 };
431 let font = graphics::Font::new(ctx, self.theme.font_path)?;
432 let text = graphics::Text::new((text, font, self.theme.font_scale * 2.0));
433 let dest_point = [
434 bounds.x + (bounds.w - text.width(ctx)) / 2.0,
435 bounds.y + (bounds.h - text.height(ctx)) / 2.0,
436 ];
437 graphics::draw(ctx, &text, (dest_point,))?;
438 Ok(())
439 }
440}
441
442impl event::EventHandler<GameError> for ChessGui {
443 fn update(&mut self, _ctx: &mut Context) -> GameResult {
445 for button in self.buttons.iter_mut() {
446 match button.id {
447 "declare-draw" => {
448 if self.chess.can_declare_draw() {
449 button.enable();
450 } else {
451 button.disable();
452 }
453 }
454 "accept-draw" => {
455 if self.chess.offer_draw {
456 button.enable();
457 } else {
458 button.disable();
459 }
460 }
461 _ => {}
462 }
463 }
464 if self.chess.state.is_finish() {
465 for button in self.buttons.iter_mut() {
466 match button.id {
467 "reset" | "theme" => {}
468 _ => button.disable(),
469 }
470 }
471 }
472 Ok(())
473 }
474
475 fn draw(&mut self, ctx: &mut Context) -> GameResult {
477 graphics::clear(ctx, self.theme.background_color);
479
480 self.draw_board(ctx)?;
482 self.draw_side(ctx)?;
483
484 graphics::present(ctx)?;
487
488 Ok(())
490 }
491
492 fn mouse_button_down_event(&mut self, _ctx: &mut Context, button: MouseButton, x: f32, y: f32) {
494 if button == MouseButton::Left {
495 self.click(x, y);
496 }
497 }
498
499 fn mouse_motion_event(&mut self, ctx: &mut Context, x: f32, y: f32, _dx: f32, _dy: f32) {
501 if x > BOARD_PX_SIZE.0 {
502 let mut on_button = false;
503 for button in self.buttons.iter() {
504 if button.contains(x, y) {
505 on_button = true;
506 break;
507 }
508 }
509 if on_button {
510 ggez::input::mouse::set_cursor_type(ctx, ggez::input::mouse::CursorIcon::Hand);
511 } else {
512 ggez::input::mouse::set_cursor_type(ctx, ggez::input::mouse::CursorIcon::Default);
513 }
514 }
515 }
516
517 fn key_down_event(
527 &mut self,
528 ctx: &mut Context,
529 keycode: KeyCode,
530 keymod: KeyMods,
531 _repeat: bool,
532 ) {
533 match keycode {
534 KeyCode::Escape => event::quit(ctx),
535 KeyCode::R => self.reset(),
536 KeyCode::Z if keymod == KeyMods::CTRL => self.chess.undo(),
537 _ => {}
538 };
539 }
540}
541
542impl Default for ChessGui {
543 fn default() -> Self {
544 let mut chess_gui = ChessGui::new(
545 Default::default(),
546 Default::default(),
547 Vec::with_capacity(7),
548 );
549 chess_gui.init_buttons();
550 chess_gui
551 }
552}