[gnome-games] gnotravex: Port from C to Vala



commit af7197eb48b8d5aa62a4915afe1d1cb9344c6b7e
Author: Robert Ancell <robert ancell canonical com>
Date:   Thu Nov 17 08:57:42 2011 +1100

    gnotravex: Port from C to Vala

 configure.in                   |    4 +-
 gnotravex/data/Makefile.am     |    4 -
 gnotravex/data/baize.png       |  Bin 470 -> 0 bytes
 gnotravex/src/Makefile.am      |   23 +-
 gnotravex/src/config.vapi      |    2 +
 gnotravex/src/gnotravex.c      | 1902 ----------------------------------------
 gnotravex/src/gnotravex.vala   |  413 +++++++++
 gnotravex/src/puzzle-view.vala |  450 ++++++++++
 gnotravex/src/puzzle.vala      |  280 ++++++
 gnotravex/src/theme.vala       |  272 ++++++
 10 files changed, 1437 insertions(+), 1913 deletions(-)
---
diff --git a/configure.in b/configure.in
index f2cc752..130e52a 100644
--- a/configure.in
+++ b/configure.in
@@ -126,7 +126,7 @@ for game in $gamelist; do
     *) ;;
   esac
   case $game in
-    glchess|gnomine|mahjongg) need_vala=yes ;;
+    glchess|gnomine|gnotravex|mahjongg) need_vala=yes ;;
     *) ;;
   esac
   case $game in
@@ -158,7 +158,7 @@ for game in $gamelist; do
     *) ;;
   esac
   case $game in
-    glines|gnobots2|gnotravex|gnotski|iagno) allow_smclient=yes ;;
+    glines|gnobots2|gnotski|iagno) allow_smclient=yes ;;
     *) ;;
   esac
   case $game in
diff --git a/gnotravex/data/Makefile.am b/gnotravex/data/Makefile.am
index 1faca71..2589d05 100644
--- a/gnotravex/data/Makefile.am
+++ b/gnotravex/data/Makefile.am
@@ -1,6 +1,3 @@
-imagedir = $(pkgdatadir)/pixmaps
-image_DATA = baize.png
-
 desktop_in_files = gnotravex.desktop.in.in
 desktopdir = $(datadir)/applications
 desktop_DATA = $(desktop_in_files:.desktop.in.in=.desktop)
@@ -14,7 +11,6 @@ gsettings_SCHEMAS = $(gsettings_in_file:.xml.in=.xml)
 man_MANS = gnotravex.6
 
 EXTRA_DIST = \
-	$(image_DATA) \
 	$(gsettings_in_file) \
 	$(man_MANS)
     
diff --git a/gnotravex/src/Makefile.am b/gnotravex/src/Makefile.am
index 8b938cf..f584e03 100644
--- a/gnotravex/src/Makefile.am
+++ b/gnotravex/src/Makefile.am
@@ -1,13 +1,26 @@
 bin_PROGRAMS = gnotravex
 
 gnotravex_SOURCES = \
-	gnotravex.c
-
-gnotravex_CPPFLAGS = \
-	-I$(top_srcdir) \
-	$(AM_CPPFLAGS)
+	config.vapi \
+	gnotravex.vala \
+	puzzle.vala \
+	puzzle-view.vala \
+	theme.vala
+
+gnotravex_VALAFLAGS = \
+	--pkg posix \
+	--pkg gtk+-3.0 \
+	--vapidir $(top_srcdir)/libgames-support \
+	--pkg GnomeGamesSupport-1.0
+
+if ENABLE_SETGID
+gnotravex_VALAFLAGS += -D ENABLE_SETGID
+endif
 
 gnotravex_CFLAGS = \
+	-I$(top_srcdir)/libgames-support \
+	-DVERSION=\"$(VERSION)\" \
+	-DGETTEXT_PACKAGE=\"$(GETTEXT_PACKAGE)\" \
 	$(GTK_CFLAGS)	\
 	$(AM_CFLAGS)
 
diff --git a/gnotravex/src/config.vapi b/gnotravex/src/config.vapi
new file mode 100644
index 0000000..6477226
--- /dev/null
+++ b/gnotravex/src/config.vapi
@@ -0,0 +1,2 @@
+public const string VERSION;
+public const string GETTEXT_PACKAGE;
\ No newline at end of file
diff --git a/gnotravex/src/gnotravex.vala b/gnotravex/src/gnotravex.vala
new file mode 100644
index 0000000..ec1c6a0
--- /dev/null
+++ b/gnotravex/src/gnotravex.vala
@@ -0,0 +1,413 @@
+public class Gnotravex : Gtk.Window
+{
+    private const int LONG_COUNT = 15;
+    private const int SHORT_COUNT = 5;
+    private const int DELAY = 10;
+
+    private const string KEY_GRID_SIZE = "grid-size";
+    private const string KEY_CLICK_MOVE = "click-to-move";
+
+    private Settings settings;
+
+    private Puzzle puzzle;
+    private GnomeGamesSupport.Clock clock;
+    private GnomeGamesSupport.Scores highscores;
+
+    private PuzzleView view;
+    private const GnomeGamesSupport.ScoresCategory scorecats[] =
+    {
+        { "2x2", N_("2Ã2") },
+        { "3x3", N_("3Ã3") },
+        { "4x4", N_("4Ã4") },
+        { "5x5", N_("5Ã5") },
+        { "6x6", N_("6Ã6") }
+    };
+
+    const Gtk.RadioActionEntry size_action_entry[] =
+    {
+        {"Size2x2", null, N_("_2Ã2"), null, N_("Play on a 2Ã2 board"), 2},
+        {"Size3x3", null, N_("_3Ã3"), null, N_("Play on a 3Ã3 board"), 3},
+        {"Size4x4", null, N_("_4Ã4"), null, N_("Play on a 4Ã4 board"), 4},
+        {"Size5x5", null, N_("_5Ã5"), null, N_("Play on a 5Ã5 board"), 5},
+        {"Size6x6", null, N_("_6Ã6"), null, N_("Play on a 6Ã6 board"), 6}
+    };
+
+    private Gtk.Action new_game_action;
+    private GnomeGamesSupport.PauseAction pause_action;
+    private Gtk.Action solve_action;
+    private Gtk.Action scores_action;
+    private Gtk.Action move_up_action;
+    private Gtk.Action move_left_action;
+    private Gtk.Action move_right_action;
+    private Gtk.Action move_down_action;
+
+    private const string ui_description =
+        "<ui>" +
+        "    <menubar name='MainMenu'>" +
+        "        <menu action='GameMenu'>" +
+        "            <menuitem action='NewGame'/>" +
+        "            <menuitem action='PauseGame'/>" +
+        "            <separator/>" +
+        "            <menu action='MoveMenu'>" +
+        "                <menuitem action='MoveUp'/>" +
+        "                <menuitem action='MoveLeft'/>" +
+        "                <menuitem action='MoveRight'/>" +
+        "                <menuitem action='MoveDown'/>" +
+        "            </menu>" +
+        "            <menuitem action='Solve'/>" +
+        "            <separator/>" +
+        "            <menuitem action='Scores'/>" +
+        "            <separator/>" +
+        "            <menuitem action='Quit'/>" +
+        "        </menu>" +
+        "        <menu action='SettingsMenu'>" +
+        "            <menuitem action='Fullscreen'/>" +
+        "            <menuitem action='ClickToMove'/>" +
+        "            <separator/>" +
+        "            <menuitem action='Size2x2'/>" +
+        "            <menuitem action='Size3x3'/>" +
+        "            <menuitem action='Size4x4'/>" +
+        "            <menuitem action='Size5x5'/>" +
+        "            <menuitem action='Size6x6'/>" +
+        "        </menu>" +
+        "        <menu action='HelpMenu'>" +
+        "            <menuitem action='Contents'/>" +
+        "            <menuitem action='About'/>" +
+        "        </menu>" +
+        "    </menubar>" +
+        "  <toolbar name='Toolbar'>" +
+        "    <toolitem action='NewGame'/>" +
+        "    <toolitem action='PauseGame'/>" +
+        "    <toolitem action='LeaveFullscreen'/>" +
+        "  </toolbar>" +
+        "</ui>";
+        
+    public Gnotravex ()
+    {
+        settings = new Settings ("org.gnome.gnotravex");
+
+        highscores = new GnomeGamesSupport.Scores ("gnotravex", scorecats, null, null, 0, GnomeGamesSupport.ScoreStyle.TIME_ASCENDING);
+
+        title = _("Tetravex");
+        GnomeGamesSupport.settings_bind_window_state ("/org/gnome/gnotravex/", this);
+
+        var ui_manager = new Gtk.UIManager ();
+        var action_group = new Gtk.ActionGroup ("actions");
+        action_group.set_translation_domain (GETTEXT_PACKAGE);
+        action_group.add_actions (action_entry, this);
+        action_group.add_radio_actions (size_action_entry, -1, size_cb);
+        action_group.add_toggle_actions (toggles, this);
+        ui_manager.insert_action_group (action_group, 0);
+
+        try
+        {
+            ui_manager.add_ui_from_string (ui_description, -1);
+        }
+        catch (Error e)
+        {
+            critical ("Failed to parse UI: %s", e.message);
+        }
+
+        new_game_action = action_group.get_action ("NewGame");
+        solve_action = action_group.get_action ("Solve");
+        scores_action = action_group.get_action ("Scores");
+        move_up_action = action_group.get_action ("MoveUp");
+        move_left_action = action_group.get_action ("MoveLeft");
+        move_right_action = action_group.get_action ("MoveRight");
+        move_down_action = action_group.get_action ("MoveDown");
+        pause_action = new GnomeGamesSupport.PauseAction ("PauseGame");
+        pause_action.is_important = true;
+        pause_action.state_changed.connect (pause_cb);
+        action_group.add_action_with_accel (pause_action, null);
+        var fullscreen_action = new GnomeGamesSupport.FullscreenAction ("Fullscreen", this);
+        action_group.add_action_with_accel (fullscreen_action, null);
+        var leave_fullscreen_action = new GnomeGamesSupport.FullscreenAction ("LeaveFullscreen", this);
+        action_group.add_action_with_accel (leave_fullscreen_action, null);
+        var action = (Gtk.ToggleAction) action_group.get_action ("ClickToMove");
+        action.active = settings.get_boolean (KEY_CLICK_MOVE);
+        var size = settings.get_int (KEY_GRID_SIZE);
+        if (size < 2 || size > 6)
+            size = 3;
+        var size_action = (Gtk.RadioAction) action_group.get_action (size_action_entry[size-2].name);
+        size_action.active = true;
+
+        var grid = new Gtk.Grid ();
+        grid.show ();
+        add (grid);
+        
+        var toolbar = ui_manager.get_widget ("/Toolbar");
+        toolbar.get_style_context ().add_class (Gtk.STYLE_CLASS_PRIMARY_TOOLBAR);
+        toolbar.show ();
+        grid.attach (toolbar, 0, 0, 1, 1);
+
+        var menubar = ui_manager.get_widget ("/MainMenu");
+        menubar.show ();
+        grid.attach (menubar, 0, 0, 1, 1);
+
+        view = new PuzzleView ();
+        view.hexpand = true;
+        view.vexpand = true;
+        view.click_to_move = settings.get_boolean (KEY_CLICK_MOVE);
+        view.button_press_event.connect (view_button_press_event);
+        view.show ();
+        grid.attach (view, 0, 1, 1, 1);
+
+        var statusbar = new Gtk.Statusbar ();
+        statusbar.show ();
+        GnomeGamesSupport.stock_prepare_for_statusbar_tooltips (ui_manager, statusbar);
+        grid.attach (statusbar, 0, 2, 1, 1);
+
+        var time_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0);
+        time_box.show ();
+        statusbar.pack_start (time_box, false, false, 0);
+
+        var time_label = new Gtk.Label (_("Time:"));
+        time_label.show ();
+        time_box.pack_start (time_label, false, false, 0);
+
+        var label = new Gtk.Label (" ");
+        label.show ();
+        time_box.pack_start (label, false, false, 0);
+
+        clock = new GnomeGamesSupport.Clock ();
+        clock.show ();
+        time_box.pack_start (clock, false, false, 0);
+
+        add_accel_group (ui_manager.get_accel_group ());
+
+        new_game ();
+    }
+
+    private void new_game ()
+    {
+        if (puzzle != null)
+            SignalHandler.disconnect_by_func (puzzle, null, this);
+
+        pause_action.sensitive = true;
+
+        var size = settings.get_int (KEY_GRID_SIZE);
+        if (size < 2 || size > 6)
+            size = 3;
+        highscores.set_category (scorecats[size - 2].key);
+        puzzle = new Puzzle (size);
+        puzzle.solved.connect (solved_cb);
+        view.puzzle = puzzle;
+        view.is_paused = false;
+
+        clock.reset ();
+        clock.start ();
+    }
+
+    private void solved_cb (Puzzle puzzle)
+    {
+        clock.stop ();
+
+        var seconds = clock.get_seconds ();
+        var pos = highscores.add_time_score ((seconds / 60) * 1.0 + (seconds % 60) / 100.0);
+
+        var scores_dialog = new GnomeGamesSupport.ScoresDialog (this, highscores, _("Tetravex Scores"));
+        scores_dialog.set_category_description (_("Size:"));
+        scores_dialog.set_hilight (pos);
+        scores_dialog.set_message ("<b>%s</b>\n\n%s".printf (_("Congratulations!"), pos == 1 ? _("Your score is the best!") : _("Your score has made the top ten.")));
+        scores_dialog.set_buttons (GnomeGamesSupport.ScoresButtons.QUIT_BUTTON | GnomeGamesSupport.ScoresButtons.NEW_GAME_BUTTON);
+        if (scores_dialog.run () == Gtk.ResponseType.REJECT)
+            Gtk.main_quit ();
+        else
+            new_game ();
+        scores_dialog.destroy ();
+    }
+
+    private void new_game_cb (Gtk.Action action)
+    {
+        new_game ();
+    }
+
+    private void quit_cb (Gtk.Action action)
+    {
+        Gtk.main_quit ();
+    }
+
+    private void scores_cb (Gtk.Action action)
+    {
+        var scores_dialog = new GnomeGamesSupport.ScoresDialog (this, highscores, _("Tetravex Scores"));
+        scores_dialog.set_category_description (_("Size:"));
+        scores_dialog.run ();
+        scores_dialog.destroy ();
+    }
+
+    private bool view_button_press_event (Gtk.Widget widget, Gdk.EventButton event)
+    {
+        /* Cancel pause on click */
+        if (view.is_paused)
+        {
+            toggle_pause ();
+            return true;
+        }
+
+        return false;
+    }
+
+    private void pause_cb (Gtk.Action action)
+    {
+        toggle_pause ();
+    }
+
+    private void toggle_pause ()
+    {
+        if (view.is_paused)
+        {
+            pause_action.set_is_paused (false);
+            solve_action.sensitive = true;
+            clock.start ();
+            view.is_paused = false;
+        }
+        else
+        {
+            pause_action.set_is_paused (true);
+            solve_action.sensitive = false;
+            clock.stop ();
+            view.is_paused = true;
+        }
+    }
+
+    private void solve_cb (Gtk.Action action)
+    {
+        puzzle.solve ();
+        clock.stop ();
+    }
+
+    private void help_cb (Gtk.Action action)
+    {
+        GnomeGamesSupport.help_display (this, "gnotravex", null);
+    }
+
+    private void about_cb (Gtk.Action action)
+    {
+        string[] authors = { "Lars Rydlinge", "Robert Ancell", null };
+        string[] documenters = { "Rob Bradford", null };
+        var license = GnomeGamesSupport.get_license (_("Tetravex"));
+        Gtk.show_about_dialog (this,
+                               "program-name", _("Tetravex"),
+                               "version", VERSION,
+                               "comments",
+                               _("GNOME Tetravex is a simple puzzle where pieces must be positioned so that the same numbers are touching each other.\n\nTetravex is a part of GNOME Games."),
+                               "copyright",
+                               "Copyright \xc2\xa9 1999-2008 Lars Rydlinge",
+                               "license", license,
+                               "wrap-license", true,
+                               "authors", authors,
+                               "documenters", documenters,
+                               "translator-credits", _("translator-credits"),
+                               "logo-icon-name", "gnome-tetravex",
+                               "website", "http://www.gnome.org/projects/gnome-games";,
+                               "website-label", _("GNOME Games web site"),
+                               null);
+    }
+
+    private void size_cb (Gtk.Action action)
+    {
+        var size = ((Gtk.RadioAction) action).get_current_value ();
+
+        if (size == settings.get_int (KEY_GRID_SIZE))
+            return;
+        settings.set_int (KEY_GRID_SIZE, size);
+        new_game ();
+    }
+
+    private void clickmove_toggle_cb (Gtk.Action action)
+    {
+        var click_to_move = ((Gtk.ToggleAction) action).active;
+
+        if (click_to_move == settings.get_boolean (KEY_CLICK_MOVE))
+            return;
+
+        settings.set_boolean (KEY_CLICK_MOVE, click_to_move);
+        view.click_to_move = click_to_move;
+    }
+
+    private void move_up_cb (Gtk.Action action)
+    {
+        puzzle.move_up ();
+    }
+
+    private void move_left_cb (Gtk.Action action)
+    {
+        puzzle.move_left ();
+    }
+
+    private void move_right_cb (Gtk.Action action)
+    {
+        puzzle.move_right ();
+    }
+
+    private void move_down_cb (Gtk.Action action)
+    {
+        puzzle.move_down ();
+    }
+   
+    private const Gtk.ActionEntry[] action_entry =
+    {
+        {"GameMenu", null, N_("_Game")},
+        {"MoveMenu", null, N_("_Move")},
+        {"SettingsMenu", null, N_("_Settings")},
+        {"SizeMenu", null, N_("_Size")},
+        {"HelpMenu", null, N_("_Help")},
+        {"NewGame", GnomeGamesSupport.STOCK_NEW_GAME, null, null, null, new_game_cb},
+        {"Solve", Gtk.Stock.REFRESH, N_("Sol_ve"), null, N_("Solve the game"), solve_cb},
+        {"Scores", GnomeGamesSupport.STOCK_SCORES, null, null, null, scores_cb},
+        {"Quit", Gtk.Stock.QUIT, null, null, null, quit_cb},
+        {"MoveUp", Gtk.Stock.GO_UP, N_("_Up"), "<control>Up",  N_("Move the pieces up"), move_up_cb},
+        {"MoveLeft", Gtk.Stock.GO_BACK, N_("_Left"), "<control>Left", N_("Move the pieces left"), move_left_cb},
+        {"MoveRight", Gtk.Stock.GO_FORWARD, N_("_Right"), "<control>Right", N_("Move the pieces right"), move_right_cb},
+        {"MoveDown", Gtk.Stock.GO_DOWN, N_("_Down"), "<control>Down",  N_("Move the pieces down"), move_down_cb},
+        {"Contents", GnomeGamesSupport.STOCK_CONTENTS, null, null, null, help_cb},
+        {"About", Gtk.Stock.ABOUT, null, null, null, about_cb}
+    };
+    private const Gtk.ToggleActionEntry toggles[] =
+    {
+        {"ClickToMove", null, N_("_Click to Move"), null, "Pick up and drop tiles by clicking", clickmove_toggle_cb}
+    };
+
+    public static int main (string[] args)
+    {
+        if (!GnomeGamesSupport.runtime_init ("gnotravex"))
+            return Posix.EXIT_FAILURE;
+
+#if ENABLE_SETGID
+        GnomeGamesSupport.setgid_io_init ();
+#endif
+
+        var context = new OptionContext ("");
+        context.set_translation_domain (GETTEXT_PACKAGE);
+        context.add_group (Gtk.get_option_group (true));
+        try
+        {
+            context.parse (ref args);
+        }
+        catch (Error e)
+        {
+            stderr.printf ("%s\n", e.message);
+            return Posix.EXIT_FAILURE;
+        }
+
+        Environment.set_application_name (_("Tetravex"));
+        GnomeGamesSupport.stock_init ();
+        Gtk.Window.set_default_icon_name ("gnome-tetravex");
+
+        var app = new Gnotravex ();
+        app.delete_event.connect (window_delete_event_cb);
+        app.show ();
+
+        Gtk.main ();
+
+        GnomeGamesSupport.runtime_shutdown ();
+
+        return Posix.EXIT_SUCCESS;
+    }
+
+    private static bool window_delete_event_cb (Gtk.Widget widget, Gdk.EventAny event)
+    {
+        Gtk.main_quit ();
+        return false;
+    }
+}
diff --git a/gnotravex/src/puzzle-view.vala b/gnotravex/src/puzzle-view.vala
new file mode 100644
index 0000000..4633180
--- /dev/null
+++ b/gnotravex/src/puzzle-view.vala
@@ -0,0 +1,450 @@
+private class TileImage
+{
+    /* Tile being moved */
+    public Tile tile;
+
+    /* Location of tile */
+    public double x;
+    public double y;
+
+    /* Co-ordinates to move from */
+    public double source_x;
+    public double source_y;
+
+    /* Time started moving */
+    public double source_time;
+
+    /* Co-ordinates to target for */
+    public double target_x;
+    public double target_y;
+
+    /* Duration of movement */
+    public double duration;
+
+    public TileImage (Tile tile)
+    {
+        this.tile = tile;
+    }
+}
+
+public class PuzzleView : Gtk.DrawingArea
+{
+    /* Minimum size of a tile */
+    private const int minimum_size = 40;
+
+    /* Puzzle being rendered */
+    private Puzzle? _puzzle = null;
+    public Puzzle puzzle
+    {
+        get { return _puzzle; }
+        set
+        {
+            if (_puzzle != null)
+                SignalHandler.disconnect_by_func (_puzzle, null, this);
+            _puzzle = value;
+            tiles.remove_all ();
+            for (var y = 0; y < puzzle.size; y++)
+            {
+                for (var x = 0; x < puzzle.size * 2; x++)
+                {
+                    var tile = puzzle.get_tile (x, y);
+                    if (tile == null)
+                        continue;
+
+                    var image = new TileImage (tile);
+                    move_tile_to_location (image, x, y);
+                    tiles.insert (tile, image);
+                }
+            }
+            _puzzle.tile_moved.connect (tile_moved_cb);
+            queue_resize ();
+        }
+    }
+    
+    /* Theme */
+    private Theme theme;
+
+    public bool click_to_move = false;
+
+    /* Tile being controlled by the mouse */
+    private TileImage? selected_tile = null;
+
+    /* The position inside the tile where the cursor is */
+    private double selected_x_offset;
+    private double selected_y_offset;
+
+    /* Tile images */
+    private HashTable<Tile, TileImage> tiles;
+
+    /* Animation timer */
+    private Timer animation_timer;
+    private uint animation_timeout = 0;
+
+    private bool _is_paused = false;
+    public bool is_paused
+    {
+        get { return _is_paused; }
+        set
+        {
+            _is_paused = value;
+            queue_draw ();
+        }
+    }
+
+    public PuzzleView ()
+    {
+        set_events (Gdk.EventMask.EXPOSURE_MASK | Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.POINTER_MOTION_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK);
+
+        tiles = new HashTable <Tile, TileImage> (direct_hash, direct_equal);
+        
+        theme = new Theme ();
+
+        animation_timer = new Timer ();
+        animation_timer.start ();
+    }
+    
+    private void redraw_tile (TileImage image)
+    {
+        uint x_offset, y_offset, size, gap;
+        get_dimensions (out x_offset, out y_offset, out size, out gap);
+
+        queue_draw_area ((int) (image.x + 0.5), (int) (image.y + 0.5), (int) size, (int) size);    
+    }
+
+    private void move_tile_to_location (TileImage image, uint x, uint y, double duration = 0)
+    {
+        uint x_offset, y_offset, size, gap;
+        get_dimensions (out x_offset, out y_offset, out size, out gap);
+
+        var target_x = x_offset + x * size;
+        if (x >= puzzle.size)
+            target_x += gap;
+        var target_y = y_offset + y * size;
+        move_tile (image, target_x, target_y, duration);
+    }
+
+    private void move_tile (TileImage image, double x, double y, double duration = 0)
+    {
+        if (image.x == x && image.y == y)
+            return;
+            
+        image.source_x = image.x;
+        image.source_y = image.y;
+        image.source_time = animation_timer.elapsed ();
+        image.target_x = x;
+        image.target_y = y;
+        image.duration = duration;
+
+        /* Move immediately */
+        if (duration == 0)
+        {
+            redraw_tile (image);
+            image.x = image.target_x;
+            image.y = image.target_y;
+            redraw_tile (image);
+            return;
+        }
+
+        /* Start animation (maximum of 100fps) */
+        if (animation_timeout == 0)
+            animation_timeout = Timeout.add (10, animate_cb);
+    }    
+    
+    private bool animate_cb ()
+    {
+        var t = animation_timer.elapsed ();
+        
+        uint x_offset, y_offset, size, gap;
+        get_dimensions (out x_offset, out y_offset, out size, out gap);
+
+        var animating = false;
+        var iter = HashTableIter<Tile, TileImage> (tiles);
+        while (true)
+        {
+            Tile tile;
+            TileImage image;
+            if (!iter.next (out tile, out image))
+                break;
+
+            if (image.x == image.target_x && image.y == image.target_y)
+                continue;
+
+            /* Redraw where the tile was */
+            redraw_tile (image);
+
+            /* Move the tile */
+            if (t >= image.source_time + image.duration)
+            {
+                image.x = image.target_x;
+                image.y = image.target_y;
+            }
+            else
+            {
+                var d = (t - image.source_time) / image.duration;
+                image.x = image.source_x + (image.target_x - image.source_x) * d;
+                image.y = image.source_y + (image.target_y - image.source_y) * d;
+                animating = true;
+            }
+
+            /* Draw where the tile is */
+            redraw_tile (image);
+        }
+
+        /* Keep animating if still have tiles */
+        if (animating)
+            return true;
+
+        animation_timeout = 0;
+        return false;
+    }
+
+    public override void get_preferred_width (out int minimum, out int natural)
+    {
+        var size = 0;
+        if (puzzle != null)
+            size = (int) ((puzzle.size * 2 + 1.5) * minimum_size);
+        minimum = natural = int.max (size, 500);
+    }
+
+    public override void get_preferred_height (out int minimum, out int natural)
+    {
+        var size = 0;
+        if (puzzle != null)
+            size = (int) ((puzzle.size + 1) * minimum_size);
+        minimum = natural = int.max (size, 300);
+    }
+    
+    private void get_dimensions (out uint x, out uint y, out uint size, out uint gap)
+    {
+        /* Fit in with a half tile border and spacing between boards */
+        var width = (uint) (get_allocated_width () / (2 * puzzle.size + 1.5));
+        var height = (uint) (get_allocated_height () / (puzzle.size + 1));
+        size = uint.min (width, height);
+        gap = size / 2;
+        x = (get_allocated_width () - 2 * puzzle.size * size - gap) / 2;
+        y = (get_allocated_height () - puzzle.size * size) / 2;
+    }
+
+    private void tile_moved_cb (Puzzle puzzle, Tile tile, uint x, uint y)
+    {
+        move_tile_to_location (tiles.lookup (tile), x, y, 0.2);
+    }
+    
+    public override bool configure_event (Gdk.EventConfigure event)
+    {
+        /* Move everything to its correct location */
+        var iter = HashTableIter<Tile, TileImage> (tiles);
+        while (true)
+        {
+            Tile tile;
+            TileImage image;
+            if (!iter.next (out tile, out image))
+                break;
+            uint x, y;
+            puzzle.get_tile_location (tile, out x, out y);
+            move_tile_to_location (image, x, y);
+        }
+        selected_tile = null;
+
+        return false;
+    }
+
+    public override bool draw (Cairo.Context context)
+    {
+        if (puzzle == null)
+            return false;
+
+        uint x_offset, y_offset, size, gap;
+        get_dimensions (out x_offset, out y_offset, out size, out gap);
+
+        /* Draw arrow */
+        context.save ();
+        var w = gap * 0.5;
+        var ax = x_offset + puzzle.size * size + (gap - w) * 0.5;
+        var ay = y_offset + puzzle.size * size * 0.5;
+        context.translate (ax, ay);
+        theme.draw_arrow (context, size, gap);
+        context.restore ();
+
+        /* Draw sockets */
+        for (var y = 0; y < puzzle.size; y++)
+        {
+            for (var x = 0; x < puzzle.size * 2; x++)
+            {
+                context.save ();
+                if (x >= puzzle.size)
+                    context.translate (x_offset + gap + x * size, y_offset + y * size);
+                else
+                    context.translate (x_offset + x * size, y_offset + y * size);
+                theme.draw_socket (context, size);
+                context.restore ();
+            }
+        }
+
+        /* Draw stationary tiles */
+        var iter = HashTableIter<Tile, TileImage> (tiles);
+        while (true)
+        {
+            Tile tile;
+            TileImage image;
+            if (!iter.next (out tile, out image))
+                break;
+
+            if (image == selected_tile || image.x != image.target_x || image.y != image.target_y)
+                continue;
+
+            context.save ();
+            context.translate ((int) (image.x + 0.5), (int) (image.y + 0.5));
+            if (is_paused)
+                theme.draw_paused_tile (context, size);
+            else
+                theme.draw_tile (context, size, tile);
+            context.restore ();
+        }
+
+        /* Draw moving tiles */
+        iter = HashTableIter<Tile, TileImage> (tiles);
+        while (true)
+        {
+            Tile tile;
+            TileImage image;
+            if (!iter.next (out tile, out image))
+                break;
+
+            if (image != selected_tile && image.x == image.target_x && image.y == image.target_y)
+                continue;
+
+            context.save ();
+            context.translate ((int) (image.x + 0.5), (int) (image.y + 0.5));
+            if (is_paused)
+                theme.draw_paused_tile (context, size);
+            else
+                theme.draw_tile (context, size, tile);
+            context.restore ();
+        }
+
+        /* Draw pause overlay */
+        if (is_paused)
+        {
+            context.set_source_rgba (0, 0, 0, 0.75);
+            context.paint ();
+
+            context.select_font_face ("Sans", Cairo.FontSlant.NORMAL, Cairo.FontWeight.BOLD);
+            context.set_font_size (get_allocated_width () * 0.125);
+
+            var text = _("Paused");
+            Cairo.TextExtents extents;
+            context.text_extents (text, out extents);
+            context.move_to ((get_allocated_width () - extents.width) / 2.0, (get_allocated_height () + extents.height) / 2.0);
+            context.set_source_rgb (1, 1, 1);
+            context.show_text (text);
+        }
+
+        return false;
+    }
+    
+    private void pick_tile (double x, double y)
+    {
+        if (selected_tile != null)
+            return;
+
+        if (puzzle.is_solved)
+            return;
+
+        uint x_offset, y_offset, size, gap;
+        get_dimensions (out x_offset, out y_offset, out size, out gap);
+
+        var iter = HashTableIter<Tile, TileImage> (tiles);
+        while (true)
+        {
+            Tile tile;
+            TileImage image;
+            if (!iter.next (out tile, out image))
+                break;
+
+            if (x >= image.x && x <= image.x + size && y >= image.y && y <= image.y + size)
+            {
+                selected_tile = image;
+                selected_x_offset = x - image.x;
+                selected_y_offset = y - image.y;
+            }
+        }
+    }
+
+    private void drop_tile (double x, double y)
+    {
+        if (selected_tile == null)
+            return;
+
+        uint x_offset, y_offset, size, gap;
+        get_dimensions (out x_offset, out y_offset, out size, out gap);
+
+        /* Select from the middle of the tile */
+        x += size * 0.5 - selected_x_offset;
+        y += size * 0.5 - selected_x_offset;
+
+        var tile_y = (int) Math.floor ((y - y_offset) / size);
+        tile_y = int.max (tile_y, 0);
+        tile_y = int.min (tile_y, (int) puzzle.size - 1);
+
+        /* Check which side we are on */
+        int tile_x;
+        if (x > x_offset + size * puzzle.size + gap * 0.5)
+        {
+            tile_x = (int) puzzle.size + (int) Math.floor ((x - (x_offset + puzzle.size * size + gap)) / size);
+            tile_x = int.max (tile_x, (int) puzzle.size);
+            tile_x = int.min (tile_x, 2 * (int) puzzle.size - 1);
+        }
+        else
+        {
+            tile_x = (int) Math.floor ((x - x_offset) / size);
+            tile_x = int.max (tile_x, 0);
+            tile_x = int.min (tile_x, (int) puzzle.size - 1);
+        }
+
+        /* Drop the tile here, or move it back if can't */
+        uint selected_x, selected_y;
+        puzzle.get_tile_location (selected_tile.tile, out selected_x, out selected_y);
+        if (puzzle.can_switch (selected_x, selected_y, (uint) tile_x, (uint) tile_y))
+            puzzle.switch_tiles (selected_x, selected_y, (uint) tile_x, (uint) tile_y);
+        else
+            move_tile_to_location (selected_tile, selected_x, selected_y, 0.2);
+        selected_tile = null;
+    }
+
+    public override bool button_press_event (Gdk.EventButton event)
+    {
+        if (is_paused)
+            return false;
+
+        /* Ignore double click events */
+        if (event.type != Gdk.EventType.BUTTON_PRESS)
+            return false;
+
+        if (event.button == 1)
+        {
+            if (selected_tile == null)
+                pick_tile (event.x, event.y);
+            else if (click_to_move)
+                drop_tile (event.x, event.y);
+        }
+
+        return false;
+    }
+
+    public override bool button_release_event (Gdk.EventButton event)
+    {
+        if (event.button == 1 && selected_tile != null && !click_to_move)
+            drop_tile (event.x, event.y);
+            
+        return false;
+    }
+
+    public override bool motion_notify_event (Gdk.EventMotion event)
+    {
+        if (selected_tile != null)
+            move_tile (selected_tile, (int) (event.x - selected_x_offset), (int) (event.y - selected_y_offset));
+
+        return false;
+    }
+}
diff --git a/gnotravex/src/puzzle.vala b/gnotravex/src/puzzle.vala
new file mode 100644
index 0000000..b5ebb4d
--- /dev/null
+++ b/gnotravex/src/puzzle.vala
@@ -0,0 +1,280 @@
+public class Tile
+{
+    /* Edge colors */
+    public int north;
+    public int west;
+    public int east;
+    public int south;
+
+    /* Solution location */
+    public uint x;
+    public uint y;
+
+    public Tile (uint x, uint y)
+    {
+        this.x = x;
+        this.y = y;
+    }
+}
+
+public class Puzzle
+{
+    private uint _size;
+    public uint size
+    {
+        get { return _size; }
+    }
+    private Tile[,] board;
+    
+    public signal void tile_moved (Tile tile, uint x, uint y);    
+    public signal void solved ();
+    
+    public bool is_solved
+    {
+        get
+        {
+            /* Solved if entire left hand side is complete (we ensure only tiles
+               that fit are allowed */
+            for (var x = 0; x < size; x++)
+            {
+                for (var y = 0; y < size; y++)
+                {
+                    var tile = board[x, y];
+                    if (tile == null)
+                        return false;
+                }
+            }
+
+            return true;
+        }
+    }
+
+    public Puzzle (uint size)
+    {
+        _size = size;
+        board = new Tile[size * 2, size];
+        for (var x = 0; x < size; x++)
+            for (var y = 0; y < size; y++)
+                board[x, y] = new Tile (x, y);
+
+        /* Pick random colours for edges */
+        for (var x = 0; x < size; x++)
+        {
+            for (var y = 0; y <= size; y++)
+            {
+                var n = Random.int_range (0, 10);
+                if (y - 1 >= 0)
+                    board[x, y - 1].south = n;
+                if (y < size)
+                    board[x, y].north = n;
+            }
+        }
+        for (var x = 0; x <= size; x++)
+        {
+            for (var y = 0; y < size; y++)
+            {
+                var n = Random.int_range (0, 10);
+                if (x - 1 >= 0)
+                    board[x - 1, y].east = n;
+                if (x < size)
+                    board[x, y].west = n;
+            }
+        }
+
+        /* Pick up the tiles... */
+        List<Tile> tiles = null;
+        for (var x = 0; x < size; x++)
+        {
+            for (var y = 0; y < size; y++)
+            {
+                tiles.append (board[x, y]);
+                board[x, y] = null;
+            }
+        }
+
+        /* ...and place then randomly on the right hand side */
+        for (var x = 0; x < size; x++)
+        {
+            for (var y = 0; y < size; y++)
+            {
+                var n = Random.int_range (0, (int32) tiles.length ());
+                var tile = tiles.nth_data (n);
+                board[x + size, y] = tile;
+                tiles.remove (tile);
+            }
+        }
+    }
+
+    public Tile? get_tile (uint x, uint y)
+    {
+        return board[x, y];
+    }
+    
+    public void get_tile_location (Tile tile, out uint x, out uint y)
+    {
+        x = y = 0;
+        for (x = 0; x < size * 2; x++)
+            for (y = 0; y < size; y++)
+                if (board[x, y] == tile)
+                    return;
+    }
+
+    public bool tile_fits (uint x0, uint y0, uint x1, uint y1)
+    {
+        var tile = board[x0, y0];
+        if (tile == null)
+            return false;
+
+        if (x1 > 0 && !(x1 - 1 == x0 && y1 == y0) && board[x1 - 1, y1] != null && board[x1 - 1, y1].east != tile.west)
+            return false;
+        if (x1 < size - 1 && !(x1 + 1 == x0 && y1 == y0) && board[x1 + 1, y1] != null && board[x1 + 1, y1].west != tile.east)
+            return false;
+        if (y1 > 0 && !(x1 == x0 && y1 - 1 == y0) && board[x1, y1 - 1] != null && board[x1, y1 - 1].south != tile.north)
+            return false;
+        if (y1 < size - 1 && !(x1 == x0 && y1 + 1 == y0) && board[x1, y1 + 1] != null && board[x1, y1 + 1].north != tile.south)
+            return false;
+
+        return true;
+    }
+    
+    public bool can_switch (uint x0, uint y0, uint x1, uint y1)
+    {
+        if (x0 == x1 && y0 == y1)
+            return false;
+
+        var t0 = board[x0, y0];
+        var t1 = board[x1, y1];
+
+        /* No tiles to switch */
+        if (t0 == null && t1 == null)
+            return false;
+
+        /* If placing onto the final area check if it fits */
+        if (t0 != null && x1 < size && !tile_fits (x0, y0, x1, y1))
+            return false;
+        if (t1 != null && x0 < size && !tile_fits (x1, y1, x0, y0))
+            return false;
+
+        return true;
+    }
+
+    public void switch_tiles (uint x0, uint y0, uint x1, uint y1)
+    {
+        if (x0 == x1 && y0 == y1)
+            return;            
+
+        var t0 = board[x0, y0];
+        var t1 = board[x1, y1];
+        board[x0, y0] = t1;
+        board[x1, y1] = t0;
+
+        if (t0 != null)
+            tile_moved (t0, x1, y1);
+        if (t1 != null)
+            tile_moved (t1, x0, y0);
+
+        if (is_solved)
+            solved ();
+    }
+
+    public bool can_move_up
+    {
+        get
+        {
+            for (var x = 0; x < size; x++)
+                if (board[x, 0] != null)
+                    return false;
+            return true;
+        }
+    }
+
+    public void move_up ()
+    {
+        if (!can_move_up)
+            return;
+        for (var y = 1; y < size; y++)
+            for (var x = 0; x < size; x++)
+                switch_tiles (x, y, x, y - 1);
+    }
+
+    public bool can_move_down
+    {
+        get
+        {
+            for (var x = 0; x < size; x++)
+                if (board[x, size - 1] != null)
+                    return false;
+            return true;
+        }
+    }
+
+    public void move_down ()
+    {
+        if (!can_move_down)
+            return;
+        for (var y = (int) size - 2; y >= 0; y--)
+            for (var x = 0; x < size; x++)
+                switch_tiles (x, y, x, y + 1);
+    }
+
+    public bool can_move_left
+    {
+        get
+        {
+            for (var y = 0; y < size; y++)
+                if (board[0, y] != null)
+                    return false;
+            return true;
+        }
+    }
+
+    public void move_left ()
+    {
+        if (!can_move_left)
+            return;
+        for (var x = 1; x < size; x++)
+            for (var y = 0; y < size; y++)
+                switch_tiles (x, y, x - 1, y);
+    }
+
+    public bool can_move_right
+    {
+        get
+        {
+            for (var y = 0; y < size; y++)
+                if (board[size - 1, y] != null)
+                    return false;
+            return true;
+        }
+    }
+
+    public void move_right ()
+    {
+        if (!can_move_right)
+            return;
+        for (var x = (int) size - 2; x >= 0; x--)
+            for (var y = 0; y < size; y++)
+                switch_tiles (x, y, x + 1, y);
+    }
+
+    public void solve ()
+    {
+        List<Tile> wrong_tiles = null;
+        for (var x = 0; x < size * 2; x++)
+        {
+            for (var y = 0; y < size; y++)
+            {
+                var tile = board[x, y];
+                if (tile != null && (tile.x != x || tile.y != y))
+                    wrong_tiles.append (tile);
+                board[x, y] = null;
+            }
+        }
+
+        foreach (var tile in wrong_tiles)
+        {
+            board[tile.x, tile.y] = tile;
+            tile_moved (tile, tile.x, tile.y);
+        }
+    }
+}
diff --git a/gnotravex/src/theme.vala b/gnotravex/src/theme.vala
new file mode 100644
index 0000000..75a9410
--- /dev/null
+++ b/gnotravex/src/theme.vala
@@ -0,0 +1,272 @@
+public class Theme
+{
+    /* Colors of tiles and text */
+    private Cairo.Pattern tile_colors[10];
+    private Cairo.Pattern paused_color;
+    private Cairo.Pattern text_colors[10];
+
+    public Theme ()
+    {
+        tile_colors[0] = make_color_pattern ("#000000");
+        tile_colors[1] = make_color_pattern ("#C17D11");
+        tile_colors[2] = make_color_pattern ("#CC0000");
+        tile_colors[3] = make_color_pattern ("#F57900");
+        tile_colors[4] = make_color_pattern ("#EDD400");
+        tile_colors[5] = make_color_pattern ("#73D216");
+        tile_colors[6] = make_color_pattern ("#3465A4");
+        tile_colors[7] = make_color_pattern ("#75507B");
+        tile_colors[8] = make_color_pattern ("#BABDB6");
+        tile_colors[9] = make_color_pattern ("#FFFFFF");
+        
+        paused_color = make_color_pattern ("#CCCCCC");
+
+        text_colors[0] = new Cairo.Pattern.rgb (1, 1, 1);
+        text_colors[1] = new Cairo.Pattern.rgb (1, 1, 1);
+        text_colors[2] = new Cairo.Pattern.rgb (1, 1, 1);
+        text_colors[3] = new Cairo.Pattern.rgb (1, 1, 1);
+        text_colors[4] = new Cairo.Pattern.rgb (0, 0, 0);
+        text_colors[5] = new Cairo.Pattern.rgb (0, 0, 0);
+        text_colors[6] = new Cairo.Pattern.rgb (1, 1, 1);
+        text_colors[7] = new Cairo.Pattern.rgb (1, 1, 1);
+        text_colors[8] = new Cairo.Pattern.rgb (0, 0, 0);
+        text_colors[9] = new Cairo.Pattern.rgb (0, 0, 0);
+    }
+
+    private Cairo.Pattern make_color_pattern (string color)
+    {
+        var r = (hex_value (color[1]) * 16 + hex_value (color[2])) / 255.0;
+        var g = (hex_value (color[3]) * 16 + hex_value (color[4])) / 255.0;
+        var b = (hex_value (color[5]) * 16 + hex_value (color[6])) / 255.0;
+        return new Cairo.Pattern.rgb (r, g, b);
+    }
+
+    private double hex_value (char c)
+    {
+        if (c >= '0' && c <= '9')
+            return c - '0';
+        else if (c >= 'a' && c <= 'f')
+            return c - 'a' + 10;
+        else if (c >= 'A' && c <= 'F')
+            return c - 'A' + 10;
+        else
+            return 0;
+    }
+
+    public void draw_arrow (Cairo.Context context, uint size, uint gap)
+    {
+        var w = gap * 0.5;
+        var h = size * 1.5;
+        var depth = uint.min ((uint) (size * 0.025), 2);
+        var dx = 1.4142 * depth;
+        var dy = 6.1623 * depth;
+        
+        /* Background */
+        context.move_to (0, 0);
+        context.line_to (w, h * 0.5);
+        context.line_to (w, -h * 0.5);
+        context.close_path ();
+        context.set_source_rgba (0, 0, 0, 0.125);
+        context.fill ();
+
+        /* Arrow highlight */
+        context.move_to (w, -h * 0.5);
+        context.line_to (w, h * 0.5);
+        context.line_to (w - depth, h * 0.5 - dy);
+        context.line_to (w - depth, -h * 0.5 + dy);
+        context.close_path ();
+        context.set_source_rgba (1, 1, 1, 0.125);
+        context.fill ();
+
+        /* Arrow shadow */
+        context.move_to (w, -h * 0.5);
+        context.line_to (0, 0);
+        context.line_to (w, h * 0.5);
+        context.line_to (w - depth, h * 0.5 - dy);
+        context.line_to (dx, 0);
+        context.line_to (w - depth, -h * 0.5 + dy);
+        context.close_path ();
+        context.set_source_rgba (0, 0, 0, 0.25);
+        context.fill ();   
+    }
+
+    public void draw_socket (Cairo.Context context, uint size)
+    {
+        var depth = uint.min ((uint) (size * 0.05), 4);
+
+        /* Background */
+        context.rectangle (depth, depth, size - depth * 2, size - depth * 2);
+        context.set_source_rgba (0, 0, 0, 0.125);
+        context.fill ();
+
+        /* Shadow */
+        context.move_to (size, 0);
+        context.line_to (0, 0);
+        context.line_to (0, size);
+        context.line_to (depth, size - depth);
+        context.line_to (depth, depth);
+        context.line_to (size - depth, depth);
+        context.close_path ();
+        context.set_source_rgba (0, 0, 0, 0.25);
+        context.fill ();
+
+        /* Highlight */
+        context.move_to (0, size);
+        context.line_to (size, size);
+        context.line_to (size, 0);
+        context.line_to (size - depth, depth);
+        context.line_to (size - depth, size - depth);
+        context.line_to (depth, size - depth);
+        context.close_path ();
+        context.set_source_rgba (1, 1, 1, 0.125);
+        context.fill ();
+    }
+    
+    public void draw_paused_tile (Cairo.Context context, uint size)
+    {
+        draw_tile_background (context, size, paused_color, paused_color, paused_color, paused_color);
+    }
+
+    public void draw_tile (Cairo.Context context, uint size, Tile? tile)
+    {
+        draw_tile_background (context, size, tile_colors[tile.north], tile_colors[tile.east], tile_colors[tile.south], tile_colors[tile.west]);
+
+        context.select_font_face ("Sans", Cairo.FontSlant.NORMAL, Cairo.FontWeight.BOLD);
+        context.set_font_size (size / 3.5);
+        context.set_source (text_colors[tile.north]);
+        draw_number (context, size * 0.5, size / 5, tile.north);
+        context.set_source (text_colors[tile.south]);
+        draw_number (context, size * 0.5, size * 4 / 5, tile.south);
+        context.set_source (text_colors[tile.east]);
+        draw_number (context, size * 4 / 5, size * 0.5, tile.east);
+        context.set_source (text_colors[tile.west]);
+        draw_number (context, size / 5, size * 0.5, tile.west);
+    }
+
+    private void draw_tile_background (Cairo.Context context, uint size, Cairo.Pattern north_color, Cairo.Pattern east_color, Cairo.Pattern south_color, Cairo.Pattern west_color)
+    {
+        var depth = uint.min ((uint) (size * 0.05), 4);
+        var dx = 2.4142 * depth;
+        var dy = 1.4142 * depth;
+
+        /* North */
+        context.rectangle (0, 0, size, size * 0.5);
+        context.set_source (north_color);
+        context.fill ();
+
+        /* North highlight */
+        context.move_to (0, 0);
+        context.line_to (size, 0);
+        context.line_to (size - dx, depth);
+        context.line_to (dx, depth);
+        context.line_to (size * 0.5, size * 0.5 - dy);
+        context.line_to (size * 0.5, size * 0.5);
+        context.close_path ();
+        context.set_source_rgba (1, 1, 1, 0.125);
+        context.fill ();
+
+        /* North shadow */
+        context.move_to (size, 0);
+        context.line_to (size * 0.5, size * 0.5);
+        context.line_to (size * 0.5, size * 0.5 - dy);
+        context.line_to (size - dx, depth);
+        context.close_path ();
+        context.set_source_rgba (0, 0, 0, 0.25);
+        context.fill ();
+
+        /* South */
+        context.rectangle (0, size * 0.5, size, size * 0.5);
+        context.set_source (south_color);
+        context.fill ();
+
+        /* South highlight */
+        context.move_to (0, size);
+        context.line_to (dx, size - depth);
+        context.line_to (size * 0.5, size * 0.5 + dy);
+        context.line_to (size * 0.5, size * 0.5);
+        context.close_path ();
+        context.set_source_rgba (1, 1, 1, 0.125);
+        context.fill ();
+
+        /* South shadow */
+        context.move_to (0, size);
+        context.line_to (size, size);
+        context.line_to (size * 0.5, size * 0.5);
+        context.line_to (size * 0.5, size * 0.5 + dy);
+        context.line_to (size - dx, size - depth);
+        context.line_to (dx, size - depth);
+        context.close_path ();
+        context.set_source_rgba (0, 0, 0, 0.25);
+        context.fill ();
+
+        /* East */
+        context.move_to (size, 0);
+        context.line_to (size, size);
+        context.line_to (size * 0.5, size * 0.5);
+        context.close_path ();
+        context.set_source (east_color);
+        context.fill ();
+
+        /* East highlight */
+        context.move_to (size, 0);
+        context.line_to (size * 0.5, size * 0.5);
+        context.line_to (size, size);
+        context.line_to (size - depth, size - dx);
+        context.line_to (size * 0.5 + dy, size * 0.5);
+        context.line_to (size - depth, dx);
+        context.close_path ();
+        context.set_source_rgba (1, 1, 1, 0.125);
+        context.fill ();
+
+        /* East shadow */
+        context.move_to (size, 0);
+        context.line_to (size, size);
+        context.line_to (size - depth, size - dx);
+        context.line_to (size - depth, dx);
+        context.close_path ();
+        context.set_source_rgba (0, 0, 0, 0.25);
+        context.fill ();
+
+        /* West */
+        context.move_to (0, 0);
+        context.line_to (0, size);
+        context.line_to (size * 0.5, size * 0.5);
+        context.close_path ();
+        context.set_source (west_color);
+        context.fill ();
+
+        /* West highlight */
+        context.move_to (0, 0);
+        context.line_to (0, size);
+        context.line_to (depth, size - dx);
+        context.line_to (depth, dx);
+        context.close_path ();
+        context.set_source_rgba (1, 1, 1, 0.125);
+        context.fill ();
+
+        /* West shadow */
+        context.move_to (0, 0);
+        context.line_to (size * 0.5, size * 0.5);
+        context.line_to (0, size);
+        context.line_to (depth, size - dx);
+        context.line_to (size * 0.5 - dy, size * 0.5);
+        context.line_to (depth, dx);
+        context.close_path ();
+        context.set_source_rgba (0, 0, 0, 0.25);
+        context.fill ();
+
+        /* Draw outline */
+        context.set_line_width (1.0);
+        context.set_source_rgb (0.0, 0.0, 0.0);
+        context.rectangle (0.5, 0.5, size - 1.0, size - 1.0);
+        context.stroke ();
+    }
+
+    private void draw_number (Cairo.Context context, double x, double y, uint number)
+    {
+        var text = "%u".printf (number);
+        Cairo.TextExtents extents;
+        context.text_extents (text, out extents);
+        context.move_to (x - extents.width / 2.0, y + extents.height / 2.0);
+        context.show_text (text);
+    }
+}
\ No newline at end of file



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]