1 | /* Sliding Puzzle |
2 | * #Keywords: GdkPaintable, GdkGesture, GtkShortcutController, game |
3 | * |
4 | * This demo demonstrates how to use gestures and paintables to create a |
5 | * small sliding puzzle game. |
6 | */ |
7 | |
8 | #include <gtk/gtk.h> |
9 | |
10 | /* Include the header for the puzzle piece */ |
11 | #include "puzzlepiece.h" |
12 | #include "paintable.h" |
13 | |
14 | |
15 | static GtkWidget *window = NULL; |
16 | static GtkWidget *frame = NULL; |
17 | static GtkWidget *choices = NULL; |
18 | static GtkWidget *size_spin = NULL; |
19 | static GdkPaintable *puzzle = NULL; |
20 | |
21 | static gboolean solved = TRUE; |
22 | static guint width = 3; |
23 | static guint height = 3; |
24 | static guint pos_x; |
25 | static guint pos_y; |
26 | |
27 | static gboolean |
28 | move_puzzle (GtkWidget *grid, |
29 | int dx, |
30 | int dy) |
31 | { |
32 | GtkWidget *pos, *next; |
33 | GdkPaintable *piece; |
34 | guint next_x, next_y; |
35 | |
36 | /* We don't move anything if the puzzle is solved */ |
37 | if (solved) |
38 | return FALSE; |
39 | |
40 | /* Return FALSE if we can't move to where the call |
41 | * wants us to move. |
42 | */ |
43 | if ((dx < 0 && pos_x < -dx) || |
44 | dx + pos_x >= width || |
45 | (dy < 0 && pos_y < -dy) || |
46 | dy + pos_y >= height) |
47 | return FALSE; |
48 | |
49 | /* Compute the new position */ |
50 | next_x = pos_x + dx; |
51 | next_y = pos_y + dy; |
52 | |
53 | /* Get the current and next image */ |
54 | pos = gtk_grid_get_child_at (GTK_GRID (grid), column: pos_x, row: pos_y); |
55 | next = gtk_grid_get_child_at (GTK_GRID (grid), column: next_x, row: next_y); |
56 | |
57 | /* Move the displayed piece. */ |
58 | piece = gtk_picture_get_paintable (self: GTK_PICTURE (ptr: next)); |
59 | gtk_picture_set_paintable (self: GTK_PICTURE (ptr: pos), paintable: piece); |
60 | gtk_picture_set_paintable (self: GTK_PICTURE (ptr: next), NULL); |
61 | |
62 | /* Update the current position */ |
63 | pos_x = next_x; |
64 | pos_y = next_y; |
65 | |
66 | /* Return TRUE because we successfully moved the piece */ |
67 | return TRUE; |
68 | } |
69 | |
70 | static void |
71 | shuffle_puzzle (GtkWidget *grid) |
72 | { |
73 | guint i, n_steps; |
74 | |
75 | /* Do this many random moves */ |
76 | n_steps = width * height * 50; |
77 | |
78 | for (i = 0; i < n_steps; i++) |
79 | { |
80 | /* Get a random number for the direction to move in */ |
81 | switch (g_random_int_range (begin: 0, end: 4)) |
82 | { |
83 | case 0: |
84 | /* left */ |
85 | move_puzzle (grid, dx: -1, dy: 0); |
86 | break; |
87 | |
88 | case 1: |
89 | /* up */ |
90 | move_puzzle (grid, dx: 0, dy: -1); |
91 | break; |
92 | |
93 | case 2: |
94 | /* right */ |
95 | move_puzzle (grid, dx: 1, dy: 0); |
96 | break; |
97 | |
98 | case 3: |
99 | /* down */ |
100 | move_puzzle (grid, dx: 0, dy: 1); |
101 | break; |
102 | |
103 | default: |
104 | g_assert_not_reached (); |
105 | continue; |
106 | } |
107 | } |
108 | } |
109 | |
110 | static gboolean |
111 | check_solved (GtkWidget *grid) |
112 | { |
113 | GtkWidget *picture; |
114 | GdkPaintable *piece; |
115 | guint x, y; |
116 | |
117 | /* Nothing to check if the puzzle is already solved */ |
118 | if (solved) |
119 | return TRUE; |
120 | |
121 | /* If the empty cell isn't in the bottom right, |
122 | * the puzzle is obviously not solved */ |
123 | if (pos_x != width - 1 || |
124 | pos_y != height - 1) |
125 | return FALSE; |
126 | |
127 | /* Check that all pieces are in the right position */ |
128 | for (y = 0; y < height; y++) |
129 | { |
130 | for (x = 0; x < width; x++) |
131 | { |
132 | picture = gtk_grid_get_child_at (GTK_GRID (grid), column: x, row: y); |
133 | piece = gtk_picture_get_paintable (self: GTK_PICTURE (ptr: picture)); |
134 | |
135 | /* empty cell */ |
136 | if (piece == NULL) |
137 | continue; |
138 | |
139 | if (gtk_puzzle_piece_get_x (self: GTK_PUZZLE_PIECE (ptr: piece)) != x || |
140 | gtk_puzzle_piece_get_y (self: GTK_PUZZLE_PIECE (ptr: piece)) != y) |
141 | return FALSE; |
142 | } |
143 | } |
144 | |
145 | /* We solved the puzzle! |
146 | */ |
147 | solved = TRUE; |
148 | |
149 | /* Fill the empty cell to show that we're done. |
150 | */ |
151 | picture = gtk_grid_get_child_at (GTK_GRID (grid), column: 0, row: 0); |
152 | piece = gtk_picture_get_paintable (self: GTK_PICTURE (ptr: picture)); |
153 | |
154 | piece = gtk_puzzle_piece_new (puzzle: gtk_puzzle_piece_get_puzzle (self: GTK_PUZZLE_PIECE (ptr: piece)), |
155 | x: pos_x, y: pos_y, |
156 | width, height); |
157 | picture = gtk_grid_get_child_at (GTK_GRID (grid), column: pos_x, row: pos_y); |
158 | gtk_picture_set_paintable (self: GTK_PICTURE (ptr: picture), paintable: piece); |
159 | |
160 | return TRUE; |
161 | } |
162 | |
163 | static gboolean |
164 | puzzle_key_pressed (GtkWidget *grid, |
165 | GVariant *args, |
166 | gpointer unused) |
167 | { |
168 | int dx, dy; |
169 | |
170 | g_variant_get (value: args, format_string: "(ii)" , &dx, &dy); |
171 | |
172 | if (!move_puzzle (grid, dx, dy)) |
173 | { |
174 | /* Make the error sound and then return TRUE. |
175 | * We handled this key, even though we didn't |
176 | * do anything to the puzzle. |
177 | */ |
178 | gtk_widget_error_bell (widget: grid); |
179 | return TRUE; |
180 | } |
181 | |
182 | check_solved (grid); |
183 | |
184 | return TRUE; |
185 | } |
186 | |
187 | static void |
188 | puzzle_button_pressed (GtkGestureClick *gesture, |
189 | int n_press, |
190 | double x, |
191 | double y, |
192 | GtkWidget *grid) |
193 | { |
194 | GtkWidget *child; |
195 | int l, t, i; |
196 | int pos; |
197 | |
198 | child = gtk_widget_pick (widget: grid, x, y, flags: GTK_PICK_DEFAULT); |
199 | |
200 | if (!child) |
201 | { |
202 | gtk_widget_error_bell (widget: grid); |
203 | return; |
204 | } |
205 | |
206 | gtk_grid_query_child (GTK_GRID (grid), child, column: &l, row: &t, NULL, NULL); |
207 | |
208 | if (l == pos_x && t == pos_y) |
209 | { |
210 | gtk_widget_error_bell (widget: grid); |
211 | } |
212 | else if (l == pos_x) |
213 | { |
214 | pos = pos_y; |
215 | for (i = t; i < pos; i++) |
216 | { |
217 | if (!move_puzzle (grid, dx: 0, dy: -1)) |
218 | gtk_widget_error_bell (widget: grid); |
219 | } |
220 | for (i = pos; i < t; i++) |
221 | { |
222 | if (!move_puzzle (grid, dx: 0, dy: 1)) |
223 | gtk_widget_error_bell (widget: grid); |
224 | } |
225 | } |
226 | else if (t == pos_y) |
227 | { |
228 | pos = pos_x; |
229 | for (i = l; i < pos; i++) |
230 | { |
231 | if (!move_puzzle (grid, dx: -1, dy: 0)) |
232 | gtk_widget_error_bell (widget: grid); |
233 | } |
234 | for (i = pos; i < l; i++) |
235 | { |
236 | if (!move_puzzle (grid, dx: 1, dy: 0)) |
237 | gtk_widget_error_bell (widget: grid); |
238 | } |
239 | } |
240 | else |
241 | { |
242 | gtk_widget_error_bell (widget: grid); |
243 | } |
244 | |
245 | check_solved (grid); |
246 | } |
247 | |
248 | static void |
249 | add_move_binding (GtkShortcutController *controller, |
250 | guint keyval, |
251 | guint kp_keyval, |
252 | int dx, |
253 | int dy) |
254 | { |
255 | GtkShortcut *shortcut; |
256 | |
257 | shortcut = gtk_shortcut_new_with_arguments ( |
258 | trigger: gtk_alternative_trigger_new (first: gtk_keyval_trigger_new (keyval, modifiers: 0), |
259 | second: gtk_keyval_trigger_new (keyval: kp_keyval, modifiers: 0)), |
260 | action: gtk_callback_action_new (callback: puzzle_key_pressed, NULL, NULL), |
261 | format_string: "(ii)" , dx, dy); |
262 | gtk_shortcut_controller_add_shortcut (self: controller, shortcut); |
263 | } |
264 | |
265 | static void |
266 | start_puzzle (GdkPaintable *paintable) |
267 | { |
268 | GtkWidget *picture, *grid; |
269 | GtkEventController *controller; |
270 | guint x, y; |
271 | float aspect_ratio; |
272 | |
273 | /* Create a new grid */ |
274 | grid = gtk_grid_new (); |
275 | gtk_widget_set_focusable (widget: grid, TRUE); |
276 | gtk_aspect_frame_set_child (GTK_ASPECT_FRAME (frame), child: grid); |
277 | aspect_ratio = gdk_paintable_get_intrinsic_aspect_ratio (paintable); |
278 | if (aspect_ratio == 0.0) |
279 | aspect_ratio = 1.0; |
280 | gtk_aspect_frame_set_ratio (GTK_ASPECT_FRAME (frame), ratio: aspect_ratio); |
281 | gtk_aspect_frame_set_obey_child (GTK_ASPECT_FRAME (frame), FALSE); |
282 | |
283 | /* Add shortcuts so people can use the arrow |
284 | * keys to move the puzzle |
285 | */ |
286 | controller = gtk_shortcut_controller_new (); |
287 | gtk_shortcut_controller_set_scope (GTK_SHORTCUT_CONTROLLER (controller), |
288 | scope: GTK_SHORTCUT_SCOPE_LOCAL); |
289 | add_move_binding (GTK_SHORTCUT_CONTROLLER (controller), |
290 | GDK_KEY_Left, GDK_KEY_KP_Left, |
291 | dx: -1, dy: 0); |
292 | add_move_binding (GTK_SHORTCUT_CONTROLLER (controller), |
293 | GDK_KEY_Right, GDK_KEY_KP_Right, |
294 | dx: 1, dy: 0); |
295 | add_move_binding (GTK_SHORTCUT_CONTROLLER (controller), |
296 | GDK_KEY_Up, GDK_KEY_KP_Up, |
297 | dx: 0, dy: -1); |
298 | add_move_binding (GTK_SHORTCUT_CONTROLLER (controller), |
299 | GDK_KEY_Down, GDK_KEY_KP_Down, |
300 | dx: 0, dy: 1); |
301 | gtk_widget_add_controller (GTK_WIDGET (grid), controller); |
302 | |
303 | controller = GTK_EVENT_CONTROLLER (gtk_gesture_click_new ()); |
304 | g_signal_connect (controller, "pressed" , |
305 | G_CALLBACK (puzzle_button_pressed), |
306 | grid); |
307 | gtk_widget_add_controller (GTK_WIDGET (grid), controller); |
308 | |
309 | /* Make sure the cells have equal size */ |
310 | gtk_grid_set_row_homogeneous (GTK_GRID (grid), TRUE); |
311 | gtk_grid_set_column_homogeneous (GTK_GRID (grid), TRUE); |
312 | |
313 | /* Reset the variables */ |
314 | solved = FALSE; |
315 | pos_x = width - 1; |
316 | pos_y = height - 1; |
317 | |
318 | /* add a picture for every cell */ |
319 | for (y = 0; y < height; y++) |
320 | { |
321 | for (x = 0; x < width; x++) |
322 | { |
323 | GdkPaintable *piece; |
324 | |
325 | /* Don't paint anything for the lsiding part of the video */ |
326 | if (x == pos_x && y == pos_y) |
327 | piece = NULL; |
328 | else |
329 | piece = gtk_puzzle_piece_new (puzzle: paintable, |
330 | x, y, |
331 | width, height); |
332 | picture = gtk_picture_new_for_paintable (paintable: piece); |
333 | gtk_picture_set_keep_aspect_ratio (self: GTK_PICTURE (ptr: picture), FALSE); |
334 | gtk_grid_attach (GTK_GRID (grid), |
335 | child: picture, |
336 | column: x, row: y, |
337 | width: 1, height: 1); |
338 | } |
339 | } |
340 | |
341 | shuffle_puzzle (grid); |
342 | } |
343 | |
344 | static void |
345 | reshuffle (void) |
346 | { |
347 | GtkWidget *grid; |
348 | |
349 | if (solved) |
350 | { |
351 | start_puzzle (paintable: puzzle); |
352 | grid = gtk_aspect_frame_get_child (GTK_ASPECT_FRAME (frame)); |
353 | } |
354 | else |
355 | { |
356 | grid = gtk_aspect_frame_get_child (GTK_ASPECT_FRAME (frame)); |
357 | shuffle_puzzle (grid); |
358 | } |
359 | gtk_widget_grab_focus (widget: grid); |
360 | } |
361 | |
362 | static void |
363 | reconfigure (void) |
364 | { |
365 | GtkWidget *popover; |
366 | GtkWidget *grid; |
367 | GtkWidget *child; |
368 | GtkWidget *image; |
369 | GList *selected; |
370 | |
371 | width = height = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (size_spin)); |
372 | |
373 | selected = gtk_flow_box_get_selected_children (GTK_FLOW_BOX (choices)); |
374 | if (selected == NULL) |
375 | child = gtk_widget_get_first_child (widget: choices); |
376 | else |
377 | { |
378 | child = selected->data; |
379 | g_list_free (list: selected); |
380 | } |
381 | |
382 | image = gtk_flow_box_child_get_child (GTK_FLOW_BOX_CHILD (child)); |
383 | puzzle = gtk_image_get_paintable (GTK_IMAGE (image)); |
384 | |
385 | start_puzzle (paintable: puzzle); |
386 | popover = gtk_widget_get_ancestor (widget: size_spin, GTK_TYPE_POPOVER); |
387 | gtk_popover_popdown (GTK_POPOVER (popover)); |
388 | grid = gtk_aspect_frame_get_child (GTK_ASPECT_FRAME (frame)); |
389 | gtk_widget_grab_focus (widget: grid); |
390 | } |
391 | |
392 | static void |
393 | add_choice (GtkWidget *container, |
394 | GdkPaintable *paintable) |
395 | { |
396 | GtkWidget *icon; |
397 | |
398 | icon = gtk_image_new_from_paintable (paintable); |
399 | gtk_image_set_icon_size (GTK_IMAGE (icon), icon_size: GTK_ICON_SIZE_LARGE); |
400 | |
401 | gtk_flow_box_insert (GTK_FLOW_BOX (container), widget: icon, position: -1); |
402 | } |
403 | |
404 | static void |
405 | widget_destroyed (gpointer data, |
406 | GObject *widget) |
407 | { |
408 | if (data) |
409 | *(gpointer *) data = NULL; |
410 | } |
411 | |
412 | |
413 | GtkWidget * |
414 | do_sliding_puzzle (GtkWidget *do_widget) |
415 | { |
416 | if (!window) |
417 | { |
418 | GtkWidget *; |
419 | GtkWidget *restart; |
420 | GtkWidget *tweak; |
421 | GtkWidget *popover; |
422 | GtkWidget *tweaks; |
423 | GtkWidget *apply; |
424 | GtkWidget *label; |
425 | GtkWidget *sw; |
426 | GtkMediaStream *media; |
427 | |
428 | puzzle = GDK_PAINTABLE (ptr: gdk_texture_new_from_resource (resource_path: "/sliding_puzzle/portland-rose.jpg" )); |
429 | |
430 | tweaks = gtk_grid_new (); |
431 | gtk_grid_set_row_spacing (GTK_GRID (tweaks), spacing: 10); |
432 | gtk_grid_set_column_spacing (GTK_GRID (tweaks), spacing: 10); |
433 | gtk_widget_set_margin_start (widget: tweaks, margin: 10); |
434 | gtk_widget_set_margin_end (widget: tweaks, margin: 10); |
435 | gtk_widget_set_margin_top (widget: tweaks, margin: 10); |
436 | gtk_widget_set_margin_bottom (widget: tweaks, margin: 10); |
437 | |
438 | choices = gtk_flow_box_new (); |
439 | gtk_widget_add_css_class (widget: choices, css_class: "view" ); |
440 | add_choice (container: choices, paintable: puzzle); |
441 | add_choice (container: choices, paintable: gtk_nuclear_animation_new (TRUE)); |
442 | media = gtk_media_file_new_for_resource (resource_path: "/images/gtk-logo.webm" ); |
443 | gtk_media_stream_set_loop (self: media, TRUE); |
444 | gtk_media_stream_set_muted (self: media, TRUE); |
445 | gtk_media_stream_play (self: media); |
446 | add_choice (container: choices, paintable: GDK_PAINTABLE (ptr: media)); |
447 | sw = gtk_scrolled_window_new (); |
448 | gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (sw), child: choices); |
449 | gtk_grid_attach (GTK_GRID (tweaks), child: sw, column: 0, row: 0, width: 2, height: 1); |
450 | |
451 | label = gtk_label_new (str: "Size" ); |
452 | gtk_label_set_xalign (GTK_LABEL (label), xalign: 0.0); |
453 | gtk_grid_attach (GTK_GRID (tweaks), child: label, column: 0, row: 1, width: 1, height: 1); |
454 | size_spin = gtk_spin_button_new_with_range (min: 2, max: 10, step: 1); |
455 | gtk_spin_button_set_value (GTK_SPIN_BUTTON (size_spin), value: width); |
456 | gtk_grid_attach (GTK_GRID (tweaks), child: size_spin, column: 1, row: 1, width: 1, height: 1); |
457 | |
458 | apply = gtk_button_new_with_label (label: "Apply" ); |
459 | gtk_widget_set_halign (widget: apply, align: GTK_ALIGN_END); |
460 | gtk_grid_attach (GTK_GRID (tweaks), child: apply, column: 1, row: 2, width: 1, height: 1); |
461 | g_signal_connect (apply, "clicked" , G_CALLBACK (reconfigure), NULL); |
462 | |
463 | popover = gtk_popover_new (); |
464 | gtk_popover_set_child (GTK_POPOVER (popover), child: tweaks); |
465 | |
466 | tweak = gtk_menu_button_new (); |
467 | gtk_menu_button_set_popover (GTK_MENU_BUTTON (tweak), popover); |
468 | gtk_menu_button_set_icon_name (GTK_MENU_BUTTON (tweak), icon_name: "emblem-system-symbolic" ); |
469 | |
470 | restart = gtk_button_new_from_icon_name (icon_name: "view-refresh-symbolic" ); |
471 | g_signal_connect (restart, "clicked" , G_CALLBACK (reshuffle), NULL); |
472 | |
473 | header = gtk_header_bar_new (); |
474 | gtk_header_bar_pack_start (GTK_HEADER_BAR (header), child: restart); |
475 | gtk_header_bar_pack_end (GTK_HEADER_BAR (header), child: tweak); |
476 | window = gtk_window_new (); |
477 | gtk_window_set_display (GTK_WINDOW (window), |
478 | display: gtk_widget_get_display (widget: do_widget)); |
479 | gtk_window_set_title (GTK_WINDOW (window), title: "Sliding Puzzle" ); |
480 | gtk_window_set_titlebar (GTK_WINDOW (window), titlebar: header); |
481 | gtk_window_set_default_size (GTK_WINDOW (window), width: 400, height: 300); |
482 | g_object_weak_ref (G_OBJECT (window), notify: widget_destroyed, data: &window); |
483 | |
484 | frame = gtk_aspect_frame_new (xalign: 0.5, yalign: 0.5, ratio: (float) gdk_paintable_get_intrinsic_aspect_ratio (paintable: puzzle), FALSE); |
485 | gtk_window_set_child (GTK_WINDOW (window), child: frame); |
486 | |
487 | start_puzzle (paintable: puzzle); |
488 | } |
489 | |
490 | if (!gtk_widget_get_visible (widget: window)) |
491 | gtk_widget_show (widget: window); |
492 | else |
493 | gtk_window_destroy (GTK_WINDOW (window)); |
494 | |
495 | return window; |
496 | } |
497 | |