From 0b1ba27037bd116087cb1bdcfe81d850b20eab97 Mon Sep 17 00:00:00 2001 From: tslil clingman <> Date: Tue, 10 Nov 2020 22:30:08 -0500 Subject: Refactoring --- src/game.rs | 13 ++ src/gui.rs | 419 ------------------------------------------------- src/gui/boardwidget.rs | 264 +++++++++++++++++++++++++++++++ src/gui/mod.rs | 122 ++++++++++++++ 4 files changed, 399 insertions(+), 419 deletions(-) delete mode 100644 src/gui.rs create mode 100644 src/gui/boardwidget.rs create mode 100644 src/gui/mod.rs diff --git a/src/game.rs b/src/game.rs index 4a91755..e86cf58 100644 --- a/src/game.rs +++ b/src/game.rs @@ -154,6 +154,19 @@ impl fmt::Display for WinType { pub type Stack = Vec; +/* +const-generics would solve a lot of my consistency problems here, + + GameState { + ... + board: [Stack; size], + } + +but at the time of writing this is not yet part of the language. +Perhaps one day we'll have a systems programming language with a nice +dependent type system ;) +*/ + struct GameState { size: u8, black_flats: u8, diff --git a/src/gui.rs b/src/gui.rs deleted file mode 100644 index 3e60913..0000000 --- a/src/gui.rs +++ /dev/null @@ -1,419 +0,0 @@ -/* - This file is part of Takwrap. - - Takwrap is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Foobar is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Foobar. If not, see . -*/ - -use fltk::{app::*, button::*, draw::*, menu::*, text::*, window::*}; -use std::rc::*; - -use crate::game::*; - -struct BoardRendererDetails { - xorg: i32, - yorg: i32, - square_size: i32, -} - -impl BoardRendererDetails { - fn default() -> BoardRendererDetails { - BoardRendererDetails { - xorg: 0, - yorg: 0, - square_size: 0, - } - } - - fn set(&mut self, button: &Button, frame_pad: i32, game_size: i32) { - let x0 = button.x(); - let y0 = button.y(); - self.xorg = x0 + frame_pad; - self.yorg = y0 + frame_pad; - let board_size = i32::min(button.width(), button.height()) - frame_pad * 2; - self.square_size = board_size / game_size; - } -} - -struct BoardRenderer { - even_square_colour: Color, - odd_square_colour: Color, - black_colour: Color, - white_colour: Color, - stone_size: f32, - game_size: i32, - frame_pad: i32, -} - -impl BoardRenderer { - fn default() -> BoardRenderer { - BoardRenderer { - // Sandy brown - odd_square_colour: Color::from_rgb(244, 164, 96), - // Moccasin - even_square_colour: Color::from_rgb(255, 228, 181), - // Colours for pieces - black_colour: Color::from_rgb(178, 34, 34), - white_colour: Color::from_rgb(250, 235, 215), - // Gap between stones and edged of square - stone_size: 0.8, - // I'm not sure where this should go - game_size: 5, - frame_pad: 5, - } - } - - fn with_size(mut self, size: i32) -> BoardRenderer { - self.game_size = size; - return self; - } - - fn render_stone( - &self, - details: &BoardRendererDetails, - player: Player, - stone: Stone, - row: i32, - col: i32, - shift: i32, - ) { - set_draw_color(match player { - Player::Black => self.black_colour, - Player::White => self.white_colour, - }); - - // Gap scales with square size - let stone_gap = (details.square_size as f32 * (1.00 - self.stone_size)).round() as i32; - let d = details.square_size - stone_gap * 2; - let shift = (shift * d) / 8; - let x1 = details.xorg + col * details.square_size + stone_gap + shift; - let y1 = details.yorg + row * details.square_size + stone_gap - shift; - - // Outline with thickness varying by stone size - let setup_border_pen = || { - set_line_style(LineStyle::Solid, i32::max(1, d / 24)); - set_draw_color(Color::Black); - }; - - match stone { - Stone::Capstone => { - draw_pie(x1, y1, d, d, 0.0, 360.0); - setup_border_pen(); - let r = (d as f64) / 2.0; - draw_circle(r + x1 as f64, r + y1 as f64, r); - } - Stone::Flat => { - draw_rectf(x1, y1, d, d); - setup_border_pen(); - draw_rect_with_color(x1, y1, d, d, Color::Black); - } - Stone::Standing => { - // These look bad shifted so we shift it back if was - let df = d as f64; - // and we have to fudge it a little? Some rounding things presumably - let ds = if shift > 0 { df / 8.0 - 0.5 } else { 0.0 }; - let x1 = x1 as f64 - ds; - let y1 = y1 as f64 + ds; - - let verts = [ - (x1 + df * 0.3, y1 + df * 0.075), - (x1 + df * 0.925, y1 + df * 0.7), - (x1 + df * 0.7, y1 + df * 0.925), - (x1 + df * 0.075, y1 + df * 0.3), - ]; - - begin_polygon(); - for (x, y) in verts.iter() { - vertex(*x, *y) - } - end_polygon(); - - setup_border_pen(); - begin_loop(); - for (x, y) in verts.iter() { - vertex(*x, *y); - } - end_loop(); - } - } - set_line_style(LineStyle::Solid, 1); - } - - fn render_icons( - &self, - details: &BoardRendererDetails, - player: Player, - row: i32, - col: i32, - shift: i32, - ) { - let stone_gap = details.square_size as f32 * (1.00 - self.stone_size); - let icon_gap = (stone_gap * 0.15).round() as i32; - let icon_width = (stone_gap * 0.7).round() as i32; - let icon_height = icon_width / 2; - - let x1 = details.xorg + col * details.square_size + icon_gap; - let y1 = details.yorg + (row + 1) * details.square_size - icon_gap; - - set_line_style(LineStyle::Solid, i32::max(1, icon_width / 8)); - set_draw_color(Color::Black); - - set_draw_color(match player { - Player::Black => self.black_colour, - Player::White => self.white_colour, - }); - let y2 = y1 - icon_height * (1 + shift as i32); - draw_rectf(x1, y2, icon_width, icon_height); - draw_rect_with_color(x1, y2, icon_width, icon_height, Color::Black); - - set_line_style(LineStyle::Solid, 1); - } - - fn render_stack(&self, details: &BoardRendererDetails, row: i32, col: i32, stack: &Stack) { - // Is there a better way than usize::min((...), i32::MAX as usize) as i32 ? - let cutoff = stack.len() as i32 - self.game_size; - - for (shift, piece) in stack.iter().enumerate() { - let shift = shift as i32; // TODO: please don't overflow - if shift < cutoff { - self.render_icons(details, piece.player, row, col, shift); - } else { - self.render_stone( - details, - piece.player, - piece.stone, - row, - col, - shift - i32::max(0, cutoff), - ); - } - } - } - - fn render_board(&self, details: &BoardRendererDetails) { - for x in 0..self.game_size { - for y in 0..self.game_size { - if (x + y) % 2 == 0 { - set_draw_color(self.even_square_colour); - } else { - set_draw_color(self.odd_square_colour); - } - draw_rectf( - details.xorg + x * details.square_size, - details.yorg + y * details.square_size, - details.square_size, - details.square_size, - ); - } - } - } -} - -struct GUIBoard { - button: Button, - renderer: Rc, - size: i32, -} - -impl GUIBoard { - fn new(button: Button, size: i32) -> GUIBoard { - let renderer = Rc::new(BoardRenderer::default().with_size(size)); - let mut g = GUIBoard { - button, - size, - renderer, - }; - g.button.draw2(g.render()); - return g; - } - - fn to_square(&self, x: i32, y: i32) -> (i32, i32) { - // Irritatingly we have to basically inline the exact same - // BoardRendererDetails code here as elsewhere, because - // *.draw2 wants to own the closure and i can't find a way to - // juggle this properly - - let mut details = BoardRendererDetails::default(); - details.set( - &self.button, - self.renderer.frame_pad, - self.renderer.game_size, - ); - - let x = x - details.xorg; - let y = y - details.yorg; - - (x / details.square_size, y / details.square_size) - } - - fn render(&self) -> impl FnMut(&mut Button) { - let renderer = self.renderer.clone(); - let game_size = self.size; - - // test code - let stack = vec![ - Piece { - player: Player::Black, - stone: Stone::Flat, - }, - Piece { - player: Player::White, - stone: Stone::Flat, - }, - Piece { - player: Player::White, - stone: Stone::Flat, - }, - Piece { - player: Player::Black, - stone: Stone::Flat, - }, - Piece { - player: Player::White, - stone: Stone::Flat, - }, - Piece { - player: Player::Black, - stone: Stone::Flat, - }, - Piece { - player: Player::Black, - stone: Stone::Flat, - }, - Piece { - player: Player::White, - stone: Stone::Flat, - }, - Piece { - player: Player::Black, - stone: Stone::Flat, - }, - Piece { - player: Player::White, - stone: Stone::Standing, - }, - ]; - - let mut details = BoardRendererDetails::default(); - move |button: &mut Button| { - details.set(button, renderer.frame_pad, game_size); - renderer.render_board(&details); - - // Test code for now - renderer.render_stack(&details, 1, 4, &stack); - renderer.render_stone(&details, Player::Black, Stone::Capstone, 1, 2, 0); - renderer.render_stone(&details, Player::Black, Stone::Standing, 2, 2, 0); - } - } -} - -#[derive(Clone, Copy)] -enum GUIMessage { - None, - Test, - BoardClick, -} - -pub struct GUI { - app: App, - window: DoubleWindow, - board: GUIBoard, - log: TextDisplay, - sender: Sender, - receiver: Receiver, -} - -impl GUI { - pub fn new() -> GUI { - let app = App::default(); - - // TODO: Why is this not the same as - /* - let mut window = Window::default() - .with_size(800, 600) - .with_label("Takwrap") - .center_screen(); - */ - // Windows made that way are not resizable? - - let mut window = DoubleWindow::new(0, 0, 800, 600, "Takwrap").center_screen(); - - let (sender, receiver) = channel::(); - - const MENU_HEIGHT: i32 = 24; - let mut menu = SysMenuBar::default().with_size(800, MENU_HEIGHT); - // menu.set_text_font(Font::Helvetica); - menu.set_color(Color::Light2); - menu.add_emit( - "&File/New...\t", - Shortcut::None, - MenuFlag::Normal, - sender, - GUIMessage::None, - ); - menu.add_emit( - "&File/Open...\t", - Shortcut::None, - MenuFlag::Normal, - sender, - GUIMessage::Test, - ); - - const LOG_WIDTH: i32 = 800 - 600 + MENU_HEIGHT; - let mut log = TextDisplay::default() - .with_size(LOG_WIDTH, 600 - menu.height()) - .with_pos(800 - LOG_WIDTH, menu.height()); - log.set_buffer(Some(TextBuffer::default())); - log.insert("test\n"); - - let mut board_button = Button::default() - .with_size(800 - LOG_WIDTH, 600 - menu.height()) - .below_of(&menu, 0); - board_button.set_frame(FrameType::EmbossedBox); - board_button.emit(sender, GUIMessage::BoardClick); - - window.resizable(&mut board_button); - window.end(); - window.show(); - - let board = GUIBoard::new(board_button, 5); - - // button.emit(sender, GUIMessage::Foo); - - GUI { - app, - board, - window, - log, - sender, - receiver, - } - } - - pub fn run(&mut self) -> Result<(), fltk::prelude::FltkError> { - while self.app.wait() { - if let Some(msg) = self.receiver.recv() { - match msg { - GUIMessage::BoardClick => { - let x = event_x(); - let y = event_y(); - let (x, y) = self.board.to_square(x, y); - self.log.insert(&format!("Board click ({},{})\n", x, y)); - } - _ => (), - } - } - } - Ok(()) - } -} diff --git a/src/gui/boardwidget.rs b/src/gui/boardwidget.rs new file mode 100644 index 0000000..3eca0ed --- /dev/null +++ b/src/gui/boardwidget.rs @@ -0,0 +1,264 @@ +/* + This file is part of Takwrap. + + Takwrap is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Foobar is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Foobar. If not, see . +*/ + +use fltk::{app::*, button::*, draw::*}; +use std::rc::*; + +use crate::game::*; + +// All of theses properties are not expected to change under window +// resizing, so we don't need more than an Rc<...> to one of these +// when rendering in button.draw2(...) later. We do, however, need to +// use compute_square_size because the widget may be resized. +struct BoardRenderer { + even_square_colour: Color, + odd_square_colour: Color, + black_colour: Color, + white_colour: Color, + stone_size: f32, + game_size: i32, + frame_pad: i32, + xorg: i32, + yorg: i32, +} + +impl BoardRenderer { + fn default() -> BoardRenderer { + BoardRenderer { + // Sandy brown + odd_square_colour: Color::from_rgb(244, 164, 96), + // Moccasin + even_square_colour: Color::from_rgb(255, 228, 181), + // Colours for pieces + black_colour: Color::from_rgb(178, 34, 34), + white_colour: Color::from_rgb(250, 235, 215), + // Gap between stones and edged of square + stone_size: 0.8, + // Defaults + game_size: 5, + frame_pad: 5, + xorg: 0, + yorg: 0, + } + } + + fn with_size(mut self, size: i32) -> BoardRenderer { + self.game_size = size; + return self; + } + + fn with_org(mut self, button: &Button) -> BoardRenderer { + let x0 = button.x(); + let y0 = button.y(); + self.xorg = x0 + self.frame_pad; + self.yorg = y0 + self.frame_pad; + return self; + } + + fn compute_square_size(&self, button: &Button) -> i32 { + let board_size = i32::min(button.width(), button.height()) - self.frame_pad * 2; + return board_size / self.game_size; + } + + fn render_stone( + &self, + square_size: i32, + player: Player, + stone: Stone, + row: i32, + col: i32, + shift: i32, + ) { + set_draw_color(match player { + Player::Black => self.black_colour, + Player::White => self.white_colour, + }); + + // Gap scales with square size + let stone_gap = (square_size as f32 * (1.00 - self.stone_size)).round() as i32; + let d = square_size - stone_gap * 2; + let shift = (shift * d) / 8; + let x1 = self.xorg + col * square_size + stone_gap + shift; + let y1 = self.yorg + row * square_size + stone_gap - shift; + + // Outline with thickness varying by stone size + let setup_border_pen = || { + set_line_style(LineStyle::Solid, i32::max(1, d / 24)); + set_draw_color(Color::Black); + }; + + match stone { + Stone::Capstone => { + draw_pie(x1, y1, d, d, 0.0, 360.0); + setup_border_pen(); + let r = (d as f64) / 2.0; + draw_circle(r + x1 as f64, r + y1 as f64, r); + } + Stone::Flat => { + draw_rectf(x1, y1, d, d); + setup_border_pen(); + draw_rect_with_color(x1, y1, d, d, Color::Black); + } + Stone::Standing => { + // These look bad shifted so we shift it back if was + let df = d as f64; + // and we have to fudge it a little? Some rounding things presumably + let ds = if shift > 0 { df / 8.0 - 0.5 } else { 0.0 }; + let x1 = x1 as f64 - ds; + let y1 = y1 as f64 + ds; + + let verts = [ + (x1 + df * 0.3, y1 + df * 0.075), + (x1 + df * 0.925, y1 + df * 0.7), + (x1 + df * 0.7, y1 + df * 0.925), + (x1 + df * 0.075, y1 + df * 0.3), + ]; + + begin_polygon(); + for (x, y) in verts.iter() { + vertex(*x, *y) + } + end_polygon(); + + setup_border_pen(); + begin_loop(); + for (x, y) in verts.iter() { + vertex(*x, *y); + } + end_loop(); + } + } + set_line_style(LineStyle::Solid, 1); + } + + fn render_icons(&self, square_size: i32, player: Player, row: i32, col: i32, shift: i32) { + let stone_gap = square_size as f32 * (1.00 - self.stone_size); + let icon_gap = (stone_gap * 0.15).round() as i32; + let icon_width = (stone_gap * 0.7).round() as i32; + let icon_height = icon_width / 2; + + let x1 = self.xorg + col * square_size + icon_gap; + let y1 = self.yorg + (row + 1) * square_size - icon_gap; + + set_line_style(LineStyle::Solid, i32::max(1, icon_width / 8)); + set_draw_color(Color::Black); + + set_draw_color(match player { + Player::Black => self.black_colour, + Player::White => self.white_colour, + }); + let y2 = y1 - icon_height * (1 + shift as i32); + draw_rectf(x1, y2, icon_width, icon_height); + draw_rect_with_color(x1, y2, icon_width, icon_height, Color::Black); + + set_line_style(LineStyle::Solid, 1); + } + + fn render_stack(&self, square_size: i32, row: i32, col: i32, stack: &Stack) { + // Is there a better way than usize::min((...), i32::MAX as usize) as i32 ? + let cutoff = stack.len() as i32 - self.game_size; + + for (shift, piece) in stack.iter().enumerate() { + let shift = shift as i32; // TODO: please don't overflow + if shift < cutoff { + self.render_icons(square_size, piece.player, row, col, shift); + } else { + self.render_stone( + square_size, + piece.player, + piece.stone, + row, + col, + shift - i32::max(0, cutoff), + ); + } + } + } + + fn render_board(&self, square_size: i32, board: &Vec) { + let mut x = 0; + let mut y = 0; + for stack in board.iter() { + self.render_stack(square_size, x, y, stack); + x += 1; + if x + 1 >= self.game_size { + x = 0; + y += 1; + if y + 1 >= self.game_size { + panic!("Game size mismatch between renderer and internal state."); + } + } + } + } + + fn render_background(&self, square_size: i32) { + for x in 0..self.game_size { + for y in 0..self.game_size { + if (x + y) % 2 == 0 { + set_draw_color(self.even_square_colour); + } else { + set_draw_color(self.odd_square_colour); + } + draw_rectf( + self.xorg + x * square_size, + self.yorg + y * square_size, + square_size, + square_size, + ); + } + } + } +} + +pub struct BoardWidget { + button: Button, + renderer: Rc, +} + +impl BoardWidget { + pub fn new(button: Button, size: i32) -> BoardWidget { + let renderer = Rc::new(BoardRenderer::default().with_size(size).with_org(&button)); + let mut g = BoardWidget { button, renderer }; + g.button.draw2(g.generate_draw(&Vec::new())); + return g; + } + + pub fn to_square(&self, x: i32, y: i32) -> (i32, i32) { + let square_size = self.renderer.compute_square_size(&self.button); + + let x = x - self.renderer.xorg; + let y = y - self.renderer.yorg; + + (x / square_size, y / square_size) + } + + pub fn generate_draw(&self, board: &Vec) -> impl FnMut(&mut Button) { + let renderer = self.renderer.clone(); + let board = board.clone(); + + move |button: &mut Button| { + let square_size = renderer.compute_square_size(&button); + renderer.render_background(square_size); + renderer.render_board(square_size, &board); + + // // Test code for now + // renderer.render_stack(square_size, 1, 4, &stack); + // renderer.render_stone(square_size, Player::Black, Stone::Capstone, 1, 2, 0); + // renderer.render_stone(square_size, Player::Black, Stone::Standing, 2, 2, 0); + } + } +} diff --git a/src/gui/mod.rs b/src/gui/mod.rs new file mode 100644 index 0000000..341bded --- /dev/null +++ b/src/gui/mod.rs @@ -0,0 +1,122 @@ +/* + This file is part of Takwrap. + + Takwrap is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Foobar is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Foobar. If not, see . +*/ + +mod boardwidget; +use crate::gui::boardwidget::*; + +use fltk::{app::*, button::*, menu::*, text::*, window::*}; + +#[derive(Clone, Copy)] +enum GUIMessage { + None, + Test, + BoardClick, +} + +pub struct GUI { + app: App, + window: DoubleWindow, + board: BoardWidget, + log: TextDisplay, + sender: Sender, + receiver: Receiver, +} + +impl GUI { + pub fn new() -> GUI { + let app = App::default(); + + // TODO: Why is this not the same as + /* + let mut window = Window::default() + .with_size(800, 600) + .with_label("Takwrap") + .center_screen(); + */ + // Windows made that way are not resizable? + + let mut window = DoubleWindow::new(0, 0, 800, 600, "Takwrap").center_screen(); + + let (sender, receiver) = channel::(); + + const MENU_HEIGHT: i32 = 24; + let mut menu = SysMenuBar::default().with_size(800, MENU_HEIGHT); + // menu.set_text_font(Font::Helvetica); + menu.set_color(Color::Light2); + menu.add_emit( + "&File/New...\t", + Shortcut::None, + MenuFlag::Normal, + sender, + GUIMessage::None, + ); + menu.add_emit( + "&File/Open...\t", + Shortcut::None, + MenuFlag::Normal, + sender, + GUIMessage::Test, + ); + + const LOG_WIDTH: i32 = 800 - 600 + MENU_HEIGHT; + let mut log = TextDisplay::default() + .with_size(LOG_WIDTH, 600 - menu.height()) + .with_pos(800 - LOG_WIDTH, menu.height()); + log.set_buffer(Some(TextBuffer::default())); + log.insert("test\n"); + + let mut board_button = Button::default() + .with_size(800 - LOG_WIDTH, 600 - menu.height()) + .below_of(&menu, 0); + board_button.set_frame(FrameType::EmbossedBox); + board_button.emit(sender, GUIMessage::BoardClick); + + window.resizable(&mut board_button); + window.end(); + window.show(); + + let board = BoardWidget::new(board_button, 5); + + // button.emit(sender, GUIMessage::Foo); + + GUI { + app, + board, + window, + log, + sender, + receiver, + } + } + + pub fn run(&mut self) -> Result<(), fltk::prelude::FltkError> { + while self.app.wait() { + if let Some(msg) = self.receiver.recv() { + match msg { + GUIMessage::BoardClick => { + let x = event_x(); + let y = event_y(); + let (x, y) = self.board.to_square(x, y); + self.log.insert(&format!("Board click ({},{})\n", x, y)); + } + _ => (), + } + } + } + Ok(()) + } +} -- cgit v1.3.1