1/* -*- Mode: vala; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2/*
3   This file is part of GNOME Four-in-a-row.
4
5   Copyright © 2018 Jacob Humphrey
6
7   GNOME Four-in-a-row is free software: you can redistribute it and/or
8   modify it under the terms of the GNU General Public License as published
9   by the Free Software Foundation, either version 3 of the License, or
10   (at your option) any later version.
11
12   GNOME Four-in-a-row is distributed in the hope that it will be useful,
13   but WITHOUT ANY WARRANTY; without even the implied warranty of
14   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15   GNU General Public License for more details.
16
17   You should have received a copy of the GNU General Public License along
18   with GNOME Four-in-a-row.  If not, see <https://www.gnu.org/licenses/>.
19*/
20
21private class GameBoardView : Gtk.DrawingArea
22{
23    private enum Tile {
24        PLAYER1,
25        PLAYER2,
26        CLEAR,
27        CLEAR_CURSOR,
28        PLAYER1_CURSOR,
29        PLAYER2_CURSOR;
30    }
31
32    [CCode (notify = false)] public Board        game_board    { private get; protected construct; }
33    [CCode (notify = false)] public ThemeManager theme_manager { private get; protected construct; }
34
35    internal GameBoardView (Board game_board, ThemeManager theme_manager)
36    {
37        Object (game_board: game_board, theme_manager: theme_manager);
38    }
39
40    construct
41    {
42        events = Gdk.EventMask.EXPOSURE_MASK
43               | Gdk.EventMask.BUTTON_PRESS_MASK
44               | Gdk.EventMask.BUTTON_RELEASE_MASK;
45        theme_manager.theme_changed.connect (refresh_pixmaps);
46
47        init_mouse ();
48    }
49
50    /*\
51    * * drawing variables
52    \*/
53
54    private int board_size = 0;
55    private int tile_size = 0;
56    private int offset [6];
57    private int board_x;
58    private int board_y;
59
60    internal inline void draw_tile (int row, int col)
61    {
62        queue_draw_area (/* start */ col * tile_size + board_x,
63                                     row * tile_size + board_y,
64                         /* size  */ tile_size,
65                                     tile_size);
66    }
67
68    protected override bool configure_event (Gdk.EventConfigure e)
69    {
70        int allocated_width  = get_allocated_width ();
71        int allocated_height = get_allocated_height ();
72        int size = int.min (allocated_width, allocated_height);
73        tile_size = size / game_board.size;
74        board_size = tile_size * game_board.size;
75        board_x = (allocated_width  - board_size) / 2;
76        board_y = (allocated_height - board_size) / 2;
77
78        offset [Tile.PLAYER1]        = 0;
79        offset [Tile.PLAYER2]        = tile_size;
80        offset [Tile.CLEAR]          = tile_size * 2;
81        offset [Tile.CLEAR_CURSOR]   = tile_size * 3;
82        offset [Tile.PLAYER1_CURSOR] = tile_size * 4;
83        offset [Tile.PLAYER2_CURSOR] = tile_size * 5;
84
85        refresh_pixmaps ();
86        return true;
87    }
88
89    /*\
90    * * drawing
91    \*/
92
93    protected override bool draw (Cairo.Context cr)
94    {
95        /* background */
96        cr.save ();
97        cr.translate (board_x, board_y);
98        Gdk.cairo_set_source_pixbuf (cr, pb_bground, 0.0, 0.0);
99        cr.rectangle (0.0, 0.0, board_size, board_size);
100        cr.paint ();
101        cr.restore ();
102
103        /* tiles */
104        for (uint8 row = 0; row < /* BOARD_ROWS_PLUS_ONE */ game_board.size; row++)
105            for (uint8 col = 0; col < /* BOARD_COLUMNS */ game_board.size; col++)
106                paint_tile (cr, row, col);
107
108        /* grid */
109        cr.save ();
110        cr.translate (board_x, board_y);
111        draw_grid (cr);
112        cr.restore ();
113
114        return false;
115    }
116
117    private inline void paint_tile (Cairo.Context cr, uint8 row, uint8 col)
118    {
119        Player tile = game_board [row, col];
120        if (tile == Player.NOBODY && row != 0)
121            return;
122
123        int os = 0;
124        if (row == 0)
125            switch (tile)
126            {
127                case Player.HUMAN   : os = offset [Tile.PLAYER1_CURSOR]; break;
128                case Player.OPPONENT: os = offset [Tile.PLAYER2_CURSOR]; break;
129                case Player.NOBODY  : os = offset [Tile.CLEAR_CURSOR];   break;
130            }
131        else
132            switch (tile)
133            {
134                case Player.HUMAN   : os = offset [Tile.PLAYER1]; break;
135                case Player.OPPONENT: os = offset [Tile.PLAYER2]; break;
136                case Player.NOBODY  : assert_not_reached ();
137            }
138
139        cr.save ();
140        int x = col * tile_size + board_x;
141        int y = row * tile_size + board_y;
142        Gdk.cairo_set_source_pixbuf (cr, pb_tileset, x - os, y);
143        cr.rectangle (x, y, tile_size, tile_size);
144
145        cr.clip ();
146        cr.paint ();
147        cr.restore ();
148    }
149
150    private inline void draw_grid (Cairo.Context cr)
151    {
152        const double dashes [] = { 4.0, 4.0 };
153        Gdk.RGBA color = Gdk.RGBA ();
154
155        color.parse (theme_manager.get_grid_color ());
156        Gdk.cairo_set_source_rgba (cr, color);
157        cr.set_operator (Cairo.Operator.SOURCE);
158        cr.set_line_width (1.0);
159        cr.set_line_cap (Cairo.LineCap.BUTT);
160        cr.set_line_join (Cairo.LineJoin.MITER);
161        cr.set_dash (dashes, /* offset */ 0.0);
162
163        /* draw the grid on the background pixmap */
164        for (uint8 i = 1; i < /* BOARD_SIZE */ game_board.size; i++)
165        {
166            double line_offset = i * tile_size + 0.5;
167            // vertical lines
168            cr.move_to (line_offset, 0.0        );
169            cr.line_to (line_offset, board_size );
170            // horizontal lines
171            cr.move_to (0.0        , line_offset);
172            cr.line_to (board_size , line_offset);
173        }
174        cr.stroke ();
175
176        /* Draw separator line at the top */
177        cr.set_dash (null, /* offset */ 0.0);
178        cr.move_to (0.0, tile_size + 0.5);
179        cr.line_to (board_size, tile_size + 0.5);
180
181        cr.stroke ();
182    }
183
184    /*\
185    * * pixmaps
186    \*/
187
188    /* scaled pixbufs */
189    private Gdk.Pixbuf pb_tileset;
190    private Gdk.Pixbuf pb_bground;
191
192    private void refresh_pixmaps ()
193    {
194        if (tile_size == 0) // happens at game start
195            return;
196
197        Gdk.Pixbuf? tmp_pixbuf;
198
199        tmp_pixbuf = theme_manager.pb_tileset_raw.scale_simple (tile_size * 6, tile_size, Gdk.InterpType.BILINEAR);
200        if (tmp_pixbuf == null)
201            assert_not_reached ();
202        pb_tileset = (!) tmp_pixbuf;
203
204        tmp_pixbuf = theme_manager.pb_bground_raw.scale_simple (board_size, board_size, Gdk.InterpType.BILINEAR);
205        if (tmp_pixbuf == null)
206            assert_not_reached ();
207        pb_bground = (!) tmp_pixbuf;
208
209        queue_draw ();
210    }
211
212    /*\
213    * * mouse play
214    \*/
215
216    private Gtk.GestureMultiPress click_controller;     // for keeping in memory
217
218    private inline void init_mouse ()
219    {
220        click_controller = new Gtk.GestureMultiPress (this);
221        click_controller.set_button (/* all buttons */ 0);
222        click_controller.pressed.connect (on_click);
223    }
224
225    /**
226     * column_clicked:
227     *
228     * emitted when a column on the game board is clicked
229     *
230     * @column:
231     *
232     * Which column was clicked on
233     */
234    internal signal bool column_clicked (uint8 column);
235
236    private inline void on_click (Gtk.GestureMultiPress _click_controller, int n_press, double event_x, double event_y)
237    {
238        uint button = _click_controller.get_current_button ();
239        if (button != Gdk.BUTTON_PRIMARY && button != Gdk.BUTTON_SECONDARY)
240            return;
241
242        Gdk.Event? event = Gtk.get_current_event ();
243        if (event == null && ((!) event).type != Gdk.EventType.BUTTON_PRESS)
244            assert_not_reached ();
245
246        int x;
247        int y;
248        Gdk.Window? window = get_window ();
249        if (window == null)
250            assert_not_reached ();
251        ((!) window).get_device_position (((Gdk.EventButton) (!) event).device, out x, out y, null);
252
253        uint8 col;
254        if (get_column (x, y, out col))
255            column_clicked (col);
256    }
257
258    private inline bool get_column (int x, int y, out uint8 col)
259    {
260        int _col = (x - board_x) / tile_size;
261        if (x < board_x || y < board_y || _col < 0 || _col > /* BOARD_COLUMNS_MINUS_ONE */ game_board.size - 1)
262        {
263            col = 0;
264            return false;
265        }
266        col = (uint8) _col;
267
268        int row = (y - board_y) / tile_size;
269        if (row < 0 || row > /* BOARD_ROWS */ game_board.size - 1)
270            return false;
271
272        return true;
273    }
274}
275