use gdk4::cairo::{Context as CairoContext, Matrix, Rectangle}; use gtk4::cairo::Error; use gtk4::{self as gtk, gdk::ffi::GDK_BUTTON_PRIMARY}; use gtk4::{DrawingArea, prelude::*}; use rdraught::draughts::{DraughtsBoard, DraughtsGame, Move, Piece, Player}; use rdraught::position::Position; mod geo2d; use core::f64::consts::PI; use geo2d::Point; use rsvg::SvgHandle; use std::cell::{Cell, RefCell}; use std::rc::Rc; const SQUARE_SIZE: f64 = 1.0; const CROWN_RED: &[u8] = include_bytes!("crown_red.svg"); const CROWN_WHITE: &[u8] = include_bytes!("crown_white.svg"); fn transform_point(p: &Point, m: &Matrix) -> Point { let (x, y) = m.transform_point(p.x(), p.y()); Point::new(x, y) } trait AugmentedRect { fn from_points(tl: Point, br: Point) -> Rectangle; fn center(&self) -> Point; fn tl(&self) -> Point; fn br(&self) -> Point; fn contains(&self, p: &Point) -> bool; } impl AugmentedRect for Rectangle { fn center(&self) -> Point { Point::new( self.x() + self.width() / 2.0, self.y() + self.height() / 2.0, ) } fn tl(&self) -> Point { Point::new(self.x(), self.y()) } fn br(&self) -> Point { Point::new(self.x() + self.width(), self.y() + self.height()) } fn from_points(tl: Point, br: Point) -> Rectangle { Rectangle::new(tl.x(), tl.y(), br.x() - tl.x(), br.y() - tl.y()) } fn contains(&self, p: &Point) -> bool { self.x() < p.x() && self.x() + self.width() > p.x() && self.y() < p.y() && self.y() + self.height() > p.y() } } fn draw_piece( cr: &CairoContext, square: &Rectangle, piece: Piece, crown_red: &SvgHandle, crown_white: &SvgHandle, ) -> Result<(), Error> { if let Piece::NoPiece = piece { Ok(()) } else { cr.save()?; let center = square.center(); let outer_radius = square.width() * 0.3; let vertical_scale_factor = 0.8; let matrix = { let mut m1 = Matrix::identity(); m1.translate(0.0, -(center.y() - outer_radius)); let mut m2 = Matrix::identity(); m2.scale(1.0, vertical_scale_factor); let mut m3 = Matrix::identity(); m3.translate(0.0, center.y() - outer_radius * vertical_scale_factor); Matrix::multiply(&Matrix::multiply(&m1, &m2), &m3) }; cr.set_matrix(Matrix::multiply(&matrix, &cr.matrix())); cr.set_source_rgb(0.0, 0.0, 0.0); let thickness = outer_radius * 0.3; cr.arc( center.x(), center.y() + thickness / 2.0, outer_radius, 0.0, 2.0 * PI, ); cr.rectangle( center.x() - outer_radius, center.y() - thickness / 2.0, outer_radius * 2.0, thickness, ); cr.arc( center.x(), center.y() - thickness / 2.0, outer_radius, 0.0, 2.0 * PI, ); cr.fill().unwrap(); let (color, crowned) = match piece { Piece::NoPiece => return Ok(()), Piece::SimpleRedPawn => ((1.0, 0.0, 0.0), false), Piece::SimpleWhitePawn => ((1.0, 1.0, 1.0), false), Piece::CrownedRedPawn => ((1.0, 0.0, 0.0), true), Piece::CrownedWhitePawn => ((1.0, 1.0, 1.0), true), }; let radius = square.width() * 0.275; cr.set_source_rgb(color.0, color.1, color.2); cr.arc( center.x(), center.y() - thickness / 2.0, radius, 0.0, 2.0 * PI, ); cr.fill()?; cr.restore()?; if crowned { let renderer = match piece.player() { Some(Player::Red) => rsvg::CairoRenderer::new(crown_red), Some(Player::White) => rsvg::CairoRenderer::new(crown_white), None => panic!("This should never happen"), }; let m4 = { let mut m1 = Matrix::identity(); m1.translate(-center.x(), -(center.y() - thickness / 1.0)); let mut m2 = Matrix::identity(); m2.scale(0.5, 0.5); let mut m3 = Matrix::identity(); m3.translate(center.x(), center.y() - thickness / 1.0); Matrix::multiply(&Matrix::multiply(&m1, &m2), &m3) }; cr.set_matrix(Matrix::multiply( &Matrix::multiply(&matrix, &m4), &cr.matrix(), )); renderer .render_document( cr, &cairo::Rectangle::new( square.tl().x(), square.tl().y(), square.width(), square.height(), ), ) .unwrap(); } Ok(()) } } fn draw_score_bar( cr: &CairoContext, board: &Rectangle, draughts_game: &DraughtsGame, xform: &Matrix, ) { let score_bar = Rectangle::new( board.tl().x() - board.width() / 10.0, board.tl().y(), board.width() / 16.0, board.height(), ); let score_percentage = draughts_game.relative_score(Player::White) as f64; let tl = score_bar.tl(); let br = score_bar.br(); { let (tlx, tly) = xform.transform_point(tl.x(), tl.y()); let (brx, bry) = xform.transform_point(br.x(), br.y()); println!( "tl: ({}, {}), br: ({}, {}), score: {}", tlx, tly, brx, bry, score_percentage ); } cr.save().unwrap(); //cr.set_matrix(*xform); cr.set_source_rgb(1.0, 1.0, 1.0); cr.rectangle( score_bar.tl().x(), score_bar.tl().y(), score_bar.width(), score_bar.height() * score_percentage, ); cr.fill().unwrap(); cr.set_source_rgb(1.0, 0.0, 0.0); cr.rectangle( tl.x(), tl.y() + score_bar.height() * score_percentage, score_bar.width(), score_bar.height(), ); cr.fill().unwrap(); cr.restore().unwrap(); } fn on_activate(application: >k::Application) { // Initialize GTK before using any GTK functions. if gtk::init().is_err() { panic!("Failed to initialize GTK."); } // Create a new window. let window = gtk::ApplicationWindow::builder() .application(application) .title("Rdraught") .default_width(800) .default_height(800) .build(); // Create a DrawingArea widget where we will draw the chessboard. let drawing_area = Rc::new(RefCell::new(gtk::DrawingArea::new())); // Add the drawing area to the window. window.set_child(Some(drawing_area.borrow().as_ref() as &DrawingArea)); let draughts_game = Rc::new(RefCell::new(DraughtsGame::default())); let selected_piece: Rc>> = Rc::new(Cell::new(None)); let available_moves: Rc>> = Rc::new(RefCell::new(Vec::new())); // Get the allocation information for the widget. let board_width = SQUARE_SIZE * DraughtsBoard::rows() as f64; let board_height = SQUARE_SIZE * DraughtsBoard::columns() as f64; let board = Rectangle::from_points(Point::new(0.0, 0.0), Point::new(board_width, board_height)); let board_clone = board; let crown_red_handle = { let stream = gio::MemoryInputStream::from_bytes(&glib::Bytes::from_static(CROWN_RED)); rsvg::Loader::new() .read_stream( &stream, None::<&gio::File>, // no base file as this document has no references None::<&gio::Cancellable>, // no cancellable ) .unwrap() }; let crown_white_handle = { let stream = gio::MemoryInputStream::from_bytes(&glib::Bytes::from_static(CROWN_WHITE)); rsvg::Loader::new() .read_stream( &stream, None::<&gio::File>, // no base file as this document has no references None::<&gio::Cancellable>, // no cancellable ) .unwrap() }; let current_player = Rc::::new(Player::Red); let xform = Rc::>::new(RefCell::new(Matrix::identity())); // Set the "draw" function of the drawing area. This callback is called // whenever GTK needs to redraw this widget (for example, on first display or when resized). { let draughts_game = draughts_game.clone(); let xform = xform.clone(); let selected_piece = selected_piece.clone(); let available_moves = available_moves.clone(); let current_player = current_player.clone(); drawing_area .borrow_mut() .set_draw_func(move |_widget, cr, width, height| { let screen = Rectangle::from_points( Point::new(0.0, 0.0), Point::new(width as f64, height as f64), ); let f = f64::min( screen.width() / board.width(), screen.height() / board.height(), ); let screen_center = screen.center(); let board_center = board.center(); let mut xform = xform.borrow_mut(); *xform = Matrix::multiply( &Matrix::multiply( &Matrix::new(1.0, 0.0, 0.0, 1.0, -board_center.x(), -board_center.y()), &Matrix::new(f, 0.0, 0.0, f, 0.0, 0.0), ), &Matrix::new(1.0, 0.0, 0.0, 1.0, screen_center.x(), screen_center.y()), ); cr.set_matrix(*xform); // Loop over rows and columns to draw each chessboard cell. for row in 0..DraughtsBoard::rows() { for col in 0..DraughtsBoard::columns() { let position = match *current_player { Player::White => Position::new((8 - row - 1) as u8, col as u8), Player::Red => Position::new(row as u8, col as u8), }; let square = Rectangle::new( col as f64 * SQUARE_SIZE, row as f64 * SQUARE_SIZE, SQUARE_SIZE, SQUARE_SIZE, ); cr.save().unwrap(); // Alternate colors based on the sum of row and column indices. if (row + col) % 2 == 0 { cr.set_source_rgb(0.8, 0.8, 0.6); } else { cr.set_source_rgb(0.4, 0.4, 0.2); } // Draw and fill the square. cr.rectangle( square.tl().x(), square.tl().y(), square.width(), square.height(), ); cr.fill().unwrap(); draw_piece( cr, &square, draughts_game.borrow().piece_at(position), &crown_red_handle, &crown_white_handle, ) .unwrap(); cr.restore().unwrap(); } } if let Some(selected_position) = selected_piece.get() { let screen_position = match *current_player { Player::White => { Position::new(8 - 1 - selected_position.row(), selected_position.col()) } Player::Red => selected_position, }; let square = Rectangle::new( screen_position.col() as f64 * SQUARE_SIZE, screen_position.row() as f64 * SQUARE_SIZE, SQUARE_SIZE, SQUARE_SIZE, ); cr.save().unwrap(); cr.new_path(); cr.move_to(square.tl().x(), square.tl().y()); cr.line_to(square.tl().x(), square.br().y()); cr.line_to(square.br().x(), square.br().y()); cr.line_to(square.br().x(), square.tl().y()); cr.line_to(square.tl().x(), square.tl().y()); cr.clip(); cr.new_path(); cr.set_source_rgb(0.0, 0.0, 1.0); cr.set_line_width((square.width() + square.height()) * 0.05); cr.move_to(square.tl().x(), square.tl().y()); cr.line_to(square.tl().x(), square.br().y()); cr.line_to(square.br().x(), square.br().y()); cr.line_to(square.br().x(), square.tl().y()); cr.line_to(square.tl().x(), square.tl().y()); cr.stroke().unwrap(); cr.restore().unwrap(); } let am = available_moves.borrow(); if !am.is_empty() { for mv in am.iter() { let end_pos = mv.get_end_position(); let screen_position = match *current_player { Player::White => Position::new(8 - 1 - end_pos.row(), end_pos.col()), Player::Red => end_pos, }; let square = Rectangle::new( screen_position.col() as f64 * SQUARE_SIZE, screen_position.row() as f64 * SQUARE_SIZE, SQUARE_SIZE, SQUARE_SIZE, ); cr.save().unwrap(); cr.new_path(); cr.move_to(square.tl().x(), square.tl().y()); cr.line_to(square.tl().x(), square.br().y()); cr.line_to(square.br().x(), square.br().y()); cr.line_to(square.br().x(), square.tl().y()); cr.line_to(square.tl().x(), square.tl().y()); cr.clip(); cr.new_path(); cr.set_source_rgb(0.0, 1.0, 0.0); cr.set_line_width((square.width() + square.height()) * 0.05); cr.move_to(square.tl().x(), square.tl().y()); cr.line_to(square.tl().x(), square.br().y()); cr.line_to(square.br().x(), square.br().y()); cr.line_to(square.br().x(), square.tl().y()); cr.line_to(square.tl().x(), square.tl().y()); cr.stroke().unwrap(); cr.restore().unwrap(); } } draw_score_bar(&cr, &board, &draughts_game.borrow(), &xform); }); } let gesture = gtk::GestureClick::new(); // Set the gestures button to the right mouse button (=3) gesture.set_button(GDK_BUTTON_PRIMARY as u32); // Assign your handler to an event of the gesture (e.g. the `pressed` event) { let drawing_area = drawing_area.clone(); let available_moves = available_moves.clone(); gesture.connect_pressed(move |gesture, _, x, y| { gesture.set_state(gtk::EventSequenceState::Claimed); let xform = xform.borrow(); let inverse = { let mut m = *xform; m.invert(); m }; let p = transform_point(&Point::new(x, y), &inverse); if board_clone.contains(&p) { let p = &p - &board_clone.tl(); // println!("Point: {:?}", p); let position = match *current_player { Player::White => Position::new( (8.0 - (p.y() / SQUARE_SIZE)) as u8, (p.x() / SQUARE_SIZE) as u8, ), Player::Red => { Position::new((p.y() / SQUARE_SIZE) as u8, (p.x() / SQUARE_SIZE) as u8) } }; println!("Selected position: {:?}", position); let mut draughts_game = draughts_game.borrow_mut(); let piece = draughts_game.piece_at(position); // println!("Selected piece: {:?}", piece); let mut am = available_moves.borrow_mut(); let mut move_applied = false; if !am.is_empty() { for mv in am.iter() { if mv.get_end_position() == position { draughts_game.apply_move(mv).unwrap(); // if let Some(mv) = draughts_game.get_best_move(10) { // println!("Next best move: {:?}", mv); // } move_applied = true; break; } } if move_applied { selected_piece.set(None); am.clear(); } } if !move_applied { match piece.player() { Some(Player::Red) => selected_piece.set(Some(position)), Some(Player::White) => selected_piece.set(Some(position)), None => selected_piece.set(None), } am.clear(); if piece.player().is_none() { selected_piece.set(None) } else { selected_piece.set(Some(position)); for mv in draughts_game.moves_for_piece(position) { am.push(mv); } } } } else { selected_piece.set(None); } drawing_area.borrow_mut().queue_draw(); }); } // Assign the gesture to the treeview drawing_area.borrow_mut().add_controller(gesture); window.present(); } fn main() { // Create a new application with the builder pattern let app = gtk::Application::builder() .application_id("net.woggioni.rdraught") .build(); app.connect_activate(on_activate); // Run the application app.run(); }