use crate::{perftrace, CoreApi}; use gio::resources_lookup_data; use glib::Object; use gtk::{ gdk_pixbuf::{InterpType, Pixbuf}, prelude::*, subclass::prelude::*, }; use image::io::Reader as ImageReader; use kifu_core::{ ui::{BoardElement, IntersectionElement}, Color, }; use std::{cell::RefCell, io::Cursor, rc::Rc}; const WIDTH: i32 = 800; const HEIGHT: i32 = 800; const MARGIN: i32 = 20; #[derive(Clone, Default, PartialEq)] struct Addr { row: u8, column: u8, } pub struct BoardPrivate { drawing_area: gtk::DrawingArea, current_player: Rc>, board: Rc>, cursor_location: Rc>>, api: Rc>>, } #[glib::object_subclass] impl ObjectSubclass for BoardPrivate { const NAME: &'static str = "Board"; type Type = Board; type ParentType = gtk::Grid; fn new() -> BoardPrivate { BoardPrivate { drawing_area: Default::default(), current_player: Rc::new(RefCell::new(Color::Black)), board: Default::default(), cursor_location: Default::default(), api: Default::default(), } } } impl ObjectImpl for BoardPrivate { fn constructed(&self) { self.drawing_area.set_width_request(WIDTH); self.drawing_area.set_height_request(HEIGHT); let board = self.board.clone(); let cursor_location = self.cursor_location.clone(); let current_player = self.current_player.clone(); let wood_texture = resources_lookup_data( "/com/luminescent-dreams/kifu-gtk/wood_texture.jpg", gio::ResourceLookupFlags::NONE, ) .unwrap(); let background = ImageReader::new(Cursor::new(wood_texture)) .with_guessed_format() .unwrap() .decode(); let background = background.map(|background| { Pixbuf::from_bytes( &glib::Bytes::from(background.as_bytes()), gtk::gdk_pixbuf::Colorspace::Rgb, false, 8, background.width() as i32, background.height() as i32, background.to_rgb8().sample_layout().height_stride as i32, ) .scale_simple(WIDTH, HEIGHT, InterpType::Nearest) }); self.drawing_area .set_draw_func(move |_, context, width, height| { perftrace("render drawing area", || { let render_start = std::time::Instant::now(); let board = board.borrow(); match background { Ok(Some(ref background)) => { context.set_source_pixbuf(&background, 0., 0.); context.paint().expect("paint should succeed"); } Ok(None) | Err(_) => context.set_source_rgb(0.7, 0.7, 0.7), }; let _ = context.paint(); context.set_source_rgb(0.1, 0.1, 0.1); context.set_line_width(2.); let hspace_between = ((width - 40) as f64) / ((board.size.width - 1) as f64); let vspace_between = ((height - 40) as f64) / ((board.size.height - 1) as f64); let pen = Pen { x_offset: MARGIN as f64, y_offset: MARGIN as f64, hspace_between, vspace_between, }; (0..board.size.width).for_each(|col| { context.move_to( (MARGIN as f64) + (col as f64) * hspace_between, MARGIN as f64, ); context.line_to( (MARGIN as f64) + (col as f64) * hspace_between, (height as f64) - (MARGIN as f64), ); let _ = context.stroke(); }); (0..board.size.height).for_each(|row| { context.move_to( MARGIN as f64, (MARGIN as f64) + (row as f64) * vspace_between, ); context.line_to( (width - MARGIN) as f64, (MARGIN as f64) + (row as f64) * vspace_between, ); let _ = context.stroke(); }); context.set_source_rgb(0.1, 0.1, 0.0); vec![3, 9, 15].into_iter().for_each(|col| { vec![3, 9, 15].into_iter().for_each(|row| { pen.star_point(context, col, row); }); }); (0..19).for_each(|col| { (0..19).for_each(|row| { match board.stone(row, col) { IntersectionElement::Filled(stone) => { pen.stone(&context, row, col, stone.color, stone.liberties); } _ => {} }; }) }); let cursor = cursor_location.borrow(); match *cursor { None => {} Some(ref cursor) => match board.stone(cursor.row, cursor.column) { IntersectionElement::Empty(_) => pen.ghost_stone( context, cursor.row, cursor.column, *current_player.borrow(), ), _ => {} }, } let render_end = std::time::Instant::now(); println!("board rendering time: {:?}", render_end - render_start); }) }); let motion_controller = gtk::EventControllerMotion::new(); { let board = self.board.clone(); let cursor = self.cursor_location.clone(); let drawing_area = self.drawing_area.clone(); motion_controller.connect_motion(move |_, x, y| { let board = board.borrow(); let mut cursor = cursor.borrow_mut(); let hspace_between = ((WIDTH - 40) as f64) / ((board.size.width - 1) as f64); let vspace_between = ((HEIGHT - 40) as f64) / ((board.size.height - 1) as f64); let addr = if x.round() < MARGIN as f64 || x.round() > (WIDTH - MARGIN) as f64 || y.round() < MARGIN as f64 || y.round() > (HEIGHT - MARGIN) as f64 { None } else { Some(Addr { column: ((x.round() - MARGIN as f64) / hspace_between).round() as u8, row: ((y.round() - MARGIN as f64) / vspace_between).round() as u8, }) }; if *cursor != addr.clone() { *cursor = addr; drawing_area.queue_draw(); } }); } let gesture = gtk::GestureClick::new(); { let board = self.board.clone(); let cursor = self.cursor_location.clone(); let api = self.api.clone(); gesture.connect_released(move |_, _, _, _| { let board = board.borrow(); let cursor = cursor.borrow(); match *cursor { None => {} Some(ref cursor) => match board.stone(cursor.row, cursor.column) { IntersectionElement::Empty(request) => { println!("need to send request: {:?}", request); api.borrow() .as_ref() .expect("API must exist") .dispatch(request); } _ => {} }, } }); } self.drawing_area.add_controller(motion_controller); self.drawing_area.add_controller(gesture); } } impl WidgetImpl for BoardPrivate {} impl GridImpl for BoardPrivate {} glib::wrapper! { pub struct Board(ObjectSubclass) @extends gtk::Grid, gtk::Widget; } impl Board { pub fn new(api: CoreApi) -> Self { let s: Self = Object::builder().build(); *s.imp().api.borrow_mut() = Some(api); s.attach(&s.imp().drawing_area, 1, 1, 1, 1); s } pub fn set_board(&self, board: BoardElement) { *self.imp().board.borrow_mut() = board; self.imp().drawing_area.queue_draw(); } pub fn set_current_player(&self, color: Color) { *self.imp().current_player.borrow_mut() = color; } } struct Pen { x_offset: f64, y_offset: f64, hspace_between: f64, vspace_between: f64, } impl Pen { fn star_point(&self, context: &cairo::Context, row: u8, col: u8) { context.arc( self.x_offset + (col as f64) * self.hspace_between, self.y_offset + (row as f64) * self.vspace_between, 5., 0., 2. * std::f64::consts::PI, ); let _ = context.fill(); } fn stone( &self, context: &cairo::Context, row: u8, col: u8, color: Color, liberties: Option, ) { match color { Color::White => context.set_source_rgb(0.9, 0.9, 0.9), Color::Black => context.set_source_rgb(0.0, 0.0, 0.0), }; self.draw_stone(context, row, col); if let Some(liberties) = liberties { let stone_location = self.stone_location(row, col); context.set_source_rgb(1., 0., 1.); context.set_font_size(32.); context.move_to(stone_location.0 - 10., stone_location.1 + 10.); let _ = context.show_text(&format!("{}", liberties)); } } fn ghost_stone(&self, context: &cairo::Context, row: u8, col: u8, color: Color) { match color { Color::White => context.set_source_rgba(0.9, 0.9, 0.9, 0.5), Color::Black => context.set_source_rgba(0.0, 0.0, 0.0, 0.5), }; self.draw_stone(context, row, col); } fn draw_stone(&self, context: &cairo::Context, row: u8, col: u8) { let radius = self.hspace_between / 2. - 2.; let (x_loc, y_loc) = self.stone_location(row, col); context.arc(x_loc, y_loc, radius, 0.0, 2.0 * std::f64::consts::PI); let _ = context.fill(); } fn stone_location(&self, row: u8, col: u8) -> (f64, f64) { ( self.x_offset + (col as f64) * self.hspace_between, self.y_offset + (row as f64) * self.vspace_between, ) } }