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::GobanElement, Color}; use std::{cell::RefCell, io::Cursor, rc::Rc}; const WIDTH: i32 = 800; const HEIGHT: i32 = 800; #[derive(Clone, Default, PartialEq)] struct Addr { row: u8, column: u8, } pub struct GobanPrivate { drawing_area: gtk::DrawingArea, current_player: Rc>, goban: Rc>, cursor_location: Rc>, } #[glib::object_subclass] impl ObjectSubclass for GobanPrivate { const NAME: &'static str = "Goban"; type Type = Goban; type ParentType = gtk::Grid; fn new() -> GobanPrivate { GobanPrivate { drawing_area: Default::default(), current_player: Rc::new(RefCell::new(Color::Black)), goban: Default::default(), cursor_location: Default::default(), } } } impl ObjectImpl for GobanPrivate { fn constructed(&self) { self.drawing_area.set_width_request(WIDTH); self.drawing_area.set_height_request(HEIGHT); let goban = self.goban.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| { let goban = goban.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) / ((goban.size.width - 1) as f64); let vspace_between = ((height - 40) as f64) / ((goban.size.height - 1) as f64); let pen = Pen { x_offset: 20., y_offset: 20., hspace_between, vspace_between, }; (0..goban.size.width).for_each(|col| { context.move_to(20.0 + (col as f64) * hspace_between, 20.0); context.line_to(20.0 + (col as f64) * hspace_between, (height as f64) - 20.0); let _ = context.stroke(); }); (0..goban.size.height).for_each(|row| { context.move_to(20.0, 20.0 + (row as f64) * vspace_between); context.line_to((width - 20) as f64, 20.0 + (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 goban.stone(row, col) { None => {} Some(element) => { pen.stone(&context, row, col, element.color); } }; }) }); let cursor = cursor_location.borrow(); pen.ghost_stone(context, cursor.row, cursor.column, *current_player.borrow()); }); let motion_controller = gtk::EventControllerMotion::new(); { let goban = self.goban.clone(); let cursor = self.cursor_location.clone(); let drawing_area = self.drawing_area.clone(); motion_controller.connect_motion(move |_, x, y| { let goban = goban.borrow(); let mut cursor = cursor.borrow_mut(); let hspace_between = ((WIDTH - 40) as f64) / ((goban.size.width - 1) as f64); let vspace_between = ((HEIGHT - 40) as f64) / ((goban.size.height - 1) as f64); let addr = Addr { column: ((x.round() - 20.) / hspace_between).round() as u8, row: ((y.round() - 20.) / vspace_between).round() as u8, }; if *cursor != addr { *cursor = addr; drawing_area.queue_draw(); } }); } self.drawing_area.add_controller(motion_controller); } } impl WidgetImpl for GobanPrivate {} impl GridImpl for GobanPrivate {} glib::wrapper! { pub struct Goban(ObjectSubclass) @extends gtk::Grid, gtk::Widget; } impl Goban { pub fn new() -> Self { let s: Self = Object::builder().build(); s.attach(&s.imp().drawing_area, 1, 1, 1, 1); s } pub fn set_board(&self, goban: GobanElement) { *self.imp().goban.borrow_mut() = goban; 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, 10., 0., 2. * std::f64::consts::PI, ); let _ = context.fill(); } fn stone(&self, context: &cairo::Context, row: u8, col: u8, color: Color) { 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), }; context.arc( 20.0 + (col as f64) * self.hspace_between, 20.0 + (row as f64) * self.vspace_between, 25.0, 0.0, 2.0 * std::f64::consts::PI, ); let _ = context.fill(); } 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), }; context.arc( 20.0 + (col as f64) * self.hspace_between, 20.0 + (row as f64) * self.vspace_between, 25.0, 0.0, 2.0 * std::f64::consts::PI, ); let _ = context.fill(); } }