From b6c8943bbfb046f4f8c2b468a71b407115735760 Mon Sep 17 00:00:00 2001 From: Benjamin Otte Date: Mon, 4 Jun 2018 05:41:44 +0200 Subject: [PATCH] demo: Add the sliding puzzle demo --- demos/gtk-demo/demo.gresource.xml | 12 +- demos/gtk-demo/meson.build | 3 +- demos/gtk-demo/puzzlepiece.c | 220 ++++++++++++++++++++++ demos/gtk-demo/puzzlepiece.h | 23 +++ demos/gtk-demo/sliding_puzzle.c | 303 ++++++++++++++++++++++++++++++ 5 files changed, 557 insertions(+), 4 deletions(-) create mode 100644 demos/gtk-demo/puzzlepiece.c create mode 100644 demos/gtk-demo/puzzlepiece.h create mode 100644 demos/gtk-demo/sliding_puzzle.c diff --git a/demos/gtk-demo/demo.gresource.xml b/demos/gtk-demo/demo.gresource.xml index 0b6952d237..1849e509f9 100644 --- a/demos/gtk-demo/demo.gresource.xml +++ b/demos/gtk-demo/demo.gresource.xml @@ -105,9 +105,6 @@ gnome-fs-directory.png gnome-fs-regular.png - - stack.ui - shortcuts.ui shortcuts-builder.ui @@ -115,6 +112,14 @@ shortcuts-clocks.ui shortcuts-boxes.ui + + puzzlepiece.c + puzzlepiece.h + portland-rose.jpg + + + stack.ui + revealer.ui @@ -198,6 +203,7 @@ shortcuts.c sizegroup.c sidebar.c + sliding_puzzle.c stack.c spinbutton.c spinner.c diff --git a/demos/gtk-demo/meson.build b/demos/gtk-demo/meson.build index 06e8b7f502..98edcfd6d4 100644 --- a/demos/gtk-demo/meson.build +++ b/demos/gtk-demo/meson.build @@ -61,6 +61,7 @@ demos = files([ 'shortcuts.c', 'sidebar.c', 'sizegroup.c', + 'sliding_puzzle.c', 'spinbutton.c', 'spinner.c', 'stack.c', @@ -76,7 +77,7 @@ demos = files([ gtkdemo_deps = [ libgtk_dep, ] -extra_demo_sources = files(['main.c', 'gtkfishbowl.c', 'fontplane.c', 'gtkgears.c']) +extra_demo_sources = files(['main.c', 'gtkfishbowl.c', 'fontplane.c', 'gtkgears.c', 'puzzlepiece.c']) if harfbuzz_dep.found() and pangoft_dep.found() demos += files('font_features.c') diff --git a/demos/gtk-demo/puzzlepiece.c b/demos/gtk-demo/puzzlepiece.c new file mode 100644 index 0000000000..cb8a81b9b9 --- /dev/null +++ b/demos/gtk-demo/puzzlepiece.c @@ -0,0 +1,220 @@ +/* Paintable/A simple paintable + * + * GdkPaintable is an interface used by GTK for drawings of any sort + * that do not require layouting or positioning. + * + * This demo code gives a simple example on how a paintable can + * be created. + * + * Paintables can be used in many places inside GTK widgets, but the + * most common usage is inside GtkImage and that's what we're going + * to do here. + */ + +#include + +#include "puzzlepiece.h" + +/* Declare the struct. */ +struct _GtkPuzzlePiece +{ + GObject parent_instance; + + GdkPaintable *puzzle; + guint x; + guint y; + guint width; + guint height; +}; + +struct _GtkPuzzlePieceClass +{ + GObjectClass parent_class; +}; + +/* This is the function that draws the puzzle piece. + * It just draws a rectangular cutout of the puzzle by clipping + * away the rest. + */ +static void +gtk_puzzle_piece_snapshot (GdkPaintable *paintable, + GdkSnapshot *snapshot, + double width, + double height) +{ + GtkPuzzlePiece *self = GTK_PUZZLE_PIECE (paintable); + + gtk_snapshot_push_clip (snapshot, + &GRAPHENE_RECT_INIT (0, 0, width, height)); + + gtk_snapshot_offset (snapshot, + - width * self->x, + - height * self->y); + gdk_paintable_snapshot (self->puzzle, + snapshot, + width * self->width, + height * self->height); + + gtk_snapshot_pop (snapshot); +} + +static GdkPaintableFlags +gtk_puzzle_piece_get_flags (GdkPaintable *paintable) +{ + GtkPuzzlePiece *self = GTK_PUZZLE_PIECE (paintable); + + /* The flags are the same as the ones of the puzzle. + * If the puzzle changes in some way, so do the pieces. + */ + return gdk_paintable_get_flags (self->puzzle); +} + +static int +gtk_puzzle_piece_get_intrinsic_width (GdkPaintable *paintable) +{ + GtkPuzzlePiece *self = GTK_PUZZLE_PIECE (paintable); + + /* We can compute our width relative to the puzzle. + * This logic even works for the case where the puzzle + * has no width, because the 0 return value is unchanged. + * Round up the value. + */ + return (gdk_paintable_get_intrinsic_width (self->puzzle) + self->width - 1) / self->width; +} + +static int +gtk_puzzle_piece_get_intrinsic_height (GdkPaintable *paintable) +{ + GtkPuzzlePiece *self = GTK_PUZZLE_PIECE (paintable); + + /* Do the same thing we did for the width with the height. + */ + return (gdk_paintable_get_intrinsic_height (self->puzzle) + self->height - 1) / self->height; +} + +static double +gtk_puzzle_piece_get_intrinsic_aspect_ratio (GdkPaintable *paintable) +{ + GtkPuzzlePiece *self = GTK_PUZZLE_PIECE (paintable); + + /* We can compute our aspect ratio relative to the puzzle. + * This logic again works for the case where the puzzle + * has no aspect ratio, because the 0 return value is unchanged. + */ + return gdk_paintable_get_intrinsic_aspect_ratio (self->puzzle) * self->height / self->width; +} + +static void +gtk_puzzle_piece_paintable_init (GdkPaintableInterface *iface) +{ + iface->snapshot = gtk_puzzle_piece_snapshot; + iface->get_flags = gtk_puzzle_piece_get_flags; + iface->get_intrinsic_width = gtk_puzzle_piece_get_intrinsic_width; + iface->get_intrinsic_height = gtk_puzzle_piece_get_intrinsic_height; + iface->get_intrinsic_aspect_ratio = gtk_puzzle_piece_get_intrinsic_aspect_ratio; +} + +/* When defining the GType, we need to implement the GdkPaintable interface */ +G_DEFINE_TYPE_WITH_CODE (GtkPuzzlePiece, gtk_puzzle_piece, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (GDK_TYPE_PAINTABLE, + gtk_puzzle_piece_paintable_init)) + +/* We need to declare a destructor to release our reference to the + * puzzle paintable and disconnect our signal handlers. + */ +static void +gtk_puzzle_piece_dispose (GObject *object) +{ + GtkPuzzlePiece *self = GTK_PUZZLE_PIECE (object); + + if (self->puzzle) + { + g_signal_handlers_disconnect_by_func (self->puzzle, gdk_paintable_invalidate_contents, self); + g_signal_handlers_disconnect_by_func (self->puzzle, gdk_paintable_invalidate_size, self); + g_clear_object (&self->puzzle); + } + + G_OBJECT_CLASS (gtk_puzzle_piece_parent_class)->dispose (object); +} + +/* Here's the boilerplate for the GObject declaration. + */ +static void +gtk_puzzle_piece_class_init (GtkPuzzlePieceClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->dispose = gtk_puzzle_piece_dispose; +} + +static void +gtk_puzzle_piece_init (GtkPuzzlePiece *self) +{ +} + +/* And finally, we add a constructor. + * It is declared in the header so that the other examples + * can use it. + */ +GdkPaintable * +gtk_puzzle_piece_new (GdkPaintable *puzzle, + guint x, + guint y, + guint width, + guint height) +{ + GtkPuzzlePiece *self; + + /* These are sanity checks, so that we get warnings if we accidentally + * do anything stupid. */ + g_return_val_if_fail (GDK_IS_PAINTABLE (puzzle), NULL); + g_return_val_if_fail (width > 0, NULL); + g_return_val_if_fail (height > 0, NULL); + g_return_val_if_fail (x < width, NULL); + g_return_val_if_fail (y < height, NULL); + + self = g_object_new (GTK_TYPE_PUZZLE_PIECE, NULL); + + self->puzzle = g_object_ref (puzzle); + g_signal_connect_swapped (puzzle, "invalidate-contents", G_CALLBACK (gdk_paintable_invalidate_contents), self); + g_signal_connect_swapped (puzzle, "invalidate-size", G_CALLBACK (gdk_paintable_invalidate_size), self); + self->x = x; + self->y = y; + self->width = width; + self->height = height; + + return GDK_PAINTABLE (self); +} + +/* Here are the accessors that we need to inspect the puzzle + * pieces in other code. + */ +GdkPaintable * +gtk_puzzle_piece_get_puzzle (GtkPuzzlePiece *self) +{ + /* Add sanity checks here, too. + * If you make a habit out of this, you can always rely + * on your code having sanity checks, which makes it + * way easier to debug. + */ + g_return_val_if_fail (GTK_IS_PUZZLE_PIECE (self), NULL); + + return self->puzzle; +} + +guint +gtk_puzzle_piece_get_x (GtkPuzzlePiece *self) +{ + g_return_val_if_fail (GTK_IS_PUZZLE_PIECE (self), 0); + + return self->x; +} + +guint +gtk_puzzle_piece_get_y (GtkPuzzlePiece *self) +{ + g_return_val_if_fail (GTK_IS_PUZZLE_PIECE (self), 0); + + return self->y; +} + diff --git a/demos/gtk-demo/puzzlepiece.h b/demos/gtk-demo/puzzlepiece.h new file mode 100644 index 0000000000..b7fc97dc05 --- /dev/null +++ b/demos/gtk-demo/puzzlepiece.h @@ -0,0 +1,23 @@ +#ifndef __PUZZLE_PIECE_H__ +#define __PUZZLE_PIECE_H__ + +#include + +/* First, add the boilerplate for the object itself. + */ +#define GTK_TYPE_PUZZLE_PIECE (gtk_puzzle_piece_get_type ()) +G_DECLARE_FINAL_TYPE (GtkPuzzlePiece, gtk_puzzle_piece, GTK, PUZZLE_PIECE, GObject) + +/* Then, declare all constructors */ +GdkPaintable * gtk_puzzle_piece_new (GdkPaintable *puzzle, + guint x, + guint y, + guint width, + guint height); + +/* Next, add the getters and setters for object properties */ +GdkPaintable * gtk_puzzle_piece_get_puzzle (GtkPuzzlePiece *self); +guint gtk_puzzle_piece_get_x (GtkPuzzlePiece *self); +guint gtk_puzzle_piece_get_y (GtkPuzzlePiece *self); + +#endif /* __PUZZLE_PIECE_H__ */ diff --git a/demos/gtk-demo/sliding_puzzle.c b/demos/gtk-demo/sliding_puzzle.c new file mode 100644 index 0000000000..0ba76155d8 --- /dev/null +++ b/demos/gtk-demo/sliding_puzzle.c @@ -0,0 +1,303 @@ +/* Sliding puzzle + * + * This demo demonstrates how to use gestures and paintables to create a + * small sliding puzzle game. + * + */ + +#include + +/* Include the header for the puzzle piece */ +#include "puzzlepiece.h" + +static GtkWidget *window = NULL; + +static gboolean solved = TRUE; +static guint width = 6; +static guint height = 6; +static guint pos_x; +static guint pos_y; + +static gboolean +move_puzzle (GtkWidget *grid, + int dx, + int dy) +{ + GtkWidget *pos, *next; + GdkPaintable *piece; + guint next_x, next_y; + + /* We don't move anything if the puzzle is solved */ + if (solved) + return FALSE; + + /* Return FALSE if we can't move to where the call + * wants us to move. + */ + if ((dx < 0 && pos_x < -dx) || + dx + pos_x >= width || + (dy < 0 && pos_y < -dy) || + dy + pos_y >= height) + return FALSE; + + /* Compute the new position */ + next_x = pos_x + dx; + next_y = pos_y + dy; + + /* Get the current and next image */ + pos = gtk_grid_get_child_at (GTK_GRID (grid), pos_x, pos_y); + next = gtk_grid_get_child_at (GTK_GRID (grid), next_x, next_y); + + /* Move the displayed piece. */ + piece = gtk_image_get_paintable (GTK_IMAGE (next)); + gtk_image_set_from_paintable (GTK_IMAGE (pos), piece); + gtk_image_clear (GTK_IMAGE (next)); + + /* Update the current position */ + pos_x = next_x; + pos_y = next_y; + + /* Return TRUE because we successfully moved the piece */ + return TRUE; +} + +static void +shuffle_puzzle (GtkWidget *grid) +{ + guint i, n_steps; + + /* Do this many random moves */ + n_steps = width * height * 50; + + for (i = 0; i < n_steps; i++) + { + /* Get a random number for the direction to move in */ + switch (g_random_int_range (0, 4)) + { + case 0: + /* left */ + move_puzzle (grid, -1, 0); + break; + + case 1: + /* up */ + move_puzzle (grid, 0, -1); + break; + + case 2: + /* right */ + move_puzzle (grid, 1, 0); + break; + + case 3: + /* down */ + move_puzzle (grid, 0, 1); + break; + + default: + g_assert_not_reached (); + continue; + } + } +} + +static gboolean +check_solved (GtkWidget *grid) +{ + GtkWidget *image; + GdkPaintable *piece; + guint x, y; + + /* Nothing to check if the puzzle is already solved */ + if (solved) + return TRUE; + + /* If the empty cell isn't in the bottom right, + * the puzzle is obviously not solved */ + if (pos_x != width - 1 || + pos_y != height - 1) + return FALSE; + + /* Check that all pieces are in the right position */ + for (y = 0; y < height; y++) + { + for (x = 0; x < width; x++) + { + image = gtk_grid_get_child_at (GTK_GRID (grid), x, y); + piece = gtk_image_get_paintable (GTK_IMAGE (image)); + + /* empty cell */ + if (piece == NULL) + continue; + + if (gtk_puzzle_piece_get_x (GTK_PUZZLE_PIECE (piece)) != x || + gtk_puzzle_piece_get_y (GTK_PUZZLE_PIECE (piece)) != y) + return FALSE; + } + } + + /* We solved the puzzle! + */ + solved = TRUE; + + /* Fill the empty cell to show that we're done. + */ + image = gtk_grid_get_child_at (GTK_GRID (grid), 0, 0); + piece = gtk_image_get_paintable (GTK_IMAGE (image)); + + piece = gtk_puzzle_piece_new (gtk_puzzle_piece_get_puzzle (GTK_PUZZLE_PIECE (piece)), + pos_x, pos_y, + width, height); + image = gtk_grid_get_child_at (GTK_GRID (grid), pos_x, pos_y); + gtk_image_set_from_paintable (GTK_IMAGE (image), piece); + + return TRUE; +} + +static gboolean +puzzle_key_pressed (GtkEventControllerKey *controller, + guint keyval, + guint keycode, + GdkModifierType state, + GtkWidget *grid) +{ + int dx, dy; + + dx = 0; + dy = 0; + + switch (keyval) + { + case GDK_KEY_KP_Left: + case GDK_KEY_Left: + /* left */ + dx = -1; + break; + + case GDK_KEY_KP_Up: + case GDK_KEY_Up: + /* up */ + dy = -1; + break; + + case GDK_KEY_KP_Right: + case GDK_KEY_Right: + /* right */ + dx = 1; + break; + + case GDK_KEY_KP_Down: + case GDK_KEY_Down: + /* down */ + dy = 1; + break; + + default: + /* We return FALSE here because we didn't handle the key that was pressed */ + return FALSE; + } + + if (!move_puzzle (grid, dx, dy)) + { + /* Make the error sound and then return TRUE. + * We handled this key, even though we didn't + * do anything to the puzzle. + */ + gtk_widget_error_bell (grid); + return TRUE; + } + + check_solved (grid); + + return TRUE; +} + +static void +start_puzzle (GdkPaintable *puzzle) +{ + GtkWidget *image, *grid; + GtkEventController *controller; + guint x, y; + + /* Remove the old grid (if there is one) */ + grid = gtk_bin_get_child (GTK_BIN (window)); + if (grid) + gtk_container_remove (GTK_CONTAINER (window), grid); + + /* Create a new grid */ + grid = gtk_grid_new (); + gtk_widget_set_can_focus (grid, TRUE); + gtk_container_add (GTK_CONTAINER (window), grid); + + /* Add a key event controller so people can use the arrow + * keys to move the puzzle */ + controller = gtk_event_controller_key_new (); + g_signal_connect (controller, "key-pressed", + G_CALLBACK (puzzle_key_pressed), + grid); + gtk_widget_add_controller (GTK_WIDGET (grid), controller); + + /* Make sure the cells have equal size */ + gtk_grid_set_row_homogeneous (GTK_GRID (grid), TRUE); + gtk_grid_set_column_homogeneous (GTK_GRID (grid), TRUE); + + /* Reset the variables */ + solved = FALSE; + pos_x = width - 1; + pos_y = height - 1; + + /* add an image for every cell */ + for (y = 0; y < height; y++) + { + for (x = 0; x < width; x++) + { + GdkPaintable *piece; + + /* Don't paint anything for the lsiding part of the video */ + if (x == pos_x && y == pos_y) + piece = NULL; + else + piece = gtk_puzzle_piece_new (puzzle, + x, y, + width, height); + image = gtk_image_new_from_paintable (piece); + gtk_image_set_keep_aspect_ratio (GTK_IMAGE (image), FALSE); + gtk_image_set_can_shrink (GTK_IMAGE (image), TRUE); + gtk_grid_attach (GTK_GRID (grid), + image, + x, y, + 1, 1); + } + } + + shuffle_puzzle (grid); +} + +GtkWidget * +do_sliding_puzzle (GtkWidget *do_widget) +{ + GdkPaintable *puzzle; + + if (!window) + { + window = gtk_window_new (GTK_WINDOW_TOPLEVEL); + gtk_window_set_display (GTK_WINDOW (window), + gtk_widget_get_display (do_widget)); + gtk_window_set_title (GTK_WINDOW (window), "Sliding Puzzle"); + gtk_window_set_default_size (GTK_WINDOW (window), 300, 200); + g_signal_connect (window, "destroy", + G_CALLBACK (gtk_widget_destroyed), &window); + + /* Start a puzzle with a default image */ + puzzle = GDK_PAINTABLE (gdk_texture_new_from_resource ("/sliding_puzzle/portland-rose.jpg")); + start_puzzle (puzzle); + g_object_unref (puzzle); + } + + if (!gtk_widget_get_visible (window)) + gtk_widget_show (window); + else + gtk_widget_destroy (window); + + return window; +}