[gnome-games] Revert "aisleriot: Remove sol/clutter from 3.0 branch"



commit 2f5bb0b9c815fbe88f6e70527ab6d299a9345217
Author: Christian Persch <chpe gnome org>
Date:   Sat Apr 2 21:56:31 2011 +0200

    Revert "aisleriot: Remove sol/clutter from 3.0 branch"
    
    This reverts commit d0ce382f6016986b6030be2e7f09d8c1f5274439.

 aisleriot/Makefile.am                  |   65 +
 aisleriot/ar-clutter-embed.c           |  271 +++
 aisleriot/ar-clutter-embed.h           |   61 +
 aisleriot/ar-style-gtk.c               |   19 +
 aisleriot/ar-style-private.h           |   12 +
 aisleriot/ar-style.c                   |  103 +
 aisleriot/ar-style.h                   |   15 +
 aisleriot/baize.c                      |   91 +
 aisleriot/baize.h                      |   65 +
 aisleriot/board.c                      | 3536 ++++++++++++++++++++++++++++++++
 aisleriot/board.h                      |   99 +
 aisleriot/card.c                       |  364 ++++
 aisleriot/card.h                       |   79 +
 aisleriot/game.c                       |   55 +
 aisleriot/game.h                       |   14 +
 aisleriot/lib/Makefile.am              |   12 +
 aisleriot/lib/ar-card-textures-cache.c |  374 ++++
 aisleriot/lib/ar-card-textures-cache.h |   74 +
 aisleriot/prop-editor.c                |   95 +
 aisleriot/slot-renderer.c              |  691 +++++++
 aisleriot/slot-renderer.h              |  101 +
 aisleriot/sol.c                        |   28 +
 aisleriot/window.c                     |  156 ++-
 configure.in                           |   19 +-
 po/POTFILES.in                         |    2 +
 25 files changed, 6396 insertions(+), 5 deletions(-)
---
diff --git a/aisleriot/Makefile.am b/aisleriot/Makefile.am
index 8fc8f0d..c5047aa 100644
--- a/aisleriot/Makefile.am
+++ b/aisleriot/Makefile.am
@@ -110,6 +110,71 @@ if PLATFORM_WIN32_NATIVE
 sol_LDFLAGS += -mwindows
 endif
 
+if HAVE_CLUTTER
+if ENABLE_AISLERIOT_CLUTTER
+
+noinst_PROGRAMS = sol-clutter
+
+sol_clutter_SOURCES = \
+	ar-clutter-embed.c \
+	ar-clutter-embed.h \
+	ar-cursor.c	\
+	ar-cursor.h	\
+	ar-game-chooser.c \
+	ar-game-chooser.h \
+	ar-style.c	\
+	ar-style.h	\
+	ar-style-private.h \
+	ar-style-gtk.c	\
+	ar-style-gtk.h	\
+	baize.c		\
+	baize.h		\
+	board.c		\
+	board.h		\
+	card.c          \
+	card.h          \
+	conf.c		\
+	conf.h		\
+	game.c		\
+	game.h		\
+	sol.c		\
+	slot-renderer.c	\
+	slot-renderer.h \
+	stats-dialog.c	\
+	stats-dialog.h	\
+	util.c		\
+	util.h		\
+	window.c	\
+	window.h	\
+	$(NULL)
+
+if HAVE_MAEMO_5
+sol_clutter_SOURCES += \
+	ar-fullscreen-button.c \
+	ar-fullscreen-button.h \
+	$(NULL)
+endif
+
+if !HAVE_GUILE_1_8
+sol_clutter_SOURCES += guile16-compat.h
+endif
+		
+if ENABLE_DEBUG_UI
+sol_clutter_SOURCES += \
+	prop-editor.c	\
+	prop-editor.h	\
+	$(NULL)
+endif
+
+sol_clutter_CPPFLAGS = $(sol_CPPFLAGS) -DHAVE_CLUTTER
+sol_clutter_CFLAGS = $(sol_CFLAGS) $(CLUTTER_GTK_CFLAGS) $(CLUTTER_CFLAGS)
+sol_clutter_LDFLAGS = $(sol_LDFLAGS)
+sol_clutter_LDADD = $(sol_LDADD) $(CLUTTER_GTK_LIBS) $(CLUTTER_LIBS)
+
+
+endif # ENABLE_AISLERIOT_CLUTTER
+endif # HAVE_CLUTTER
+
 desktop_in_files = \
 	sol.desktop.in.in
 
diff --git a/aisleriot/ar-clutter-embed.c b/aisleriot/ar-clutter-embed.c
new file mode 100644
index 0000000..e09ee9b
--- /dev/null
+++ b/aisleriot/ar-clutter-embed.c
@@ -0,0 +1,271 @@
+/*
+ * Copyright © 1998, 2003 Jonathan Blandford <jrb mit edu>
+ * Copyright © 2007, 2008, 2009, 2010 Christian Persch
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <config.h>
+
+#include "ar-clutter-embed.h"
+
+#include "ar-style.h"
+#include "ar-style-gtk.h"
+
+/**
+ * SECTION: ar-clutter-embed
+ * @short_description: a #GtkClutterEmbed derivative
+ *
+ * #ArClutterEmbed is a #GtkClutterEmbed derivative that syncs the
+ * properties of a #ArStyle to its style properties and the #GtkSettings
+ * properties of its #GdkScreen.
+ */
+
+G_DEFINE_TYPE (ArClutterEmbed, ar_clutter_embed, GTK_CLUTTER_TYPE_EMBED)
+
+enum
+{
+  PROP_0,
+  PROP_STYLE
+};
+
+struct _ArClutterEmbedPrivate
+{
+  ArStyle *style;
+
+  GdkCursor *cursor[AR_LAST_CURSOR];
+};
+
+/* GtkWidgetClass impl */
+
+static void
+ar_clutter_embed_realize (GtkWidget *widget)
+{
+  ArClutterEmbed *embed = AR_CLUTTER_EMBED (widget);
+  ArClutterEmbedPrivate *priv = embed->priv;
+#ifndef HAVE_HILDON
+  GdkDisplay *display;
+  GdkWindow *window;
+#endif
+
+  GTK_WIDGET_CLASS (ar_clutter_embed_parent_class)->realize (widget);
+
+  /* FIXMEchpe: this isn't really HILDON, but don't-support-mouse */
+#ifndef HAVE_HILDON
+  /* Create cursors */
+  display = gtk_widget_get_display (widget);
+  window = gtk_widget_get_window (widget);
+
+  priv->cursor[AR_CURSOR_DEFAULT] = gdk_cursor_new_for_display (display, GDK_LEFT_PTR);
+  priv->cursor[AR_CURSOR_OPEN] = ar_cursor_new (window, AR_CURSOR_OPEN);
+  priv->cursor[AR_CURSOR_CLOSED] = ar_cursor_new (window, AR_CURSOR_CLOSED);
+  priv->cursor[AR_CURSOR_DROPPABLE] = gdk_cursor_new_for_display (display, GDK_DOUBLE_ARROW); /* FIXMEchpe: better cursor */
+#endif /* !HAVE_HILDON */
+
+  ar_clutter_embed_set_cursor (embed, AR_CURSOR_DEFAULT);
+}
+
+static void
+ar_clutter_embed_unrealize (GtkWidget *widget)
+{
+  /* FIXMEchpe */
+#ifndef HAVE_HILDON
+  ArClutterEmbed *embed = AR_CLUTTER_EMBED (widget);
+  ArClutterEmbedPrivate *priv = embed->priv;
+  guint i;
+
+  for (i = 0; i < AR_LAST_CURSOR; ++i) {
+    gdk_cursor_unref (priv->cursor[i]);
+    priv->cursor[i] = NULL;
+  }
+#endif /* !HAVE_HILDON*/
+
+  GTK_WIDGET_CLASS (ar_clutter_embed_parent_class)->unrealize (widget);
+}
+
+static gboolean
+ar_clutter_embed_focus_in (GtkWidget *widget,
+                           GdkEventFocus *event)
+{
+  gboolean retval;
+
+  retval = GTK_WIDGET_CLASS (ar_clutter_embed_parent_class)->focus_in_event (widget, event);
+
+#if 0
+  ClutterActor *stage;
+  stage = gtk_clutter_embed_get_stage (GTK_CLUTTER_EMBED (widget));
+  clutter_stage_set_key_focus (CLUTTER_STAGE (stage), FIXME board actor);
+#endif
+
+  return retval;
+}
+
+static gboolean
+ar_clutter_embed_focus_out (GtkWidget *widget,
+                            GdkEventFocus *event)
+{
+#ifdef FIXMEchpe
+  clear_state (board);
+#endif
+
+  return GTK_WIDGET_CLASS (ar_clutter_embed_parent_class)->focus_out_event (widget, event);
+}
+
+static gboolean
+ar_clutter_embed_focus (GtkWidget *widget,
+                        GtkDirectionType direction)
+{
+//   ArClutterEmbed *embed = AR_CLUTTER_EMBED (embed);
+//   ArClutterEmbedPrivate *priv = embed->priv;
+  int count;
+  gboolean retval = FALSE;
+
+  switch (direction) {
+    case GTK_DIR_TAB_FORWARD:
+      count = 1;
+      break;
+    case GTK_DIR_TAB_BACKWARD:
+      count = -1;
+      break;
+    default:
+      break;
+  }
+
+#ifdef FIXMEchpe
+  g_signal_emit_by_name (priv->board_actor, "focus", count, &retval);
+#endif
+
+  if (retval)
+    return TRUE;
+
+  return GTK_WIDGET_CLASS (ar_clutter_embed_parent_class)->focus (widget, direction);
+}
+
+/* GObjectClass impl */
+
+static void
+ar_clutter_embed_init (ArClutterEmbed *embed)
+{
+  GtkWidget *widget = GTK_WIDGET (embed);
+
+  embed->priv = G_TYPE_INSTANCE_GET_PRIVATE (embed, AR_TYPE_CLUTTER_EMBED, ArClutterEmbedPrivate);
+
+  gtk_widget_set_can_focus (widget, TRUE);
+}
+
+static void
+ar_clutter_embed_dispose (GObject *object)
+{
+  ArClutterEmbed *embed = AR_CLUTTER_EMBED (object);
+  ArClutterEmbedPrivate *priv = embed->priv;
+
+  if (priv->style != NULL) {
+    _ar_style_gtk_detach (priv->style, GTK_WIDGET (embed));
+
+    g_object_unref (priv->style);
+    priv->style = NULL;
+  }
+
+  G_OBJECT_CLASS (ar_clutter_embed_parent_class)->dispose (object);
+}
+
+static void
+ar_clutter_embed_set_property (GObject      *object,
+                               guint         property_id,
+                               const GValue *value,
+                               GParamSpec   *pspec)
+{
+  ArClutterEmbed *embed = AR_CLUTTER_EMBED (object);
+  ArClutterEmbedPrivate *priv = embed->priv;
+
+  switch (property_id) {
+    case PROP_STYLE:
+      priv->style = g_value_dup_object (value);
+      _ar_style_gtk_attach (priv->style, GTK_WIDGET (embed));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+  }
+}
+
+static void
+ar_clutter_embed_class_init (ArClutterEmbedClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  g_type_class_add_private (klass, sizeof (ArClutterEmbedPrivate));
+
+  object_class->set_property = ar_clutter_embed_set_property;
+  object_class->dispose = ar_clutter_embed_dispose;
+
+  widget_class->realize = ar_clutter_embed_realize;
+  widget_class->unrealize = ar_clutter_embed_unrealize;
+  widget_class->focus_in_event = ar_clutter_embed_focus_in;
+  widget_class->focus_out_event = ar_clutter_embed_focus_out;
+  widget_class->focus = ar_clutter_embed_focus;
+
+  /**
+   * ArClutterEmbed:style:
+   *
+   * An #ArStyle that @embed will update with its widget style properties.
+   */
+  g_object_class_install_property
+    (object_class,
+     PROP_STYLE,
+     g_param_spec_object ("style", NULL, NULL,
+                          AR_TYPE_STYLE,
+                          G_PARAM_WRITABLE |
+                          G_PARAM_CONSTRUCT_ONLY |
+                          G_PARAM_STATIC_STRINGS));
+
+  _ar_style_gtk_class_install_style_properties (widget_class);
+}
+
+/* public API */
+
+/**
+ * ar_clutter_embed_new:
+ * @style: an #ArStyle
+ *
+ * Returns: a new #ArClutterEmbed
+ */
+ArClutterEmbed *
+ar_clutter_embed_new (ArStyle *style)
+{
+  return g_object_new (AR_TYPE_CLUTTER_EMBED,
+                       "style", style,
+                       NULL);
+}
+
+/**
+ * ar_clutter_embed_set_cursor:
+ * @embed: an #ArClutterEmbed
+ * @cursor_type: the cursor type
+ *
+ * Sets the cursor on @embed to @cursor_type.
+ */
+void
+ar_clutter_embed_set_cursor (ArClutterEmbed *embed,
+                             ArCursorType cursor)
+{
+  /* FIXMEchpe */
+#ifndef HAVE_HILDON
+  ArClutterEmbedPrivate *priv = embed->priv;
+
+  gdk_window_set_cursor (gtk_widget_get_window (GTK_WIDGET (embed)),
+                         priv->cursor[cursor]);
+#endif /* !HAVE_HILDON */
+}
diff --git a/aisleriot/ar-clutter-embed.h b/aisleriot/ar-clutter-embed.h
new file mode 100644
index 0000000..6bc9e7c
--- /dev/null
+++ b/aisleriot/ar-clutter-embed.h
@@ -0,0 +1,61 @@
+/*
+ * Copyright © 2009 Christian Persch <chpe gnome org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __AR_CLUTTER_EMBED_H__
+#define __AR_CLUTTER_EMBED_H__
+
+#include <clutter-gtk/clutter-gtk.h>
+
+#include "ar-style.h"
+#include "ar-cursor.h"
+
+G_BEGIN_DECLS
+
+#define AR_TYPE_CLUTTER_EMBED            (ar_clutter_embed_get_type())
+#define AR_CLUTTER_EMBED(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), AR_TYPE_CLUTTER_EMBED, ArClutterEmbed))
+#define AR_CLUTTER_EMBED_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass),  AR_TYPE_CLUTTER_EMBED, ArClutterEmbedClass))
+#define AR_IS_CLUTTER_EMBED(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), AR_TYPE_CLUTTER_EMBED))
+#define AR_IS_CLUTTER_EMBED_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass),  AR_TYPE_CLUTTER_EMBED))
+#define AR_CLUTTER_EMBED_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj),  AR_TYPE_CLUTTER_EMBED, ArClutterEmbedClass))
+
+typedef struct _ArClutterEmbed        ArClutterEmbed;
+typedef struct _ArClutterEmbedClass   ArClutterEmbedClass;
+typedef struct _ArClutterEmbedPrivate ArClutterEmbedPrivate;
+
+struct _ArClutterEmbed
+{
+  GtkClutterEmbed parent;
+
+  /*< private >*/
+  ArClutterEmbedPrivate *priv;
+};
+
+struct _ArClutterEmbedClass
+{
+  GtkClutterEmbedClass parent_class;
+};
+
+GType ar_clutter_embed_get_type (void);
+
+ArClutterEmbed* ar_clutter_embed_new (ArStyle *style);
+
+void ar_clutter_embed_set_cursor (ArClutterEmbed *embed,
+                                  ArCursorType cursor_type);
+
+G_END_DECLS
+
+#endif /* __AR_CLUTTER_EMBED_H__ */
diff --git a/aisleriot/ar-style-gtk.c b/aisleriot/ar-style-gtk.c
index a1b9b0c..edfe917 100644
--- a/aisleriot/ar-style-gtk.c
+++ b/aisleriot/ar-style-gtk.c
@@ -269,6 +269,24 @@ style_set_cb (GtkWidget *widget,
     g_object_notify (style_object, AR_STYLE_PROP_CARD_STEP);
   }
 
+#ifdef HAVE_CLUTTER
+{
+  ClutterColor selection_color;
+
+  if (color != NULL) {
+    _ar_clutter_color_from_gdk_color (&selection_color, color);
+    gdk_color_free (color);
+  } else {
+    _ar_clutter_color_from_gdk_color (&selection_color, &default_selection_color);
+  }
+
+  if (!clutter_color_equal (&style_priv->selection_color, &selection_color)) {
+    style_priv->selection_color = selection_color;
+
+    g_object_notify (style_object, AR_STYLE_PROP_SELECTION_COLOR);
+  }
+}
+#else
 {
   GdkColor selection_color;
 
@@ -285,6 +303,7 @@ style_set_cb (GtkWidget *widget,
     g_object_notify (style_object, AR_STYLE_PROP_SELECTION_COLOR);
   }
 }
+#endif /* HAVE_CLUTTER */
 
   g_object_thaw_notify (style_object);
 }
diff --git a/aisleriot/ar-style-private.h b/aisleriot/ar-style-private.h
index 22cf407..3e76f67 100644
--- a/aisleriot/ar-style-private.h
+++ b/aisleriot/ar-style-private.h
@@ -49,8 +49,13 @@ struct _ArStylePrivate
 {
   ArCardTheme* card_theme;
 
+#ifdef HAVE_CLUTTER
+  ClutterColor selection_color;
+  ClutterColor baize_color;
+#else
   GdkColor selection_color;
   GdkColor baize_color;
+#endif
 
   double card_slot_ratio;
   double card_overhang;
@@ -79,9 +84,16 @@ struct _ArStylePrivate
   guint show_highlight          : 1;
   guint show_seleccion          : 1;
 
+#ifndef HAVE_CLUTTER
   guint pixbuf_drawing          : 1;
+#endif
 };
 
+#ifdef HAVE_CLUTTER
+void _ar_clutter_color_from_gdk_color (ClutterColor *clutter_color,
+                                       const GdkColor *gdk_color);
+#endif
+
 G_END_DECLS
 
 #endif /* __AR_STYLE_PRIVATE_H__ */
diff --git a/aisleriot/ar-style.c b/aisleriot/ar-style.c
index 2e4998a..3a074df 100644
--- a/aisleriot/ar-style.c
+++ b/aisleriot/ar-style.c
@@ -39,6 +39,9 @@ enum
   PROP_FOCUS_LINE_WIDTH,
   PROP_FOCUS_PADDING,
   PROP_INTERIOR_FOCUS,
+#ifndef HAVE_CLUTTER
+  PROP_PIXBUF_DRAWING,
+#endif
   PROP_RTL,
   PROP_SELECTION_COLOR,
   PROP_SHOW_TOOLTIPS,
@@ -59,8 +62,13 @@ ar_style_init (ArStyle *style)
 
   priv = style->priv = G_TYPE_INSTANCE_GET_PRIVATE (style, AR_TYPE_STYLE, ArStylePrivate);
 
+#ifdef HAVE_CLUTTER
+  _ar_clutter_color_from_gdk_color (&priv->selection_color, &default_selection_color);
+  _ar_clutter_color_from_gdk_color (&priv->baize_color, &default_baize_color);
+#else
   priv->selection_color = default_selection_color;
   priv->baize_color = default_baize_color;
+#endif
 
   priv->card_slot_ratio = DEFAULT_CARD_SLOT_RATIO;
   priv->card_overhang = DEFAULT_CARD_OVERHANG;
@@ -80,6 +88,8 @@ ar_style_init (ArStyle *style)
   priv->enable_tooltips = DEFAULT_SHOW_TOOLTIPS;
   priv->enable_status_messages = DEFAULT_SHOW_STATUS_MESSAGES;
 
+#ifndef HAVE_CLUTTER
+
 #ifdef HAVE_HILDON
   priv->pixbuf_drawing = FALSE;
 #else
@@ -96,6 +106,8 @@ ar_style_init (ArStyle *style)
   _games_debug_print (GAMES_DEBUG_GAME_STYLE,
                       "[ArStyle %p] Using %s drawing\n",
                       style, priv->pixbuf_drawing ? "pixbuf" : "pixmap");
+
+#endif /* !HAVE_CLUTTER */
 }
 
 static void
@@ -173,6 +185,12 @@ ar_style_get_property (GObject    *object,
       g_value_set_boolean (value, ar_style_get_interior_focus (style));
       break;
 
+#ifndef HAVE_CLUTTER
+    case PROP_PIXBUF_DRAWING:
+      g_value_set_boolean (value, ar_style_get_pixbuf_drawing (style));
+      break;
+#endif
+
     case PROP_RTL:
       g_value_set_boolean (value, ar_style_get_rtl (style));
       break;
@@ -209,6 +227,15 @@ ar_style_set_property (GObject      *object,
 
   switch (property_id) {
     case PROP_BAIZE_COLOR: {
+#ifdef HAVE_CLUTTER
+      ClutterColor *color;
+
+      if ((color = g_value_get_boxed (value)) != NULL) {
+        priv->baize_color = *color;
+      } else {
+        _ar_clutter_color_from_gdk_color (&priv->baize_color, &default_baize_color);
+      }
+#else
       GdkColor *color;
 
       if ((color = g_value_get_boxed (value)) != NULL) {
@@ -216,6 +243,7 @@ ar_style_set_property (GObject      *object,
       } else {
         priv->baize_color = default_baize_color;
       }
+#endif
       break;
     }
 
@@ -263,11 +291,25 @@ ar_style_set_property (GObject      *object,
       priv->interior_focus = g_value_get_boolean (value) != FALSE;
       break;
 
+#ifndef HAVE_CLUTTER
+    case PROP_PIXBUF_DRAWING:
+      priv->pixbuf_drawing = g_value_get_boolean (value) != FALSE;
+#endif
+
     case PROP_RTL:
       priv->rtl = g_value_get_boolean (value) != FALSE;
       break;
 
     case PROP_SELECTION_COLOR: {
+#ifdef HAVE_CLUTTER
+      ClutterColor *color;
+
+      if ((color = g_value_get_boxed (value)) != NULL) {
+        priv->selection_color = *color;
+      } else {
+        _ar_clutter_color_from_gdk_color (&priv->selection_color, &default_selection_color);
+      }
+#else
       GdkColor *color;
 
       if ((color = g_value_get_boxed (value)) != NULL) {
@@ -275,6 +317,7 @@ ar_style_set_property (GObject      *object,
       } else {
         priv->selection_color = default_selection_color;
       }
+#endif
       break;
     }
 
@@ -303,6 +346,9 @@ static void
 ar_style_class_init (ArStyleClass *klass)
 {
   GObjectClass *object_class = G_OBJECT_CLASS (klass);
+#ifdef HAVE_CLUTTER
+  ClutterColor color;
+#endif
 
   g_type_class_add_private (klass, sizeof (ArStylePrivate));
 
@@ -315,6 +361,16 @@ ar_style_class_init (ArStyleClass *klass)
    *
    * The board baize color.
    */
+#ifdef HAVE_CLUTTER
+  _ar_clutter_color_from_gdk_color (&color, &default_baize_color);
+  g_object_class_install_property
+    (object_class,
+     PROP_BAIZE_COLOR,
+     clutter_param_spec_color (AR_STYLE_PROP_BAIZE_COLOR, NULL, NULL,
+                               &color,
+                               G_PARAM_READWRITE |
+                               G_PARAM_STATIC_STRINGS));
+#else
   g_object_class_install_property
     (object_class,
      PROP_BAIZE_COLOR,
@@ -322,6 +378,7 @@ ar_style_class_init (ArStyleClass *klass)
                          GDK_TYPE_COLOR,
                          G_PARAM_READWRITE |
                          G_PARAM_STATIC_STRINGS));
+#endif /* HAVE_CLUTTER */
 
   g_object_class_install_property
     (object_class,
@@ -431,6 +488,16 @@ ar_style_class_init (ArStyleClass *klass)
                            G_PARAM_READWRITE |
                            G_PARAM_STATIC_STRINGS));
 
+#ifndef HAVE_CLUTTER
+  g_object_class_install_property
+    (object_class,
+     PROP_PIXBUF_DRAWING,
+     g_param_spec_boolean (AR_STYLE_PROP_PIXBUF_DRAWING, NULL, NULL,
+                           DEFAULT_PIXBUF_DRAWING,
+                           G_PARAM_READWRITE |
+                           G_PARAM_STATIC_STRINGS));
+#endif /* !HAVE_CLUTTER */
+
   g_object_class_install_property
     (object_class,
      PROP_RTL,
@@ -439,6 +506,16 @@ ar_style_class_init (ArStyleClass *klass)
                            G_PARAM_READWRITE |
                            G_PARAM_STATIC_STRINGS));
 
+#ifdef HAVE_CLUTTER
+  _ar_clutter_color_from_gdk_color (&color, &default_selection_color);
+  g_object_class_install_property
+    (object_class,
+     PROP_SELECTION_COLOR,
+     clutter_param_spec_color (AR_STYLE_PROP_SELECTION_COLOR, NULL, NULL,
+                               &color,
+                               G_PARAM_READWRITE |
+                               G_PARAM_STATIC_STRINGS));
+#else
   g_object_class_install_property
     (object_class,
      PROP_SELECTION_COLOR,
@@ -446,6 +523,7 @@ ar_style_class_init (ArStyleClass *klass)
                          GDK_TYPE_COLOR,
                          G_PARAM_READWRITE |
                          G_PARAM_STATIC_STRINGS));
+#endif /* HAVE_CLUTTER */
 
   /**
    * ArStyle:show-tooltips:
@@ -484,6 +562,20 @@ ar_style_class_init (ArStyleClass *klass)
 
 /* private API */
 
+#ifdef HAVE_CLUTTER
+
+void
+_ar_clutter_color_from_gdk_color (ClutterColor *clutter_color,
+                                  const GdkColor *gdk_color)
+{
+  clutter_color->red   = gdk_color->red   >> 8;
+  clutter_color->green = gdk_color->green >> 8;
+  clutter_color->blue  = gdk_color->blue  >> 8;
+  clutter_color->alpha = 0xff;
+}
+
+#endif /* HAVE_CLUTTER */
+
 /* public API */
 
 /**
@@ -805,7 +897,11 @@ ar_style_get_card_step (ArStyle *style)
  */
 void
 ar_style_get_selection_color (ArStyle *style,
+#ifdef HAVE_CLUTTER
+                              ClutterColor * const color)
+#else
                               GdkColor * const color)
+#endif
 {
   ArStylePrivate *priv = style->priv;
 
@@ -820,7 +916,11 @@ ar_style_get_selection_color (ArStyle *style,
  */
 void
 ar_style_get_baize_color (ArStyle *style,
+#ifdef HAVE_CLUTTER
+                               ClutterColor * const color)
+#else
                                GdkColor * const color)
+#endif
 {
   ArStylePrivate *priv = style->priv;
 
@@ -856,6 +956,7 @@ ar_style_check_dnd_drag_threshold (ArStyle *style,
           ABS (y2 - y1) > priv->dnd_drag_threshold);
 }
 
+#ifndef HAVE_CLUTTER
 /**
  * ar_style_get_pixbuf_drawing:
  * @style:
@@ -869,3 +970,5 @@ ar_style_get_pixbuf_drawing (ArStyle *style)
 
   return priv->pixbuf_drawing;
 }
+
+#endif /* !HAVE_CLUTTER */
diff --git a/aisleriot/ar-style.h b/aisleriot/ar-style.h
index c75e5dc..db7dec8 100644
--- a/aisleriot/ar-style.h
+++ b/aisleriot/ar-style.h
@@ -20,7 +20,11 @@
 
 #include <glib-object.h>
 
+#ifdef HAVE_CLUTTER
+#include <clutter/clutter.h>
+#else
 #include <gdk/gdk.h>
+#endif
 
 #include "ar-card-theme.h"
 
@@ -104,10 +108,17 @@ double ar_style_get_card_slot_ratio (ArStyle *style);
 double ar_style_get_card_overhang   (ArStyle *style);
 double ar_style_get_card_step       (ArStyle *style);
 
+#ifdef HAVE_CLUTTER
+void ar_style_get_selection_color  (ArStyle *style,
+                                    ClutterColor * const color);
+void ar_style_get_baize_color (ArStyle *style,
+                                    ClutterColor * const color);
+#else
 void ar_style_get_selection_color  (ArStyle *style,
                                     GdkColor * const color);
 void ar_style_get_baize_color (ArStyle *style,
                                     GdkColor * const color);
+#endif
 
 gboolean ar_style_check_dnd_drag_threshold (ArStyle *style,
                                             float x1,
@@ -115,10 +126,14 @@ gboolean ar_style_check_dnd_drag_threshold (ArStyle *style,
                                             float x2,
                                             float y2);
 
+#ifndef HAVE_CLUTTER
+
 #define AR_STYLE_PROP_PIXBUF_DRAWING "pixbuf-drawing"
 
 gboolean ar_style_get_pixbuf_drawing (ArStyle *style);
 
+#endif /* !HAVE_CLUTTER */
+
 G_END_DECLS
 
 #endif /* __AR_STYLE_H__ */
diff --git a/aisleriot/baize.c b/aisleriot/baize.c
new file mode 100644
index 0000000..4496a53
--- /dev/null
+++ b/aisleriot/baize.c
@@ -0,0 +1,91 @@
+/*
+ * Copyright © 2008 Neil Roberts
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <config.h>
+
+#include "baize.h"
+
+#include <cogl/cogl.h>
+
+#include <libgames-support/games-runtime.h>
+
+/* Special version of ClutterTexture that repeats the texture to fill
+   the entire stage. This is used to paint the baize background */
+
+static void aisleriot_baize_paint (ClutterActor *actor);
+
+G_DEFINE_TYPE (AisleriotBaize, aisleriot_baize, CLUTTER_TYPE_TEXTURE);
+
+static void
+aisleriot_baize_class_init (AisleriotBaizeClass *klass)
+{
+  ClutterActorClass *actor_class = (ClutterActorClass *) klass;
+
+  actor_class->paint = aisleriot_baize_paint;
+}
+
+static void
+aisleriot_baize_init (AisleriotBaize *baize)
+{
+  char *path;
+  GError *error = NULL;
+
+  path = games_runtime_get_file (GAMES_RUNTIME_PIXMAP_DIRECTORY, "baize.png");
+  if (!clutter_texture_set_from_file (CLUTTER_TEXTURE (baize), path, &error)) {
+    g_warning ("Failed to load the baize from '%s': %s\n", path, error->message);
+    g_error_free (error);
+  }
+
+  g_free (path);
+}
+
+ClutterActor *
+aisleriot_baize_new (void)
+{
+  return g_object_new (AISLERIOT_TYPE_BAIZE, NULL);
+}
+
+static void
+aisleriot_baize_paint (ClutterActor *actor)
+{
+  ClutterActor *stage;
+  CoglHandle tex;
+  ClutterGeometry stage_geom;
+  guint tex_width, tex_height;
+
+  if ((stage = clutter_actor_get_stage (actor)) == NULL)
+    return;
+
+  if ((tex = clutter_texture_get_cogl_texture (CLUTTER_TEXTURE (actor)))
+      == COGL_INVALID_HANDLE)
+    return;
+
+  tex_width = cogl_texture_get_width (tex);
+  tex_height = cogl_texture_get_height (tex);
+
+  if (tex_width < 1 || tex_height < 1)
+    return;
+
+  clutter_actor_get_allocation_geometry (stage, &stage_geom);
+
+  /* Repeat the texture to fill the size of the stage */
+  cogl_set_source_texture (tex);
+  cogl_rectangle_with_texture_coords (0, 0, stage_geom.width, stage_geom.height,
+                                      0, 0,
+                                      (gfloat) stage_geom.width / tex_width,
+                                      (gfloat) stage_geom.height / tex_height);
+}
diff --git a/aisleriot/baize.h b/aisleriot/baize.h
new file mode 100644
index 0000000..d6585b3
--- /dev/null
+++ b/aisleriot/baize.h
@@ -0,0 +1,65 @@
+/*
+ * Copyright © 2008 Neil Roberts
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef AISLERIOT_BAIZE_H
+#define AISLERIOT_BAIZE_H
+
+#include <clutter/clutter.h>
+
+G_BEGIN_DECLS
+
+#define AISLERIOT_TYPE_BAIZE                                            \
+  (aisleriot_baize_get_type())
+#define AISLERIOT_BAIZE(obj)                                            \
+  (G_TYPE_CHECK_INSTANCE_CAST ((obj),                                   \
+                               AISLERIOT_TYPE_BAIZE,                    \
+                               AisleriotBaize))
+#define AISLERIOT_BAIZE_CLASS(klass)                                    \
+  (G_TYPE_CHECK_CLASS_CAST ((klass),                                    \
+                            AISLERIOT_TYPE_BAIZE,                       \
+                            AisleriotBaizeClass))
+#define AISLERIOT_IS_BAIZE(obj)                                         \
+  (G_TYPE_CHECK_INSTANCE_TYPE ((obj),                                   \
+                               AISLERIOT_TYPE_BAIZE))
+#define AISLERIOT_IS_BAIZE_CLASS(klass)                                 \
+  (G_TYPE_CHECK_CLASS_TYPE ((klass),                                    \
+                            AISLERIOT_TYPE_BAIZE))
+#define AISLERIOT_BAIZE_GET_CLASS(obj)                                  \
+  (G_TYPE_INSTANCE_GET_CLASS ((obj),                                    \
+                              AISLERIOT_TYPE_BAIZE,                     \
+                              AisleriotBaizeClass))
+
+typedef struct _AisleriotBaize      AisleriotBaize;
+typedef struct _AisleriotBaizeClass AisleriotBaizeClass;
+
+struct _AisleriotBaizeClass
+{
+  ClutterTextureClass parent_class;
+};
+
+struct _AisleriotBaize
+{
+  ClutterTexture parent;
+};
+
+GType aisleriot_baize_get_type (void) G_GNUC_CONST;
+
+ClutterActor *aisleriot_baize_new (void);
+
+G_END_DECLS
+
+#endif /* AISLERIOT_BAIZE_H */
diff --git a/aisleriot/board.c b/aisleriot/board.c
new file mode 100644
index 0000000..bef8b86
--- /dev/null
+++ b/aisleriot/board.c
@@ -0,0 +1,3536 @@
+/*
+ * Copyright © 1998, 2003 Jonathan Blandford <jrb mit edu>
+ * Copyright © 2007, 2008, 2009, 2010 Christian Persch
+ *
+ * Some code copied from gtk+/gtk/gtkiconview (LGPL2+):
+ * Copyright © 2002, 2004  Anders Carlsson <andersca gnu org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <config.h>
+
+#include "board.h"
+
+#include <string.h>
+
+#include <gtk/gtk.h>
+#include <gdk/gdkkeysyms.h>
+
+#include <clutter/clutter.h>
+
+#include <libgames-support/games-debug.h>
+#include <libgames-support/games-glib-compat.h>
+#include <libgames-support/games-marshal.h>
+#include <libgames-support/games-sound.h>
+
+#include "conf.h"
+
+#include "game.h"
+#include "card.h"
+#include "slot-renderer.h"
+#include "ar-card-textures-cache.h"
+#include "ar-cursor.h"
+
+#define AISLERIOT_BOARD_GET_PRIVATE(board)(G_TYPE_INSTANCE_GET_PRIVATE ((board), AISLERIOT_TYPE_BOARD, AisleriotBoardPrivate))
+
+/* Enable keynav on non-hildon by default */
+#if !defined(HAVE_HILDON) && !defined(DISABLE_KEYNAV)
+#define ENABLE_KEYNAV
+#endif /* !HAVE_HILDON */
+
+/* The limits for how much overlap there is between cards and
+ * how much is allowed to slip off the bottom or right.
+ */
+#define MIN_DELTA (0.05)
+
+/* The minimum size for the playing area. Almost completely arbitrary. */
+#define BOARD_MIN_WIDTH 300
+#define BOARD_MIN_HEIGHT 200
+
+#define DOUBLE_TO_INT_CEIL(d) ((int) (d + 0.5))
+
+#define I_(string) g_intern_static_string (string)
+  
+/* FIXMEchpe: file a bug to get an exported function like gtk_accelerator_get_default_mod_mask() for this? */
+/* Copied from clutter-binding-pool.c */
+#define CLUTTER_DEFAULT_MOD_MASK ((CLUTTER_SHIFT_MASK   | \
+                                   CLUTTER_CONTROL_MASK | \
+                                   CLUTTER_MOD1_MASK    | \
+                                   CLUTTER_SUPER_MASK   | \
+                                   CLUTTER_HYPER_MASK   | \
+                                   CLUTTER_META_MASK)   | \
+                                  CLUTTER_RELEASE_MASK)
+
+#pragma GCC poison GtkWidget
+#pragma GCC poison widget
+
+typedef enum {
+  STATUS_NONE,
+  STATUS_MAYBE_DRAG,
+  STATUS_NOT_DRAG,
+  STATUS_IS_DRAG,
+  STATUS_SHOW,
+  LAST_STATUS
+} MoveStatus;
+
+#ifdef ENABLE_KEYNAV
+
+#define MOVE_CURSOR_LEFT_RIGHT    'h'
+#define MOVE_CURSOR_LEFT_RIGHT_S  "h"
+#define MOVE_CURSOR_UP_DOWN       'v'
+#define MOVE_CURSOR_UP_DOWN_S     "v"
+#define MOVE_CURSOR_PAGES         'p'
+#define MOVE_CURSOR_PAGES_S       "p"
+#define MOVE_CURSOR_START_END     'e'
+#define MOVE_CURSOR_START_END_S   "e"
+
+#define MOVE_LEFT     'l'
+#define MOVE_LEFT_S   "l"
+#define MOVE_RIGHT    'r'
+#define MOVE_RIGHT_S  "r"
+
+#endif /* ENABLE_KEYNAV */
+
+struct _AisleriotBoardPrivate
+{
+  AisleriotGame *game;
+
+  ArStyle *style;
+
+  ClutterActorBox allocation;
+
+  /* Card theme */
+  CardSize card_size;
+
+  /* Cards cache */
+  ArCardTexturesCache *textures;
+
+  double width;
+  double height;
+
+  /* The size of a slot in pixels. */
+  double xslotstep;
+  double yslotstep;
+
+  /* How much of the slot the card should take up */
+  double card_slot_ratio;
+
+  /* The offset of the cards within the slot. */
+  int xoffset, yoffset;
+
+  /* The offset within the window. */
+  int xbaseoffset;
+
+  /* Button press */
+  int last_click_x;
+  int last_click_y;
+  guint32 last_click_time;
+
+  /* Moving cards */
+  ArSlot *moving_cards_origin_slot;
+  int moving_cards_origin_card_id; /* The index of the card that was clicked on in hslot->cards; or -1 if the click wasn't on a card */
+  ClutterActor *moving_cards_group;
+  GByteArray *moving_cards;
+
+  /* A group to put animated cards above the slots */
+  ClutterActor *animation_layer;
+
+  /* The 'reveal card' action's slot and card link */
+  ArSlot *show_card_slot;
+  int show_card_id;
+
+  /* Click data */
+  ArSlot *last_clicked_slot;
+  int last_clicked_card_id;
+
+  /* Focus handling */
+  ArSlot *focus_slot;
+  int focus_card_id; /* -1 for focused empty slot */
+  int focus_line_width;
+  int focus_padding;
+  GdkRectangle focus_rect;
+
+  /* Selection */
+  ArSlot *selection_slot;
+  int selection_start_card_id;
+
+  /* Highlight */
+  ArSlot *highlight_slot;
+
+  /* Array RemovedCards to be dropped in animations */
+  GArray *removed_cards;
+
+  /* Idle handler where the slots will be compared for changes to
+     trigger animations */
+  guint check_animations_handler;
+
+  /* Status message */
+  const char *status_message; /* interned */
+
+  /* Bit field */
+  guint droppable_supported : 1;
+  guint touchscreen_mode : 1;
+  guint use_pixbuf_drawing : 1;
+  guint show_focus : 1; /* whether the focus is drawn */
+  guint interior_focus : 1;
+
+  guint click_to_move : 1;
+
+  guint geometry_set : 1;
+  guint is_rtl : 1;
+
+  guint last_click_left_click : 1;
+  guint click_status : 4; /* enough bits for MoveStatus */
+
+  guint show_selection : 1;
+  guint show_highlight : 1;
+  guint show_status_messages : 1;
+
+  guint force_geometry_update : 1;
+};
+
+typedef struct _RemovedCard RemovedCard;
+
+struct _RemovedCard
+{
+  Card card;
+  gint cardx, cardy;
+  gboolean from_drag;
+};
+
+G_STATIC_ASSERT (LAST_STATUS < 16 /* 2^4 */);
+
+enum
+{
+  PROP_0,
+  PROP_GAME,
+  PROP_STYLE
+};
+
+enum
+{
+  REQUEST_CURSOR,
+  ERROR_BELL,
+  STATUS_MESSAGE,
+  FOCUS,
+#ifdef ENABLE_KEYNAV
+  ACTIVATE,
+  MOVE_CURSOR,
+  TOGGLE_SELECTION,
+  SELECT_ALL,
+  DESELECT_ALL,
+#endif /* ENABLE_KEYNAV */
+  LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL];
+
+static void get_slot_and_card_from_point (AisleriotBoard *board,
+                                          int x,
+                                          int y,
+                                          ArSlot **slot,
+                                          int *_cardid);
+static void slot_update_card_images      (AisleriotBoard *board,
+                                          ArSlot *slot);
+static void slot_update_card_images_full (AisleriotBoard *board,
+                                          ArSlot *slot,
+                                          gint highlight_start_card_id);
+
+static void aisleriot_board_setup_geometry (AisleriotBoard *board);
+
+static void
+set_cursor (AisleriotBoard *board,
+            ArCursorType cursor)
+{
+  g_signal_emit (board, signals[REQUEST_CURSOR], 0, (int) cursor);
+}
+
+/* If we are over a slot, set the cursor to the given cursor,
+ * otherwise use the default cursor. */
+static void
+set_cursor_by_location (AisleriotBoard *board,
+                        int x,
+                        int y)
+{
+#ifndef HAVE_HILDON 
+  AisleriotBoardPrivate *priv = board->priv;
+  ArSlot *selection_slot = priv->selection_slot;
+  int selection_start_card_id = priv->selection_start_card_id;
+  ArSlot *slot;
+  int card_id;
+  gboolean drop_valid = FALSE;
+  ArCursorType cursor = AR_CURSOR_DEFAULT;
+
+  get_slot_and_card_from_point (board, x, y, &slot, &card_id);
+
+  if (priv->click_to_move &&
+      slot != NULL &&
+      selection_slot != NULL &&
+      slot != selection_slot &&
+      selection_start_card_id >= 0) {
+    g_return_if_fail (selection_slot->cards->len > selection_start_card_id);
+
+    drop_valid = aisleriot_game_drop_valid (priv->game,
+                                            selection_slot->id,
+                                            slot->id,
+                                            selection_slot->cards->data + selection_start_card_id,
+                                            selection_slot->cards->len - selection_start_card_id);
+  }
+  /* FIXMEchpe: special cursor when _drag_ is possible? */
+
+  if (drop_valid) {
+    cursor = AR_CURSOR_DROPPABLE;
+  } else if (slot != NULL &&
+             card_id >= 0 &&
+             !CARD_GET_FACE_DOWN (CARD (slot->cards->data[card_id]))) {
+    if (priv->click_status == STATUS_NONE) {
+      cursor = AR_CURSOR_OPEN;
+    } else {
+      cursor = AR_CURSOR_CLOSED;
+    }
+  }
+
+  set_cursor (board, cursor);
+#endif /* !HAVE_HILDON */
+}
+
+/* status message */
+
+static void
+set_status_message (AisleriotBoard *board,
+                    const char *message)
+{
+  AisleriotBoardPrivate *priv = board->priv;
+
+  if (g_strcmp0 (priv->status_message, message) == 0)
+    return;
+
+  priv->status_message = g_intern_string (message);
+
+  g_signal_emit (board, signals[STATUS_MESSAGE], 0, priv->status_message);
+}
+
+/* Slot helpers */
+
+static void
+get_slot_and_card_from_point (AisleriotBoard *board,
+                              int x,
+                              int y,
+                              ArSlot **slot,
+                              int *_cardid)
+{
+  AisleriotBoardPrivate *priv = board->priv;
+  GPtrArray *slots;
+  gboolean got_slot = FALSE;
+  int num_cards;
+  int i, n_slots;
+  int cardid;
+
+  *slot = NULL;
+  cardid = -1;
+
+  slots = aisleriot_game_get_slots (priv->game);
+
+  n_slots = slots->len;
+  for (i = n_slots - 1; i >= 0; --i) {
+    ArSlot *hslot = slots->pdata[i];
+
+    /* if point is within our rectangle */
+    if (hslot->rect.x <= x && x <= hslot->rect.x + hslot->rect.width &&
+        hslot->rect.y <= y && y <= hslot->rect.y + hslot->rect.height) {
+      num_cards = hslot->cards->len;
+
+      if (got_slot == FALSE || num_cards > 0) {
+        /* if we support exposing more than one card,
+         * find the exact card  */
+
+        gint depth = 1;
+
+        if (hslot->pixeldx > 0)
+          depth += (x - hslot->rect.x) / hslot->pixeldx;
+        else if (hslot->pixeldx < 0)
+          depth += (hslot->rect.x + hslot->rect.width - x) / -hslot->pixeldx;
+        else if (hslot->pixeldy > 0)
+          depth += (y - hslot->rect.y) / hslot->pixeldy;
+
+        /* account for the last card getting much more display area
+         * or no cards */
+
+        if (depth > hslot->exposed)
+          depth = hslot->exposed;
+        *slot = hslot;
+
+        /* card = #cards in slot + card chosen (indexed in # exposed cards) - # exposed cards */
+
+        cardid = num_cards + depth - hslot->exposed;
+
+        /* this is the topmost slot with a card */
+        /* take it and run */
+        if (num_cards > 0)
+          break;
+
+        got_slot = TRUE;
+      }
+    }
+  }
+
+  *_cardid = cardid > 0 ? cardid - 1 : -1;
+}
+
+#ifdef ENABLE_KEYNAV
+
+static gboolean
+test_slot_projection_intersects_x (ArSlot *slot,
+                                   int x_start,
+                                   int x_end)
+{
+  return slot->rect.x <= x_end &&
+         slot->rect.x + slot->rect.width >= x_start;
+}
+
+static int
+get_slot_index_from_slot (AisleriotBoard *board,
+                          ArSlot *slot)
+{
+  AisleriotBoardPrivate *priv = board->priv;
+  GPtrArray *slots;
+  guint n_slots;
+  int slot_index;
+
+  g_assert (slot != NULL);
+
+  slots = aisleriot_game_get_slots (priv->game);
+  n_slots = slots->len;
+  g_assert (n_slots > 0);
+
+  for (slot_index = 0; slot_index < n_slots; ++slot_index) {
+    if (g_ptr_array_index (slots, slot_index) == slot)
+      break;
+  }
+
+  g_assert (slot_index < n_slots); /* the slot EXISTS after all */
+
+  return slot_index;
+}
+
+#endif /* ENABLE_KEYNAV */
+
+static void
+get_rect_by_slot_and_card (AisleriotBoard *board,
+                           ArSlot *slot,
+                           int card_id,
+                           int num_cards,
+                           GdkRectangle *rect)
+{
+  AisleriotBoardPrivate *priv = board->priv;
+  guint delta;
+  int first_card_id, num;
+
+  g_return_if_fail (slot != NULL && card_id >= -1);
+
+  first_card_id = ((int) slot->cards->len) - ((int) slot->exposed);
+
+  if (card_id >= first_card_id) {
+    delta = card_id - first_card_id;
+    num = num_cards - 1;
+
+    rect->x = slot->rect.x + delta * slot->pixeldx;
+    rect->y = slot->rect.y + delta * slot->pixeldy;
+    rect->width = priv->card_size.width + num * slot->pixeldx;
+    rect->height = priv->card_size.height + num * slot->pixeldy;
+        
+    if (priv->is_rtl &&
+        slot->expanded_right) {
+      rect->x += slot->rect.width - priv->card_size.width;
+    }
+
+  } else {
+    /* card_id == -1 or no card available, return the slot rect.
+     * Its size should be card_size.
+     */
+    *rect = slot->rect;
+  }
+}
+
+/* Focus handling */
+
+static void
+widen_rect (GdkRectangle *rect,
+            int delta)
+{
+  int x, y;
+
+  x = rect->x - delta;
+  y = rect->y - delta;
+
+  rect->x = MAX (x, 0);
+  rect->y = MAX (y, 0);
+  rect->width = rect->width + 2 * delta;
+  rect->height = rect->height + 2 * delta;
+}
+
+static void
+get_focus_rect (AisleriotBoard *board,
+                GdkRectangle *rect)
+{
+  AisleriotBoardPrivate *priv = board->priv;
+
+  if (!priv->focus_slot)
+    return;
+
+  get_rect_by_slot_and_card (board,
+                             priv->focus_slot,
+                             priv->focus_card_id,
+                             1, rect);
+  widen_rect (rect, priv->focus_line_width + priv->focus_padding);
+}
+
+static void
+set_focus (AisleriotBoard *board,
+           ArSlot *slot,
+           int card_id,
+           gboolean show_focus)
+{
+  AisleriotBoardPrivate *priv = board->priv;
+//   GtkWidget *widget = GTK_WIDGET (board);
+  int top_card_id;
+
+  /* Sanitise */
+  top_card_id = slot ? ((int) slot->cards->len) - 1 : -1;
+  card_id = MIN (card_id, top_card_id);
+
+  if (priv->focus_slot == slot &&
+      priv->focus_card_id == card_id &&
+      priv->show_focus == show_focus)
+    return;
+
+  if (priv->focus_slot != NULL) {
+#ifdef FIXMEchpe
+    if (priv->show_focus &&
+        gtk_widget_has_focus (widget)) {
+      gdk_window_invalidate_rect (widget->window, &priv->focus_rect, FALSE);
+    
+      priv->show_focus = FALSE;
+    }
+#endif
+
+    priv->focus_slot = NULL;
+    priv->focus_card_id = -1;
+  }
+
+  priv->show_focus = show_focus;
+
+  if (!slot)
+    return;
+
+  priv->focus_slot = slot;
+  priv->focus_card_id = card_id;
+
+#ifdef FIXMEchpe
+  if (show_focus &&
+      gtk_widget_has_focus (widget)) {
+    get_focus_rect (board, &priv->focus_rect);
+    gdk_window_invalidate_rect (widget->window, &priv->focus_rect, FALSE);
+  }
+#endif
+}
+
+/* Selection handling */
+
+static void
+set_selection (AisleriotBoard *board,
+               ArSlot *slot,
+               int card_id,
+               gboolean show_selection)
+{
+  AisleriotBoardPrivate *priv = board->priv;
+
+  if (priv->selection_slot == slot &&
+      priv->selection_start_card_id == card_id &&
+      priv->show_selection == show_selection)
+    return;
+
+  if (priv->selection_slot != NULL) {
+    if (priv->show_selection) {
+      /* Clear selection card images */
+      slot_update_card_images_full (board, priv->selection_slot, G_MAXINT);
+    }
+
+    priv->selection_slot = NULL;
+    priv->selection_start_card_id = -1;
+  }
+
+  priv->show_selection = show_selection;
+  priv->selection_slot = slot;
+  priv->selection_start_card_id = card_id;
+  g_assert (slot != NULL || card_id == -1);
+
+  if (!slot)
+    return;
+
+  g_assert (card_id < 0 || card_id < slot->cards->len);
+
+  if (priv->show_selection) {
+    slot_update_card_images_full (board, slot, card_id);
+  }
+}
+
+/* Slot functions */
+
+static void
+slot_update_geometry (AisleriotBoard *board,
+                      ArSlot *slot)
+{
+  AisleriotBoardPrivate *priv = board->priv;
+//   GtkWidget *widget = GTK_WIDGET (board);
+  GdkRectangle old_rect;
+  GByteArray *cards;
+  int delta, xofs, yofs, pixeldx;
+  double card_step;
+
+  if (!priv->geometry_set)
+    return;
+
+  cards = slot->cards;
+  old_rect = slot->rect;
+
+  card_step = ar_style_get_card_step (priv->style);
+
+  xofs = priv->xoffset;
+  yofs = priv->yoffset;
+
+  /* FIXMEchpe: what exactly is the purpose of the following lines? */
+  if (slot->expanded_right)
+    xofs = yofs;
+  if (slot->expanded_down)
+    yofs = xofs;
+
+  if (priv->is_rtl) {
+    slot->rect.x = priv->xslotstep * (priv->width - slot->x) - priv->card_size.width - xofs + priv->xbaseoffset;
+  } else {
+    slot->rect.x = priv->xslotstep * slot->x + xofs + priv->xbaseoffset;
+  }
+
+  slot->rect.y = priv->yslotstep * slot->y + yofs; /* FIXMEchpe + priv->ybaseoffset; */
+
+  /* We need to make sure the cards fit within the board, even
+   * when there are many of them. See bug #171417.
+   */
+  /* FIXMEchpe: check |slot->exposed| instead of cards->len? */
+  pixeldx = 0;
+  if (cards->len > 1) {
+    double dx = 0, dy = 0;
+    double n_cards = cards->len - 1; /* FIXMEchpe: slot->exposed - 1 ? */
+
+    if (slot->expanded_down) {
+      double y_from_bottom, max_dy = card_step;
+      float allocation_height = priv->allocation.y2 - priv->allocation.y1;
+
+      if (slot->dy_set)
+        max_dy = slot->expansion.dy;
+
+      /* Calculate the compressed_dy that will let us fit within the board */
+#ifdef FIXMEchpe
+      y_from_bottom = ((double) (widget->allocation.height - slot->rect.y)) / ((double) priv->card_size.height);
+#else
+      y_from_bottom = ((double) (allocation_height - slot->rect.y)) / ((double) priv->card_size.height);
+#endif
+      dy = (y_from_bottom - (1.0 - ar_style_get_card_overhang (priv->style))) / n_cards;
+      dy = CLAMP (dy, MIN_DELTA, max_dy);
+    } else if (slot->expanded_right) {
+      if (priv->is_rtl) {
+        double x_from_left, max_dx = card_step;
+
+        if (slot->dx_set)
+          max_dx = slot->expansion.dx;
+
+        x_from_left = ((double) slot->rect.x) / ((double) priv->card_size.width) + 1.0;
+        dx = (x_from_left - (1.0 - ar_style_get_card_overhang (priv->style))) / n_cards;
+        dx = CLAMP (dx, MIN_DELTA, max_dx);
+
+        slot->pixeldx = DOUBLE_TO_INT_CEIL (- dx * priv->card_size.width);
+        pixeldx = -slot->pixeldx;
+      } else {
+        double x_from_right, max_dx = card_step;
+        float allocation_width = priv->allocation.x2 - priv->allocation.x1;
+
+        if (slot->dx_set)
+          max_dx = slot->expansion.dx;
+
+#ifdef FIXMEchpe
+        x_from_right = ((double) (widget->allocation.width - slot->rect.x)) / ((double) priv->card_size.width);
+#else
+        x_from_right = ((double) (allocation_width - slot->rect.x)) / ((double) priv->card_size.width);
+#endif
+        dx = (x_from_right - (1.0 - ar_style_get_card_overhang (priv->style))) / n_cards;
+        dx = CLAMP (dx, MIN_DELTA, max_dx);
+
+        pixeldx = slot->pixeldx = DOUBLE_TO_INT_CEIL (dx * priv->card_size.width);        
+      }
+    }
+
+    slot->pixeldy = DOUBLE_TO_INT_CEIL (dy * priv->card_size.height);
+  } else {
+    slot->pixeldx = slot->pixeldy = 0;
+  }
+
+  slot->exposed = cards->len;
+  if (0 < slot->expansion_depth &&
+      slot->expansion_depth < slot->exposed) {
+    slot->exposed = slot->expansion_depth;
+  }
+
+  if ((slot->pixeldx == 0 &&
+       slot->pixeldy == 0 &&
+       slot->exposed > 1) ||
+      (cards->len > 0 &&
+       slot->exposed < 1)) {
+    slot->exposed = 1;
+  }
+
+  delta = slot->exposed > 0 ? slot->exposed - 1 : 0;
+
+  slot->rect.width = priv->card_size.width + delta * pixeldx;
+  slot->rect.height = priv->card_size.height + delta * slot->pixeldy;
+
+  if (priv->is_rtl) {
+    slot->rect.x -= slot->rect.width - priv->card_size.width;
+  }
+
+  if (slot->slot_renderer)
+    clutter_actor_set_position (slot->slot_renderer,
+                                slot->rect.x, slot->rect.y);
+
+  slot->needs_update = FALSE;
+}
+
+static gboolean
+check_animations_cb (gpointer user_data)
+{
+  AisleriotBoard *board = user_data;
+  AisleriotBoardPrivate *priv = board->priv;
+  GPtrArray *slots;
+  int slot_num, i;
+  ArSlot *slot;
+  GArray *animations = g_array_new (FALSE, FALSE, sizeof (AisleriotAnimStart));
+
+  slots = aisleriot_game_get_slots (priv->game);
+
+  /* Find any cards that have been removed from the top of the
+     slots */
+  for (slot_num = 0; slot_num < slots->len; slot_num++) {
+    slot = slots->pdata[slot_num];
+
+    if (slot->old_cards->len > slot->cards->len) {
+      for (i = 0; i < slot->cards->len; i++) {
+        Card old_card = CARD (slot->old_cards->data[i]);
+        Card new_card = CARD (slot->cards->data[i]);
+
+        if (old_card.attr.suit != new_card.attr.suit
+            || old_card.attr.rank != new_card.attr.rank)
+          break;
+      }
+
+      if (i >= slot->cards->len) {
+        for (; i < slot->old_cards->len; i++) {
+          RemovedCard removed_card;
+
+          removed_card.card = CARD (slot->old_cards->data[i]);
+          aisleriot_game_get_card_offset (slot, i,
+                                          TRUE,
+                                          &removed_card.cardx,
+                                          &removed_card.cardy);
+          removed_card.cardx += slot->rect.x;
+          removed_card.cardy += slot->rect.y;
+          removed_card.from_drag = FALSE;
+          g_array_append_val (priv->removed_cards, removed_card);
+        }
+      }
+    }
+  }
+
+  for (slot_num = 0; slot_num < slots->len; slot_num++) {
+    /* Number of extra cards that aren't visible to include in the
+       animation */
+    guint n_unexposed_animated_cards = 0;
+
+    slot = slots->pdata[slot_num];
+
+    g_array_set_size (animations, 0);
+
+    /* Check if the top card has been flipped over */
+    if (slot->old_cards->len >= slot->cards->len
+        && slot->cards->len >= 1
+        && !memcmp (slot->old_cards->data, slot->cards->data,
+                    slot->cards->len - 1)) {
+      Card old_card = CARD (slot->old_cards->data[slot->cards->len - 1]);
+      Card new_card = CARD (slot->cards->data[slot->cards->len - 1]);
+
+      if (old_card.attr.suit == new_card.attr.suit
+          && old_card.attr.rank == new_card.attr.rank
+          && old_card.attr.face_down != new_card.attr.face_down) {
+        AisleriotAnimStart anim;
+
+        aisleriot_game_get_card_offset (slot, slot->cards->len - 1,
+                                        FALSE,
+                                        &anim.cardx,
+                                        &anim.cardy);
+        anim.cardx += slot->rect.x;
+        anim.cardy += slot->rect.y;
+        anim.old_card = old_card;
+        anim.raise = TRUE;
+
+        g_array_append_val (animations, anim);
+      }
+      /* Check if any cards have been added from the removed cards
+         pile */
+    } else if (slot->old_cards->len < slot->cards->len
+               && !memcmp (slot->old_cards->data, slot->cards->data,
+                           slot->old_cards->len)) {
+      for (i = MAX (slot->old_cards->len, slot->cards->len - slot->exposed);
+           i < slot->cards->len;
+           i++) {
+        Card added_card = CARD (slot->cards->data[i]);
+        int j;
+
+        for (j = 0; j < priv->removed_cards->len; j++) {
+          RemovedCard *removed_card = &g_array_index (priv->removed_cards,
+                                                      RemovedCard, j);
+
+          if (added_card.attr.suit == removed_card->card.attr.suit
+              && added_card.attr.rank == removed_card->card.attr.rank) {
+            AisleriotAnimStart anim;
+
+            anim.cardx = removed_card->cardx;
+            anim.cardy = removed_card->cardy;
+            anim.old_card = removed_card->card;
+            anim.raise = !removed_card->from_drag;
+
+            g_array_append_val (animations, anim);
+
+            g_array_remove_index (priv->removed_cards, j);
+
+            break;
+          }
+        }
+      }
+
+      /* Check if any extra unexposed cards are included in the
+         animation. There's no point in drawing these because they
+         will be hidden by the exposed cards but we don't want to draw
+         them at the slot either. This will for example happen in
+         Canfield when the discard pile is flipped over into the draw
+         pile */
+      if (animations->len > 0 && animations->len == slot->exposed)
+        {
+          AisleriotAnimStart *anim = &g_array_index (animations,
+                                                     AisleriotAnimStart, 0);
+
+          n_unexposed_animated_cards = (slot->cards->len - slot->old_cards->len
+                                        - slot->exposed);
+
+          if (n_unexposed_animated_cards > 0)
+            {
+              /* Set the bottom card of the first animation to be the
+                 lowest unexposed card */
+              anim->old_card
+                = CARD (slot->cards->data[slot->cards->len
+                                          - animations->len
+                                          - n_unexposed_animated_cards]);
+              anim->old_card.attr.face_down = !anim->old_card.attr.face_down;
+            }
+        }
+    }
+
+    aisleriot_slot_renderer_set_animations
+      (AISLERIOT_SLOT_RENDERER (slot->slot_renderer),
+       animations->len, (const AisleriotAnimStart *) animations->data,
+       n_unexposed_animated_cards);
+
+    if (slot->cards->len == 0) {
+      clutter_actor_lower_bottom (slot->slot_renderer);
+    } else {
+      clutter_actor_raise_top (slot->slot_renderer);
+      ClutterActor *animation_layer = CLUTTER_ACTOR(aisleriot_slot_renderer_get_animation_layer(AISLERIOT_SLOT_RENDERER(slot->slot_renderer)));
+      clutter_actor_raise_top (animation_layer);
+    }
+
+    /* Set the old cards back to the new cards */
+    aisleriot_game_reset_old_cards (slot);
+  }
+
+  g_array_set_size (priv->removed_cards, 0);
+
+  g_array_free (animations, TRUE);
+
+  priv->check_animations_handler = 0;
+
+  return FALSE;
+}
+
+static void
+queue_check_animations (AisleriotBoard *board)
+{
+  AisleriotBoardPrivate *priv = board->priv;
+
+  if (!ar_style_get_enable_animations (priv->style))
+    return;
+
+  /* The animations are checked for in an idle handler so that this
+     function can be called whenever the scheme script makes changes
+     to the board but it won't actually check until control has been
+     given back to the glib main loop */
+
+  if (priv->check_animations_handler == 0)
+    /* Check for animations with a high priority to ensure that the
+       animations are setup before the stage is repainted. Clutter's
+       redraw priority is unfortunately higher than all of the idle
+       priorities so we need to use G_PRIORITY_DEFAULT */
+    priv->check_animations_handler =
+      clutter_threads_add_idle_full (G_PRIORITY_DEFAULT,
+                                     check_animations_cb,
+                                     board, NULL);
+}
+
+static void
+slot_update_card_images_full (AisleriotBoard *board,
+                              ArSlot *slot,
+                              gint highlight_start_card_id)
+{
+  AisleriotBoardPrivate *priv = board->priv;
+
+  if (!priv->geometry_set)
+    return;
+
+  if (slot->slot_renderer == NULL) {
+    slot->slot_renderer = aisleriot_slot_renderer_new (priv->style, priv->textures, slot);
+    g_object_ref_sink (slot->slot_renderer);
+
+    aisleriot_slot_renderer_set_animation_layer
+      (AISLERIOT_SLOT_RENDERER (slot->slot_renderer),
+       CLUTTER_CONTAINER (priv->animation_layer));
+
+    clutter_actor_set_position (slot->slot_renderer,
+                                slot->rect.x, slot->rect.y);
+
+    clutter_container_add (CLUTTER_CONTAINER (board),
+                           slot->slot_renderer, NULL);
+
+    clutter_actor_raise_top (priv->animation_layer);
+  }
+
+  aisleriot_slot_renderer_set_animations
+    (AISLERIOT_SLOT_RENDERER (slot->slot_renderer), 0, NULL, 0);
+
+  aisleriot_slot_renderer_set_highlight
+    (AISLERIOT_SLOT_RENDERER (slot->slot_renderer),
+     priv->show_highlight ? highlight_start_card_id : G_MAXINT);
+}
+
+static void
+slot_update_card_images (AisleriotBoard *board,
+                         ArSlot *slot)
+{
+  AisleriotBoardPrivate *priv = board->priv;
+  int highlight_start_card_id = G_MAXINT;
+
+  if (G_UNLIKELY (slot == priv->highlight_slot &&
+                  priv->show_highlight)) {
+    highlight_start_card_id = slot->cards->len - 1;
+  } else if (G_UNLIKELY (slot == priv->selection_slot &&
+                         priv->selection_start_card_id >= 0 &&
+                         priv->show_selection)) {
+    highlight_start_card_id = priv->selection_start_card_id;
+  }
+
+  slot_update_card_images_full (board, slot, highlight_start_card_id);
+}
+
+/* helper functions */
+
+static void
+aisleriot_board_error_bell (AisleriotBoard *board)
+{
+  g_signal_emit (board, signals[ERROR_BELL], 0);
+}
+
+/* Work out new sizes and spacings for the cards. */
+static void
+aisleriot_board_setup_geometry (AisleriotBoard *board)
+{
+  AisleriotBoardPrivate *priv = board->priv;
+  ClutterActor *actor = CLUTTER_ACTOR (board);
+  ArCardTheme *theme;
+  GPtrArray *slots;
+  guint i, n_slots;
+  CardSize card_size;
+
+  if (!CLUTTER_ACTOR_IS_REALIZED (actor))
+    return;
+
+  /* Nothing to do yet */
+  if (aisleriot_game_get_state (priv->game) <= GAME_LOADED)
+    return;
+
+  theme = ar_style_get_card_theme (priv->style);
+  if (theme == NULL)
+    return;
+
+  g_return_if_fail (priv->width > 0 && priv->height > 0);
+
+  // FIXMEchpe
+  priv->xslotstep = ((double) priv->allocation.x2 - priv->allocation.x1) / priv->width;
+  priv->yslotstep = ((double) priv->allocation.y2 - priv->allocation.y1) / priv->height;
+
+  ar_card_theme_set_size (theme,
+                             priv->xslotstep,
+                             priv->yslotstep,
+                             priv->card_slot_ratio);
+
+  ar_card_theme_get_size (theme, &card_size);
+  priv->card_size = card_size;
+
+  /* If the cards are too far apart, bunch them in the middle. */
+  priv->xbaseoffset = 0;
+  if (priv->xslotstep > (card_size.width * 3) / 2) {
+    priv->xslotstep = (card_size.width * 3) / 2;
+    /* FIXMEchpe: if there are expand-right slots, reserve the space for them instead? */
+#ifdef FIXMEchpe
+    priv->xbaseoffset = (widget->allocation.width - priv->xslotstep * priv->width) / 2;
+#else
+    priv->xbaseoffset = (priv->allocation.x2 - priv->allocation.x1 - priv->xslotstep * priv->width) / 2;
+#endif
+  }
+  if (priv->yslotstep > (card_size.height * 3) / 2) {
+    priv->yslotstep = (card_size.height * 3) / 2;
+    /* FIXMEchpe: if there are expand-down slots, reserve the space for them instead?
+       priv->ybaseoffset = (widget->allocation.height - priv->yslotstep * priv->height) / 2;
+    */
+  }
+
+  priv->xoffset = (priv->xslotstep - card_size.width) / 2;
+  priv->yoffset = (priv->yslotstep - card_size.height) / 2;
+
+  /* NOTE! Updating the slots checks that geometry is set, so
+   * we set it to TRUE already.
+   */
+  priv->geometry_set = TRUE;
+
+  /* Now recalculate the slot locations. */
+  slots = aisleriot_game_get_slots (priv->game);
+
+  n_slots = slots->len;
+  for (i = 0; i < n_slots; ++i) {
+    ArSlot *slot = slots->pdata[i];
+
+    slot_update_geometry (board, slot);
+    slot_update_card_images (board, slot);
+  }
+
+  /* Update the focus and selection rects */
+  get_focus_rect (board, &priv->focus_rect);
+}
+
+static void
+drag_begin (AisleriotBoard *board)
+{
+  AisleriotBoardPrivate *priv = board->priv;
+  ArSlot *hslot;
+  int delta, height, width;
+  int x, y;
+  int num_moving_cards;
+  guint i;
+  GByteArray *cards;
+
+  if (!priv->selection_slot ||
+      priv->selection_start_card_id < 0) {
+    priv->click_status = STATUS_NONE;
+    return;
+  }
+
+  priv->click_status = STATUS_IS_DRAG;
+
+  hslot = priv->moving_cards_origin_slot = priv->selection_slot;
+  priv->moving_cards_origin_card_id = priv->selection_start_card_id;
+
+  num_moving_cards = hslot->cards->len - priv->moving_cards_origin_card_id;
+
+  cards = hslot->cards;
+
+  /* Save game state */
+  aisleriot_game_record_move (priv->game, hslot->id,
+                              cards->data, cards->len);
+
+  /* Unset the selection and focus. It'll be re-set if the drag is aborted */
+  set_selection (board, NULL, -1, FALSE);
+  set_focus (board, NULL, -1, FALSE);
+
+  delta = hslot->exposed - num_moving_cards;
+
+  /* (x,y) is the upper left edge of the topmost dragged card */
+  x = hslot->rect.x + delta * hslot->pixeldx;
+  if (priv->is_rtl &&
+      hslot->expanded_right) {
+    x += hslot->rect.width - priv->card_size.width;
+  }
+
+  priv->last_click_x -= x;
+  priv->last_click_y -= y = hslot->rect.y + delta * hslot->pixeldy;;
+
+  g_byte_array_set_size (priv->moving_cards, 0);
+  g_byte_array_append (priv->moving_cards,
+                       cards->data + priv->moving_cards_origin_card_id,
+                       cards->len - priv->moving_cards_origin_card_id);
+
+  width = priv->card_size.width + (num_moving_cards - 1) * hslot->pixeldx;
+  height = priv->card_size.height + (num_moving_cards - 1) * hslot->pixeldy;
+
+  priv->moving_cards_group = g_object_ref_sink (clutter_group_new ());
+  clutter_actor_set_position (priv->moving_cards_group, x, y);
+
+  /* FIXMEchpe: RTL issue: this doesn't work right when we allow dragging of
+   * more than one card from a expand-right slot. (But right now no game .scm
+   * does allow that.)
+   */
+  x = y = 0;
+  width = priv->card_size.width;
+  height = priv->card_size.height;
+
+  for (i = 0; i < priv->moving_cards->len; ++i) {
+    Card hcard = CARD (priv->moving_cards->data[i]);
+    ClutterActor *card_tex;
+    RemovedCard removed_card;
+
+    removed_card.cardx = x;
+    removed_card.cardy = y;
+    removed_card.card = hcard;
+    removed_card.from_drag = TRUE;
+
+    g_array_append_val (priv->removed_cards, removed_card);
+
+    card_tex = aisleriot_card_new (priv->textures, hcard, hcard);
+    clutter_actor_set_position (card_tex, x, y);
+    clutter_container_add (CLUTTER_CONTAINER (priv->moving_cards_group),
+                           card_tex, NULL);
+
+    x += hslot->pixeldx;
+    y += hslot->pixeldy;
+  }
+
+  /* Take the cards off of the stack */
+  g_byte_array_set_size (cards, priv->moving_cards_origin_card_id);
+
+  slot_update_geometry (board, hslot);
+  slot_update_card_images (board, hslot);
+  aisleriot_game_reset_old_cards (hslot);
+
+  clutter_container_add (CLUTTER_CONTAINER (board),
+                         priv->moving_cards_group, NULL);
+
+  if (hslot->cards->len == 0) {
+    clutter_actor_lower_bottom (hslot->slot_renderer);
+  }
+
+  set_cursor (board, AR_CURSOR_CLOSED);
+}
+
+static void
+drag_end (AisleriotBoard *board,
+          gboolean moved)
+{
+  AisleriotBoardPrivate *priv = board->priv;
+
+  if (priv->moving_cards_group != NULL) {
+    clutter_actor_destroy (priv->moving_cards_group);
+    g_object_unref (priv->moving_cards_group);
+    priv->moving_cards_group = NULL;
+  }
+
+  /* FIXMEchpe: check that slot->cards->len == moving_cards_origin_card_id !!! FIXMEchpe what to do if not, abort the game? */
+  /* Add the origin cards back to the origin slot */
+  if (!moved &&
+      priv->moving_cards_origin_slot != NULL &&
+      priv->moving_cards->len > 0) {
+    aisleriot_game_slot_add_cards (priv->game,
+                                   priv->moving_cards_origin_slot,
+                                   priv->moving_cards->data,
+                                   priv->moving_cards->len);
+    clutter_actor_raise_top (priv->moving_cards_origin_slot->slot_renderer);
+    ClutterActor *animation_layer = CLUTTER_ACTOR(aisleriot_slot_renderer_get_animation_layer(AISLERIOT_SLOT_RENDERER(priv->moving_cards_origin_slot->slot_renderer)));
+    clutter_actor_raise_top (animation_layer);
+  }
+
+  priv->click_status = STATUS_NONE;
+  priv->moving_cards_origin_slot = NULL;
+  priv->moving_cards_origin_card_id = -1;
+}
+
+static gboolean
+cards_are_droppable (AisleriotBoard *board,
+                     ArSlot *slot)
+{
+  AisleriotBoardPrivate *priv = board->priv;
+
+  return slot != NULL &&
+         priv->moving_cards_origin_slot &&
+         aisleriot_game_drop_valid (priv->game,
+                                    priv->moving_cards_origin_slot->id,
+                                    slot->id,
+                                    priv->moving_cards->data,
+                                    priv->moving_cards->len);
+}
+
+static ArSlot *
+find_drop_target (AisleriotBoard *board,
+                  gint x,
+                  gint y)
+{
+  AisleriotBoardPrivate *priv = board->priv;
+  ArSlot *new_hslot;
+  ArSlot *retval = NULL;
+  gint i, new_cardid;
+  gint min_distance = G_MAXINT;
+
+  /* Find a target directly under the center of the card. */
+  get_slot_and_card_from_point (board,
+                                x + priv->card_size.width / 2,
+                                y + priv->card_size.height / 2,
+                                &new_hslot, &new_cardid);
+
+  if (cards_are_droppable (board, new_hslot))
+    return new_hslot;
+
+  /* If that didn't work, look for a target at all 4 corners of the card. */
+  for (i = 0; i < 4; i++) {
+    get_slot_and_card_from_point (board,
+                                  x + priv->card_size.width * (i / 2),
+                                  y + priv->card_size.height * (i % 2),
+                                  &new_hslot, &new_cardid);
+
+    if (!new_hslot)
+      continue;
+
+    /* This skips corners we know are not droppable. */
+    if (!priv->droppable_supported || cards_are_droppable (board, new_hslot)) {
+      gint dx, dy, distance_squared;
+
+      dx = new_hslot->rect.x + (new_cardid - 1) * new_hslot->pixeldx - x;
+      dy = new_hslot->rect.y + (new_cardid - 1) * new_hslot->pixeldy - y;
+
+      distance_squared = dx * dx + dy * dy;
+
+      if (distance_squared <= min_distance) {
+	retval = new_hslot;
+	min_distance = distance_squared;
+      }
+    }
+  }
+
+  return retval;
+}
+
+static void
+drop_moving_cards (AisleriotBoard *board,
+                   gint x,
+                   gint y)
+{
+  AisleriotBoardPrivate *priv = board->priv;
+  ArSlot *hslot;
+  gboolean moved = FALSE;
+  guint i;
+
+  hslot = find_drop_target (board,
+                            x - priv->last_click_x,
+                            y - priv->last_click_y);
+
+  /* Reposition the removed cards so that they are relative to the
+     cursor position */
+  for (i = 0; i < priv->removed_cards->len; i++) {
+    RemovedCard *removed_card = &g_array_index (priv->removed_cards,
+                                                RemovedCard, i);
+
+    if (removed_card->from_drag) {
+      removed_card->cardx += x - priv->last_click_x;
+      removed_card->cardy += y - priv->last_click_y;
+    }
+  }
+
+  if (hslot) {
+    moved = aisleriot_game_drop_cards (priv->game,
+                                       priv->moving_cards_origin_slot->id,
+                                       hslot->id,
+                                       priv->moving_cards->data,
+                                       priv->moving_cards->len);
+  }
+
+  if (moved) {
+    aisleriot_game_end_move (priv->game);
+    games_sound_play ("click");
+  } else {
+    aisleriot_game_discard_move (priv->game);
+    games_sound_play ("slide");
+  }
+
+  drag_end (board, moved);
+
+  if (moved)
+    aisleriot_game_test_end_of_game (priv->game);
+}
+
+static void
+highlight_drop_target (AisleriotBoard *board,
+                       ArSlot *slot)
+{
+  AisleriotBoardPrivate *priv = board->priv;
+  ArSlot *old_slot = priv->highlight_slot;
+
+  if (slot == old_slot)
+    return;
+
+  /* Need to set the highlight slot even when we the cards aren't droppable
+   * since that can happen when the game doesn't support FEATURE_DROPPABLE.
+   */
+  priv->highlight_slot = slot;
+
+  /* Invalidate the old highlight rect */
+  if (old_slot != NULL &&
+      priv->show_highlight) {
+    /* FIXMEchpe only update the topmost card? */
+    /* It's ok to call this directly here, since the old highlight_slot cannot
+     * have been the same as the current selection_slot!
+     */
+    slot_update_card_images_full (board, old_slot, G_MAXINT);
+  }
+
+  if (!cards_are_droppable (board, slot))
+    return;
+
+  if (!priv->show_highlight)
+    return;
+
+  /* Prepare the highlight pixbuf/pixmaps */
+
+  /* FIXMEchpe only update the topmost card? */
+  /* It's ok to call this directly, since the highlight slot is always
+   * different from the selection slot!
+   */
+  slot_update_card_images_full (board, slot, ((int) slot->cards->len) - 1);
+}
+
+static void
+reveal_card (AisleriotBoard *board,
+             ArSlot *slot,
+             int cardid)
+{
+  AisleriotBoardPrivate *priv = board->priv;
+  Card card;
+  AisleriotSlotRenderer *renderer;
+
+  if (priv->show_card_slot == slot)
+    return;
+
+  if (priv->show_card_slot != NULL) {
+    if (priv->show_card_slot->slot_renderer) {
+      renderer = AISLERIOT_SLOT_RENDERER (priv->show_card_slot->slot_renderer);
+      aisleriot_slot_renderer_set_revealed_card (renderer, -1);
+    }
+    priv->show_card_slot = NULL;
+    priv->show_card_id = -1;
+    priv->click_status = STATUS_NONE;
+  }
+
+  if (!slot || cardid < 0 || cardid >= slot->cards->len - 1)
+    return;
+
+  card = CARD (slot->cards->data[cardid]);
+  if (CARD_GET_FACE_DOWN (card))
+    return;
+
+  priv->show_card_slot = slot;
+  priv->show_card_id = cardid;
+  priv->click_status = STATUS_SHOW;
+
+  renderer = AISLERIOT_SLOT_RENDERER (slot->slot_renderer);
+  aisleriot_slot_renderer_set_revealed_card (renderer, cardid);
+}
+
+static void
+clear_state (AisleriotBoard *board)
+{
+  AisleriotBoardPrivate *priv = board->priv;
+
+  highlight_drop_target (board, NULL);
+  drag_end (board, FALSE /* FIXMEchpe ? */);
+
+  reveal_card (board, NULL, -1);
+
+  priv->click_status = STATUS_NONE;
+  priv->last_clicked_slot = NULL;
+  priv->last_clicked_card_id = -1;
+}
+
+/* Note: this unsets the selection! */
+static gboolean
+aisleriot_board_move_selected_cards_to_slot (AisleriotBoard *board,
+                                             ArSlot *hslot)
+{
+  AisleriotBoardPrivate *priv = board->priv;
+  ArSlot *selection_slot = priv->selection_slot;
+  int selection_start_card_id = priv->selection_start_card_id;
+  gboolean moved;
+  guint8 *cards;
+  guint n_cards;
+
+  if (!selection_slot ||
+      priv->selection_start_card_id < 0)
+    return FALSE;
+
+  /* NOTE: We cannot use aisleriot_game_drop_valid here since the
+   * game may not support the "droppable" feature.
+   */
+
+  set_selection (board, NULL, -1, FALSE);
+
+  priv->click_status = STATUS_NONE;
+
+  aisleriot_game_record_move (priv->game,
+                              selection_slot->id,
+                              selection_slot->cards->data,
+                              selection_slot->cards->len);
+
+  /* Store the cards, since the move could alter slot->cards! */
+  g_assert (selection_slot->cards->len >= selection_start_card_id);
+  n_cards = selection_slot->cards->len - selection_start_card_id;
+
+  cards = g_alloca (n_cards);
+  memcpy (cards,
+          selection_slot->cards->data + selection_start_card_id,
+          n_cards);
+
+  /* Now take the cards off of the origin slot. We'll update the slot geometry later */
+  g_byte_array_set_size (selection_slot->cards, selection_start_card_id);
+  selection_slot->needs_update = TRUE;
+
+  moved = aisleriot_game_drop_cards (priv->game,
+                                      selection_slot->id,
+                                      hslot->id,
+                                      cards,
+                                      n_cards);
+  if (moved) {
+    aisleriot_game_end_move (priv->game);
+
+    games_sound_play ("click");
+
+    if (selection_slot->needs_update)
+      g_signal_emit_by_name (priv->game, "slot-changed", selection_slot); /* FIXMEchpe! */
+
+    aisleriot_game_test_end_of_game (priv->game);
+  } else {
+    /* Not moved; discard the move add the cards back to the origin slot */
+    aisleriot_game_discard_move (priv->game);
+    aisleriot_game_slot_add_cards (priv->game, selection_slot, cards, n_cards);
+  }
+
+  return moved;
+}
+
+/* Keynav */
+
+#ifdef ENABLE_KEYNAV
+
+/** Keynav specification:
+ *
+ * Focus state consists of the slot that has the focus, and the card
+ * in that slot of the card that has the focus; if the slot has no cards
+ * the focus is on the empty slot itself.
+ *
+ * Without modifiers, for focus movement:
+ * Left (Right): For right-extended slots, moves the focus to the card
+ *   under (over) the currently focused card on the same slot.
+ *   If the focused card is already the bottommost (topmost)
+ *   card of the slot, or the slot is not right-extended, moves the focus
+ *   to the topmost (bottommost) card on the slot left (right) to the currently
+ *   focused slot (wraps around from first to last slot and vv.)
+ * Up (Down): For down-extended slots, moves the focus to the card
+ *   under (over) the currently focused card on the same slot.
+ *   If the focused card is already the bottommost (topmost) card
+ *   of the slot, or the slot is not down-extended, moves the focus to
+ *   the topmost (bottommost) card of the slot over (under) the currently
+ *   focused slot (wraps around vertically)
+ * Home (End): For down- or right-extended slots, moves the focus to the
+ *   start (end) of the stack of face-up cards on the currently focused slot.
+ *   If the focus is already on that card, moves the focus over a stack of
+ *   face-down cards into the next stack of face-up cards (e.g. Athena), or
+ *   the bottommost (topmost) card on the slot if there are no such cards.
+ * PageUp, PageDown: Acts like <control>Up, <control>Down
+ * Space: selects the cards from the currently focused card to the topmost
+ *   card of the slot, if this is allowed by the game. If the focused card is
+ *   the first selected card already, unsets the selection.
+ * Return: Performs the button press action on the focused card.
+ *   If no action was performed by that, moves the selected card(s) to the top
+ *   of the focused slot, if this is allowed by the game.
+ *
+ * With <control>, for focus movement:
+ * Left (Right): Moves the focus to the bottommost (topmost) card on the
+ *   slot; if that card is already focused, moves the focus to the
+ *   topmost/bottommost card on the slot left/right to the currently
+ *   focused slot (wraps around from first to last slot and vv.)
+ * Up (Down): Moves the focus to the topmost (bottommost) card of the slot
+ *   over (under) the currently focused slot (wraps around vertically)
+ * Home (End): moves the focus to the bottommost (topmost) card on the
+ *   first (last) slot
+ * Return: Performs the double-click action on the focused card
+ *
+ * With <shift>: extends the selection; focus movement itself occurs
+ *   like for the same key without modifiers.
+ * Left (Right): for right-extended slots, extends (shrinks) the selection
+ *   by one card, if this is allowed by the game
+ * Up (Down): for down-extended slots, extends (shrinks) the selection
+ *   by one card, if this is allowed by the game
+ * Home: extends the selection maximally in the focused slot, as allowed
+ *   by the game
+ * End: shrinks the selection into nonexistence
+ * 
+ * With <control><shift>: extends selection like with <shift> alone,
+ *   and moves focus like with <control> alone
+ *
+ * Other keyboard shortcuts:
+ * <control>A: extends the selection maximally in the focused slot, as allowed
+ *   by the game
+ * <shift><control>A: unsets the selection
+ *
+ * Notes:
+ * If no slot is currently focused:
+ * Right, Up, Down, PgUp, PgDown, Home: moves the focus to the bottommost card on the first
+ *   slot
+ * Left, End: moves the focus to the topmost card on the last slot
+ */
+
+static void
+aisleriot_board_add_move_binding (ClutterBindingPool *binding_pool,
+                                  GClosure           *closure,
+                                  const char         *action,
+                                  guint               keyval,
+                                  ClutterModifierType modifiers)
+{
+  clutter_binding_pool_install_closure (binding_pool,
+                                        action,
+                                        keyval,
+                                        modifiers,
+                                        closure);
+
+  if (modifiers & CLUTTER_CONTROL_MASK)
+    return;
+
+  clutter_binding_pool_install_closure (binding_pool,
+                                        action,
+                                        keyval,
+                                        modifiers | CLUTTER_CONTROL_MASK,
+                                        closure);
+}
+
+static void
+aisleriot_board_add_move_and_select_binding (ClutterBindingPool *binding_pool,
+                                             GClosure           *closure,
+                                             const char         *action,
+                                             guint               keyval,
+                                             ClutterModifierType modifiers)
+{
+  aisleriot_board_add_move_binding (binding_pool, closure, action, keyval, modifiers);
+  aisleriot_board_add_move_binding (binding_pool, closure, action, keyval, modifiers | CLUTTER_SHIFT_MASK);
+}
+
+static void
+aisleriot_board_add_activate_binding (ClutterBindingPool *binding_pool,
+                                      GClosure           *closure,
+                                      guint               keyval,
+                                      ClutterModifierType modifiers)
+{
+  clutter_binding_pool_install_closure (binding_pool,
+                                        I_("activate"),
+                                        keyval,
+                                        modifiers,
+                                        closure);
+
+  if (modifiers & CLUTTER_CONTROL_MASK)
+    return;
+
+  clutter_binding_pool_install_closure (binding_pool,
+                                        I_("activate"),
+                                        keyval,
+                                        modifiers | CLUTTER_CONTROL_MASK,
+                                        closure);
+}
+
+static gboolean
+aisleriot_board_move_cursor_in_slot (AisleriotBoard *board,
+                                     int count)
+{
+  AisleriotBoardPrivate *priv = board->priv;
+  ArSlot *focus_slot;
+  int new_focus_card_id, first_card_id;
+
+  focus_slot = priv->focus_slot;
+  first_card_id = ((int) focus_slot->cards->len) - ((int) focus_slot->exposed);
+  new_focus_card_id = priv->focus_card_id + count;
+  if (new_focus_card_id < first_card_id || new_focus_card_id >= (int) focus_slot->cards->len)
+    return FALSE;
+
+  set_focus (board, focus_slot, new_focus_card_id, TRUE);
+  return TRUE;
+}
+
+static gboolean
+aisleriot_board_move_cursor_start_end_in_slot (AisleriotBoard *board,
+                                               int count)
+{
+  AisleriotBoardPrivate *priv = board->priv;
+  ArSlot *focus_slot = priv->focus_slot;
+  int first_card_id, top_card_id, new_focus_card_id;
+  guint8 *cards;
+
+  if (focus_slot->cards->len == 0)
+    return FALSE;
+
+  g_assert (priv->focus_card_id >= 0);
+
+  /* Moves the cursor to the first/last card above/below a face-down
+   * card, or the start/end of the slot if there are no face-down cards
+   * between the currently focused card and the slot start/end.
+   * (Jumping over face-down cards and landing on a non-face-down card
+   * happens e.g. in Athena.)
+   */
+  cards = focus_slot->cards->data;
+  top_card_id = ((int) focus_slot->cards->len) - 1;
+  first_card_id = ((int) focus_slot->cards->len) - ((int) focus_slot->exposed);
+  new_focus_card_id = priv->focus_card_id;
+
+  /* Set new_focus_card_id to the index of the last face-down card
+    * in the run of face-down cards.
+    */
+  do {
+    new_focus_card_id += count;
+  } while (new_focus_card_id >= first_card_id &&
+           new_focus_card_id <= top_card_id &&
+           CARD_GET_FACE_DOWN (((Card) cards[new_focus_card_id])));
+
+  /* We went one too far */
+  new_focus_card_id -= count;
+
+  /* Now get to the start/end of the run of face-up cards */
+  do {
+    new_focus_card_id += count;
+  } while (new_focus_card_id >= first_card_id &&
+           new_focus_card_id <= top_card_id &&
+           !CARD_GET_FACE_DOWN (((Card) cards[new_focus_card_id])));
+
+  if (new_focus_card_id < first_card_id ||
+      new_focus_card_id > top_card_id ||
+      CARD_GET_FACE_DOWN (((Card) cards[new_focus_card_id]))) {
+    /* We went one too far */
+    new_focus_card_id -= count;
+  }
+
+  new_focus_card_id = CLAMP (new_focus_card_id, first_card_id, top_card_id);
+  set_focus (board, focus_slot, new_focus_card_id, TRUE);
+
+  return TRUE;
+}
+
+static gboolean
+aisleriot_board_extend_selection_in_slot (AisleriotBoard *board,
+                                          int count)
+{
+  AisleriotBoardPrivate *priv = board->priv;
+  ArSlot *focus_slot, *selection_slot;
+  int new_selection_start_card_id, first_card_id;
+
+  focus_slot = priv->focus_slot;
+  selection_slot = priv->selection_slot;
+  first_card_id = ((int) focus_slot->cards->len) - ((int) focus_slot->exposed);
+
+  if (selection_slot == focus_slot) {
+    new_selection_start_card_id = priv->selection_start_card_id + count;
+
+    /* Can only extend the selection if the focus is adjacent to the selection */
+    if (priv->focus_card_id - 1 > new_selection_start_card_id ||
+        priv->focus_card_id + 1 < new_selection_start_card_id)
+      return FALSE;
+  } else {
+    /* No selection yet */
+    new_selection_start_card_id = ((int) focus_slot->cards->len) + count;
+
+    /* Must have the topmost card focused */
+    if (new_selection_start_card_id != priv->focus_card_id)
+      return FALSE;
+  }
+
+  if (new_selection_start_card_id < first_card_id)
+    return FALSE;
+
+  /* If it's the top card, unselect all */
+  if (new_selection_start_card_id >= focus_slot->cards->len) {
+    set_selection (board, NULL, -1, FALSE);
+    return TRUE;
+  }
+    
+  if (!aisleriot_game_drag_valid (priv->game,
+                                  focus_slot->id,
+                                  focus_slot->cards->data + new_selection_start_card_id,
+                                  focus_slot->cards->len - new_selection_start_card_id))
+    return FALSE;
+
+  set_selection (board, focus_slot, new_selection_start_card_id, TRUE);
+
+  /* Try to move the cursor too, but don't beep if that fails */
+  aisleriot_board_move_cursor_in_slot (board, count);
+  return TRUE;
+}
+
+static gboolean
+aisleriot_board_extend_selection_in_slot_maximal (AisleriotBoard *board)
+{
+  AisleriotBoardPrivate *priv = board->priv;
+  ArSlot *focus_slot = priv->focus_slot;
+  int new_selection_start_card_id, n_selected;
+
+  n_selected = 0;
+  new_selection_start_card_id = ((int) focus_slot->cards->len) - 1;
+  while (new_selection_start_card_id >= 0) {
+    if (!aisleriot_game_drag_valid (priv->game,
+                                    focus_slot->id,
+                                    focus_slot->cards->data + new_selection_start_card_id,
+                                    focus_slot->cards->len - new_selection_start_card_id))
+      break;
+
+    ++n_selected;
+    --new_selection_start_card_id;
+  }
+
+  if (n_selected == 0)
+    return FALSE;
+
+  set_selection (board, focus_slot, new_selection_start_card_id + 1, TRUE);
+  return TRUE;
+}
+
+static gboolean
+aisleriot_board_move_cursor_left_right_by_slot (AisleriotBoard *board,
+                                                int count,
+                                                gboolean wrap)
+{
+  AisleriotBoardPrivate *priv = board->priv;
+  GPtrArray *slots;
+  guint n_slots;
+  ArSlot *focus_slot, *new_focus_slot;
+  int focus_slot_index, new_focus_slot_index;
+  int new_focus_slot_topmost_card_id, new_focus_card_id;
+  gboolean is_rtl;
+
+  slots = aisleriot_game_get_slots (priv->game);
+  if (!slots || slots->len == 0)
+    return FALSE;
+
+  n_slots = slots->len;
+
+  focus_slot = priv->focus_slot;
+  g_assert (focus_slot != NULL);
+
+  focus_slot_index = get_slot_index_from_slot (board, focus_slot);
+
+  /* Move visually */
+  is_rtl = priv->is_rtl;
+  if (priv->is_rtl) {
+    new_focus_slot_index = focus_slot_index - count;
+  } else {
+    new_focus_slot_index = focus_slot_index + count;
+  }
+
+  /* Wrap-around? */
+  if (new_focus_slot_index < 0 ||
+      new_focus_slot_index >= n_slots) {
+    if (!wrap)
+      return FALSE;
+
+#ifdef FIXMEchpe
+{
+    GtkDirectionType direction;
+#if GTK_CHECK_VERSION (2, 12, 0)
+
+    if (count > 0) {
+      direction = GTK_DIR_RIGHT;
+    } else {
+      direction = GTK_DIR_LEFT;
+    }
+
+    if (!gtk_widget_keynav_failed (widget, direction)) {
+       return gtk_widget_child_focus (gtk_widget_get_toplevel (widget), direction);
+    }
+#endif /* GTK 2.12. 0 */
+}
+#endif // FIXMEchpe
+
+    if (new_focus_slot_index < 0) {
+      new_focus_slot_index = ((int) n_slots) - 1;
+    } else {
+      new_focus_slot_index = 0;
+    }
+  }
+
+  g_assert (new_focus_slot_index >= 0 && new_focus_slot_index < n_slots);
+
+  new_focus_slot = slots->pdata[new_focus_slot_index];
+  new_focus_slot_topmost_card_id = ((int) new_focus_slot->cards->len) - 1;
+
+  if (new_focus_slot->expanded_right) {
+    if ((is_rtl && count < 0) ||
+        (!is_rtl && count > 0)) {
+      if (new_focus_slot->cards->len > 0) {
+        new_focus_card_id = ((int) new_focus_slot->cards->len) - ((int) new_focus_slot->exposed);
+      } else {
+        new_focus_card_id = -1;
+      }
+    } else {
+      new_focus_card_id = new_focus_slot_topmost_card_id;
+    }
+  } else {
+    /* Just take the topmost card */
+    new_focus_card_id = new_focus_slot_topmost_card_id;
+  }
+
+  set_focus (board, new_focus_slot, new_focus_card_id, TRUE);
+  return TRUE;
+}
+
+static gboolean
+aisleriot_board_move_cursor_up_down_by_slot (AisleriotBoard *board,
+                                             int count)
+{
+  AisleriotBoardPrivate *priv = board->priv;
+  GPtrArray *slots;
+  guint n_slots;
+  ArSlot *focus_slot, *new_focus_slot;
+  int focus_slot_index, new_focus_slot_index;
+  int new_focus_slot_topmost_card_id, new_focus_card_id;
+  int x_start, x_end;
+
+  slots = aisleriot_game_get_slots (priv->game);
+  if (!slots || slots->len == 0)
+    return FALSE;
+
+  n_slots = slots->len;
+
+  focus_slot = priv->focus_slot;
+  g_assert (focus_slot != NULL);
+
+  x_start = focus_slot->rect.x;
+  x_end = x_start + focus_slot->rect.width;
+
+  focus_slot_index = get_slot_index_from_slot (board, focus_slot);
+
+  new_focus_slot_index = focus_slot_index;
+  do {
+    new_focus_slot_index += count;
+  } while (new_focus_slot_index >= 0 &&
+           new_focus_slot_index < n_slots &&
+           !test_slot_projection_intersects_x (slots->pdata[new_focus_slot_index], x_start, x_end));
+
+  if (new_focus_slot_index < 0 || new_focus_slot_index == n_slots) {
+#ifdef FIXMEchpe
+#if GTK_CHECK_VERSION (2, 12, 0)
+    GtkWidget *widget = GTK_WIDGET (board);
+    GtkDirectionType direction;
+
+    if (count > 0) {
+      direction = GTK_DIR_DOWN;
+    } else {
+      direction = GTK_DIR_UP;
+    }
+
+    if (!gtk_widget_keynav_failed (widget, direction)) {
+       return gtk_widget_child_focus (gtk_widget_get_toplevel (widget), direction);
+    }
+#endif /* GTK 2.12. 0 */
+#endif // FIXMEchpe
+
+    /* Wrap around */
+    if (count > 0) {
+      new_focus_slot_index = -1;
+    } else {
+      new_focus_slot_index = n_slots;
+    }
+
+    do {
+      new_focus_slot_index += count;
+    } while (new_focus_slot_index != focus_slot_index &&
+             !test_slot_projection_intersects_x (slots->pdata[new_focus_slot_index], x_start, x_end));
+  }
+
+  g_assert (new_focus_slot_index >= 0 && new_focus_slot_index < n_slots);
+
+  new_focus_slot = slots->pdata[new_focus_slot_index];
+  new_focus_slot_topmost_card_id = ((int) new_focus_slot->cards->len) - 1;
+
+  if (new_focus_slot->expanded_down) {
+    if (count > 0) {
+      if (new_focus_slot->cards->len > 0) {
+        new_focus_card_id = ((int) new_focus_slot->cards->len) - ((int) new_focus_slot->exposed);
+      } else {
+        new_focus_card_id = -1;
+      }
+    } else {
+      new_focus_card_id = new_focus_slot_topmost_card_id;
+    }
+  } else {
+    /* Just take the topmost card */
+    new_focus_card_id = new_focus_slot_topmost_card_id;
+  }
+
+  set_focus (board, new_focus_slot, new_focus_card_id, TRUE);
+  return TRUE;
+}
+
+static gboolean
+aisleriot_board_move_cursor_start_end_by_slot (AisleriotBoard *board,
+                                               int count)
+{
+  AisleriotBoardPrivate *priv = board->priv;
+  GPtrArray *slots;
+  ArSlot *new_focus_slot;
+  int new_focus_card_id;
+
+  slots = aisleriot_game_get_slots (priv->game);
+  if (!slots || slots->len == 0)
+    return FALSE;
+
+  if (count > 0) {
+    new_focus_slot = (ArSlot *) slots->pdata[slots->len - 1];
+    new_focus_card_id = ((int) new_focus_slot->cards->len) - 1;
+  } else {
+    new_focus_slot = (ArSlot *) slots->pdata[0];
+    if (new_focus_slot->cards->len > 0) {
+      new_focus_card_id = ((int) new_focus_slot->cards->len) - ((int) new_focus_slot->exposed);
+    } else {
+      new_focus_card_id = -1;
+    }
+  }
+
+  g_assert (new_focus_slot != NULL);
+  g_assert (new_focus_card_id >= -1);
+
+  set_focus (board, new_focus_slot, new_focus_card_id, TRUE);
+  return TRUE;
+}
+
+static gboolean
+aisleriot_board_move_cursor_left_right (AisleriotBoard *board,
+                                        int count,
+                                        gboolean is_control)
+{
+  AisleriotBoardPrivate *priv = board->priv;
+
+  /* First try in-slot focus movement */
+  if (!is_control &&
+      priv->focus_slot->expanded_right &&
+      aisleriot_board_move_cursor_in_slot (board, priv->is_rtl ? -count : count))
+    return TRUE;
+
+  /* Cannot move in-slot; move focused slot */
+  return aisleriot_board_move_cursor_left_right_by_slot (board, count, TRUE);
+}
+
+static gboolean
+aisleriot_board_move_cursor_up_down (AisleriotBoard *board,
+                                     int count,
+                                     gboolean is_control)
+{
+  AisleriotBoardPrivate *priv = board->priv;
+
+  g_assert (priv->focus_slot != NULL);
+
+  /* First try in-slot focus movement */
+  if (!is_control &&
+      priv->focus_slot->expanded_down &&
+      aisleriot_board_move_cursor_in_slot (board, count))
+    return TRUE;
+
+  /* Cannot move in-slot; move focused slot */
+  return aisleriot_board_move_cursor_up_down_by_slot (board, count);
+}
+
+static gboolean
+aisleriot_board_extend_selection_left_right (AisleriotBoard *board,
+                                             int count)
+{
+  AisleriotBoardPrivate *priv = board->priv;
+
+  if (!priv->focus_slot->expanded_right)
+    return FALSE;
+
+  return aisleriot_board_extend_selection_in_slot (board, count);
+}
+
+static gboolean
+aisleriot_board_extend_selection_up_down (AisleriotBoard *board,
+                                          int count)
+{
+  AisleriotBoardPrivate *priv = board->priv;
+
+  if (!priv->focus_slot->expanded_down)
+    return FALSE;
+
+  return aisleriot_board_extend_selection_in_slot (board, count);
+}
+
+static gboolean
+aisleriot_board_extend_selection_start_end (AisleriotBoard *board,
+                                            int count)
+{
+  AisleriotBoardPrivate *priv = board->priv;
+  ArSlot *focus_slot = priv->focus_slot;
+  int new_focus_card_id;
+
+  if (count > 0) {
+    /* Can only shrink the selection if the focus is on the selected slot,
+     * and the focused card is on or below the start of the selection.
+     */
+    if (priv->selection_slot == focus_slot &&
+        priv->selection_start_card_id >= priv->focus_card_id) {
+      set_selection (board, NULL, -1, FALSE);
+      new_focus_card_id = ((int) focus_slot->cards->len);
+    } else {
+      aisleriot_board_error_bell (board);
+      return FALSE;
+    }
+
+  } else {
+    if (!aisleriot_board_extend_selection_in_slot_maximal (board)) {
+      set_selection (board, NULL, -1, FALSE);
+      aisleriot_board_error_bell (board);
+      return FALSE;
+    }
+
+    new_focus_card_id = priv->selection_start_card_id;
+  }
+
+  set_focus (board, focus_slot, new_focus_card_id, TRUE);
+  return TRUE;
+}
+
+#endif /* ENABLE_KEYNAV */
+
+/* Game state handling */
+
+static void
+game_type_changed_cb (AisleriotGame *game,
+                      AisleriotBoard *board)
+{
+  AisleriotBoardPrivate *priv = board->priv;
+  guint features;
+
+  features = aisleriot_game_get_features (game);
+
+  priv->droppable_supported = ((features & FEATURE_DROPPABLE) != 0);
+  priv->show_highlight = priv->droppable_supported;
+}
+
+static void
+game_cleared_cb (AisleriotGame *game,
+                 AisleriotBoard *board)
+{
+  AisleriotBoardPrivate *priv = board->priv;
+
+  priv->geometry_set = FALSE;
+
+  /* So we don't re-add the cards to the now-dead slot */
+  priv->highlight_slot = NULL;
+  priv->last_clicked_slot = NULL;
+  priv->moving_cards_origin_slot = NULL;
+  priv->selection_slot = NULL;
+  priv->show_card_slot = NULL;
+
+  clear_state (board);
+}
+
+static void
+game_new_cb (AisleriotGame *game,
+             AisleriotBoard *board)
+{
+  AisleriotBoardPrivate *priv = board->priv;
+
+  clear_state (board);
+
+  set_focus (board, NULL, -1, FALSE);
+  set_selection (board, NULL, -1, FALSE);
+
+  aisleriot_game_get_geometry (game, &priv->width, &priv->height);
+
+  aisleriot_board_setup_geometry (board);
+
+#if 0
+  g_print ("{ %.3f , %.3f /* %s */ },\n",
+           priv->width, priv->height,
+           aisleriot_game_get_game_file (priv->game));
+#endif
+
+  /* Check for animations so that the differences will be reset */
+  queue_check_animations (board);
+}
+
+static void
+slot_changed_cb (AisleriotGame *game,
+                 ArSlot *slot,
+                 AisleriotBoard *board)
+{
+  AisleriotBoardPrivate *priv = board->priv;
+
+  slot_update_geometry (board, slot);
+  slot_update_card_images (board, slot);
+
+  if (slot == priv->moving_cards_origin_slot) {
+    /* PANIC! */
+    /* FIXMEchpe */
+  }
+  if (slot == priv->selection_slot) {
+    set_selection (board, NULL, -1, FALSE);
+
+    /* If this slot changes while we're in a click cycle, abort the action.
+     * That prevents a problem where the cards that were selected and
+     * about to be dragged have vanished because of autoplay; c.f. bug #449767.
+     * Note: we don't use clear_state() here since we only want to disable
+     * the highlight and revealed card if that particular slot changed, see
+     * the code above. And we don't clear last_clicked_slot/card_id either, so
+     * a double-click will still work.
+     */
+    priv->click_status = STATUS_NONE;
+  }
+  if (slot == priv->focus_slot) {
+    /* Try to keep the focus intact. If the focused card isn't there
+     * anymore, this will set the focus to the topmost card of there
+     * same slot, or the slot itself if there are no cards on it.
+     * If the slot was empty but now isn't, we set the focus to the
+     * topmost card.
+     */
+    if (priv->focus_card_id < 0) {
+      set_focus (board, slot, ((int) slot->cards->len) - 1, priv->show_focus);
+    } else {
+      set_focus (board, slot, priv->focus_card_id, priv->show_focus);
+    }
+  }
+  if (slot == priv->highlight_slot) {
+    highlight_drop_target (board, NULL);
+  }
+
+  queue_check_animations (board);
+}
+
+/* Style handling */
+
+static void
+aisleriot_board_sync_style (ArStyle *style,
+                            GParamSpec *pspec,
+                            AisleriotBoard *board)
+{
+  AisleriotBoardPrivate *priv = board->priv;
+  const char *pspec_name;
+  gboolean update_geometry = FALSE, redraw_focus = FALSE;
+
+  g_assert (style == priv->style);
+
+  if (pspec != NULL) {
+    pspec_name = pspec->name;
+  } else {
+    pspec_name = NULL;
+  }
+
+  if (pspec_name == NULL || pspec_name == I_(AR_STYLE_PROP_CARD_THEME)) {
+    ArCardTheme *theme;
+
+    theme = ar_style_get_card_theme (style);
+    if (theme != NULL) {
+      ar_card_textures_cache_set_theme (priv->textures, theme);
+
+      priv->geometry_set = FALSE;
+
+      update_geometry |= TRUE;
+    }
+  }
+
+  if (pspec_name == NULL || pspec_name == I_(AR_STYLE_PROP_CARD_SLOT_RATIO)) {
+    double card_slot_ratio;
+
+    card_slot_ratio = ar_style_get_card_slot_ratio (style);
+
+    update_geometry |= (card_slot_ratio != priv->card_slot_ratio);
+
+    priv->card_slot_ratio = card_slot_ratio;
+  }
+
+  if (pspec_name == NULL || pspec_name == I_(AR_STYLE_PROP_CARD_OVERHANG)) {
+    update_geometry |= TRUE;
+  }
+
+  if (pspec_name == NULL || pspec_name == I_(AR_STYLE_PROP_CARD_STEP)) {
+    update_geometry |= TRUE;
+  }
+
+  if (pspec_name == NULL || pspec_name == I_(AR_STYLE_PROP_INTERIOR_FOCUS)) {
+    gboolean interior_focus;
+
+    interior_focus = ar_style_get_interior_focus (style);
+
+    redraw_focus = (interior_focus != priv->interior_focus);
+
+    priv->interior_focus = interior_focus;
+  }
+
+  if (pspec_name == NULL || pspec_name == I_(AR_STYLE_PROP_FOCUS_LINE_WIDTH)) {
+    int focus_line_width;
+
+    focus_line_width = ar_style_get_focus_line_width (style);
+
+    redraw_focus = (focus_line_width != priv->focus_line_width);
+
+    priv->focus_line_width = focus_line_width;
+  }
+
+  if (pspec_name == NULL || pspec_name == I_(AR_STYLE_PROP_FOCUS_PADDING)) {
+    int focus_padding;
+
+    focus_padding = ar_style_get_focus_padding (style);
+
+    redraw_focus = (focus_padding != priv->focus_padding);
+
+    priv->focus_padding = focus_padding;
+  }
+
+  if (pspec_name == NULL || pspec_name == I_(AR_STYLE_PROP_RTL)) {
+    gboolean is_rtl;
+
+    is_rtl = ar_style_get_rtl (style);
+
+    update_geometry |= (is_rtl != priv->is_rtl);
+
+    priv->is_rtl = is_rtl;
+
+    /* FIXMEchpe: necessary? */
+    priv->force_geometry_update = TRUE;
+  }
+
+  if (pspec_name == NULL || pspec_name == I_(AR_STYLE_PROP_ENABLE_ANIMATIONS)) {
+    /* FIXMEchpe: abort animations-in-progress if the setting is now OFF */
+  }
+
+  if (pspec_name == NULL || pspec_name == I_(AR_STYLE_PROP_CLICK_TO_MOVE)) {
+    gboolean click_to_move;
+
+    click_to_move = ar_style_get_click_to_move (style);
+    if (click_to_move != priv->click_to_move) {
+      /* Clear the selection. Do this before setting the new value,
+      * since otherwise selection won't get cleared correctly.
+      */
+      set_selection (board, NULL, -1, FALSE);
+
+      priv->click_to_move = click_to_move;
+
+      /* FIXMEchpe: we used to queue a redraw here. WHY?? Check that it's safe not to. */
+    }
+  }
+
+#ifndef HAVE_HILDON
+  if (pspec_name == NULL || pspec_name == I_(AR_STYLE_PROP_SHOW_STATUS_MESSAGES)) {
+    gboolean show_status_messages;
+
+    show_status_messages = ar_style_get_show_status_messages (priv->style);
+
+    if (show_status_messages != priv->show_status_messages) {
+      priv->show_status_messages = show_status_messages;
+
+      if (!show_status_messages) {
+        /* Clear message */
+        set_status_message (board, NULL);
+      }
+    }
+  }
+#endif /* !HAVE_HILDON */
+
+  /* FIXMEchpe: queue a relayout instead? */
+  if (update_geometry) {
+    aisleriot_board_setup_geometry (board);
+  }
+
+  if (redraw_focus) {
+    /* FIXMEchpe: do redraw the focus! */
+  }
+}
+
+/* Class implementation */
+
+G_DEFINE_TYPE (AisleriotBoard, aisleriot_board, CLUTTER_TYPE_GROUP);
+
+/* AisleriotBoardClass methods */
+
+#ifdef ENABLE_KEYNAV
+
+static void
+aisleriot_board_activate (AisleriotBoard *board,
+                          const char *action,
+                          guint keyval,
+                          ClutterModifierType modifiers)
+{
+  AisleriotBoardPrivate *priv = board->priv;  ArSlot *focus_slot = priv->focus_slot;
+  ArSlot *selection_slot = priv->selection_slot;
+  int selection_start_card_id = priv->selection_start_card_id;
+
+#ifdef FIXMEchpe
+  if (!gtk_widget_has_focus (widget))
+    return;
+#endif
+
+  _games_debug_print (GAMES_DEBUG_GAME_KEYNAV,
+                      "board ::activate keyval %x modifiers %x\n",
+                      keyval, modifiers);
+
+  if (!focus_slot) {
+    aisleriot_board_error_bell (board);
+    return;
+  }
+
+  /* Focus not shown? Show it, and do nothing else */
+  if (!priv->show_focus) {
+    set_focus (board, focus_slot, priv->focus_card_id, TRUE);
+    return;
+  }
+
+  /* Control-Activate is double-click */
+  if (modifiers & CLUTTER_CONTROL_MASK) {
+    aisleriot_game_record_move (priv->game, -1, NULL, 0);
+    if (aisleriot_game_button_double_clicked_lambda (priv->game, focus_slot->id)) {
+      aisleriot_game_end_move (priv->game);
+    } else {
+      aisleriot_game_discard_move (priv->game);
+      aisleriot_board_error_bell (board);
+    }
+
+    aisleriot_game_test_end_of_game (priv->game);
+
+    return;
+  }
+
+  /* Try single click action */
+  aisleriot_game_record_move (priv->game, -1, NULL, 0);
+
+  if (aisleriot_game_button_clicked_lambda (priv->game, focus_slot->id)) {
+    aisleriot_game_end_move (priv->game);
+    games_sound_play ("click");
+    aisleriot_game_test_end_of_game (priv->game);
+
+    return;
+  }
+
+  aisleriot_game_discard_move (priv->game);
+
+  /* If we have a selection, and the topmost card of a slot is focused,
+   * try to move the selected cards to the focused slot.
+   * Note that this needs to be tested even if selection_slot == focus_slot !
+   *
+   * NOTE: We cannot use aisleriot_game_drop_valid here since the
+   * game may not support the "droppable" feature.
+   */
+  if (selection_slot != NULL &&
+      selection_start_card_id >= 0 &&
+      priv->focus_card_id == ((int) focus_slot->cards->len) - 1) {
+    if (aisleriot_board_move_selected_cards_to_slot (board, focus_slot)) {
+      /* Select the new topmost card */
+      set_focus (board, focus_slot, ((int) focus_slot->cards->len - 1), TRUE);
+
+      return;
+    }
+
+    /* Trying to move the cards has unset the selection; re-select them */
+    set_selection (board, selection_slot, selection_start_card_id, TRUE);
+  }
+
+  aisleriot_board_error_bell (board);
+}
+
+static gboolean
+aisleriot_board_move_cursor (AisleriotBoard *board,
+                             const char *action,
+                             guint keyval,
+                             ClutterModifierType modifiers)
+{
+  AisleriotBoardPrivate *priv = board->priv;
+  gboolean is_control, is_shift, moved = FALSE;
+  char step;
+  int count;
+
+#ifdef FIXMEchpe
+  if (!gtk_widget_has_focus (widget))
+    return FALSE;
+#endif
+
+  step = action[0];
+  count = (action[1] == MOVE_LEFT ? -1 : 1);
+
+  _games_debug_print (GAMES_DEBUG_GAME_KEYNAV,
+                      "board ::move-cursor keyval %x modifiers %x step '%c' count %d\n",
+                      keyval, modifiers,
+                      step, count);
+
+  /* No focus? Set focus to the first/last slot */
+  /* This will always return TRUE, no need for keynav-failed handling */
+  if (!priv->focus_slot) {
+    switch (step) {
+      case MOVE_CURSOR_UP_DOWN:
+      case MOVE_CURSOR_PAGES:
+        /* Focus the first slot */
+        return aisleriot_board_move_cursor_start_end_by_slot (board, -1);
+      case MOVE_CURSOR_LEFT_RIGHT:
+        /* Move as if we'd been on the last/first slot */
+        if (!priv->is_rtl) {
+          return aisleriot_board_move_cursor_start_end_by_slot (board, -count);
+        }
+        /* fall-through */
+      default:
+        return aisleriot_board_move_cursor_start_end_by_slot (board, count);
+    }
+  }
+
+  g_assert (priv->focus_slot != NULL);
+
+  is_shift = (modifiers & CLUTTER_SHIFT_MASK) != 0;
+  is_control = (modifiers & CLUTTER_CONTROL_MASK) != 0;
+
+  switch (step) {
+    case MOVE_CURSOR_LEFT_RIGHT:
+      if (is_shift) {
+        moved = aisleriot_board_extend_selection_left_right (board, count);
+      } else {
+        moved = aisleriot_board_move_cursor_left_right (board, count, is_control);
+      }
+      break;
+    case MOVE_CURSOR_UP_DOWN:
+      if (is_shift) {
+        moved = aisleriot_board_extend_selection_up_down (board, count);
+      } else {
+        moved = aisleriot_board_move_cursor_up_down (board, count, is_control);
+      }
+      break;
+    case MOVE_CURSOR_PAGES:
+      if (!is_shift) {
+        moved = aisleriot_board_move_cursor_up_down (board, count, TRUE);
+      }
+      break;
+    case MOVE_CURSOR_START_END:
+      if (is_shift) {
+        moved = aisleriot_board_extend_selection_start_end (board, count);
+      } else if (is_control) {
+        moved = aisleriot_board_move_cursor_start_end_by_slot (board, count);
+      } else {
+        moved = aisleriot_board_move_cursor_start_end_in_slot (board, count);
+      }
+      break;
+    default:
+      g_assert_not_reached ();
+  }
+
+  /* Show focus */
+  if (!moved &&
+      !priv->show_focus) {
+    set_focus (board, priv->focus_slot, priv->focus_card_id, TRUE);
+  }
+
+  return moved;
+}
+
+static void
+aisleriot_board_select_all (AisleriotBoard *board,
+                            const char *action,
+                            guint keyval,
+                            ClutterModifierType modifiers)
+{
+  AisleriotBoardPrivate *priv = board->priv;
+  ArSlot *focus_slot = priv->focus_slot;
+
+  _games_debug_print (GAMES_DEBUG_GAME_KEYNAV,
+                      "board ::select-all keyval %x modifiers %x\n",
+                      keyval, modifiers);
+
+  if (!focus_slot ||
+      focus_slot->cards->len == 0 ||
+      !aisleriot_board_extend_selection_in_slot_maximal (board)) {
+    set_selection (board, NULL, -1, FALSE);
+    aisleriot_board_error_bell (board);
+  }
+}
+
+static void
+aisleriot_board_deselect_all (AisleriotBoard *board,
+                              const char *action,
+                              guint keyval,
+                              ClutterModifierType modifiers)
+{
+  _games_debug_print (GAMES_DEBUG_GAME_KEYNAV,
+                      "board ::deselect-all keyval %x modifiers %x\n",
+                      keyval, modifiers);
+
+  set_selection (board, NULL, -1, FALSE);
+}
+
+static void
+aisleriot_board_toggle_selection (AisleriotBoard *board,
+                                  const char *action,
+                                  guint keyval,
+                                  ClutterModifierType modifiers)
+{
+  AisleriotBoardPrivate *priv = board->priv;
+  ArSlot *focus_slot;
+  int focus_card_id;
+
+  _games_debug_print (GAMES_DEBUG_GAME_KEYNAV,
+                      "board ::toggle-selection keyval %x modifiers %x\n",
+                      keyval, modifiers);
+
+  focus_slot = priv->focus_slot;
+  if (!focus_slot)
+    return;
+
+  focus_card_id = priv->focus_card_id;
+
+  /* Focus not shown? Show it, and proceed */
+  if (!priv->show_focus) {
+    set_focus (board, focus_slot, focus_card_id, TRUE);
+  }
+
+  if (focus_card_id < 0) {
+    aisleriot_board_error_bell (board);
+    return;
+  }
+
+  /* If the selection isn't currently showing, don't truncate it.
+   * Otherwise we get unexpected results when clicking on some cards
+   * (which selects them but doesn't show the selection) and then press
+   * Space or Shift-Up/Down etc.
+   */
+  if (priv->selection_slot == focus_slot &&
+      priv->selection_start_card_id == focus_card_id &&
+      priv->show_selection) {
+    set_selection (board, NULL, -1, FALSE);
+    return;
+  }
+
+  if (!aisleriot_game_drag_valid (priv->game,
+                                  focus_slot->id,
+                                  focus_slot->cards->data + focus_card_id,
+                                  focus_slot->cards->len - focus_card_id)) {
+    aisleriot_board_error_bell (board);
+    return;
+  }
+
+  set_selection (board, focus_slot, focus_card_id, TRUE);
+}
+
+#endif /* ENABLE_KEYNAV */
+
+
+/* ClutterActorClass impl */
+
+#if 0
+static void
+aisleriot_board_realize (GtkWidget *widget)
+{
+  AisleriotBoard *board = AISLERIOT_BOARD (widget);
+//   AisleriotBoardPrivate *priv = board->priv;
+
+  GTK_WIDGET_CLASS (aisleriot_board_parent_class)->realize (widget);
+
+  aisleriot_board_setup_geometry (board);
+}
+
+static void
+aisleriot_board_unrealize (GtkWidget *widget)
+{
+  AisleriotBoard *board = AISLERIOT_BOARD (widget);
+  AisleriotBoardPrivate *priv = board->priv;
+
+  priv->geometry_set = FALSE;
+
+  clear_state (board);
+
+  GTK_WIDGET_CLASS (aisleriot_board_parent_class)->unrealize (widget);
+}
+#endif
+
+static void
+aisleriot_board_allocate (ClutterActor *actor,
+                          const ClutterActorBox *box,
+                          ClutterAllocationFlags flags)
+{
+  AisleriotBoard *board = AISLERIOT_BOARD (actor);
+  AisleriotBoardPrivate *priv = board->priv;
+  gboolean is_same;
+
+  is_same = clutter_actor_box_equal (box, &priv->allocation);
+
+  _games_debug_print (GAMES_DEBUG_GAME_SIZING,
+                      "board ::allocate (%f / %f)-(%f / %f) => %f x %f is-same %s force-update %s\n",
+                      box->x1, box->y1, box->x2, box->y2,
+                      box->x2 - box->x1, box->y2 - box->y1,
+                      is_same ? "t" : "f",
+                      priv->force_geometry_update ? "t" : "f");
+
+  CLUTTER_ACTOR_CLASS (aisleriot_board_parent_class)->allocate (actor, box, flags);
+
+  priv->allocation = *box;
+
+  if (is_same && !priv->force_geometry_update)
+    return;
+
+  priv->force_geometry_update = FALSE;
+
+  /* FIXMEchpe: just queue this instead maybe? */
+  aisleriot_board_setup_geometry (board);
+}
+
+static void
+aisleriot_board_get_preferred_width (ClutterActor *actor,
+                                     float for_height,
+                                     float *min_width_p,
+                                     float *natural_width_p)
+{
+  _games_debug_print (GAMES_DEBUG_GAME_SIZING,
+                      "board ::get-preferred-width\n");
+
+  *min_width_p = BOARD_MIN_WIDTH;
+  *natural_width_p = 3 * BOARD_MIN_WIDTH;
+}
+
+static void
+aisleriot_board_get_preferred_height (ClutterActor *actor,
+                                      float for_width,
+                                      float *min_height_p,
+                                      float *natural_height_p)
+{
+  _games_debug_print (GAMES_DEBUG_GAME_SIZING,
+                      "board ::get-preferred-height\n");
+
+  *min_height_p = BOARD_MIN_HEIGHT;
+  *natural_height_p = 3 * BOARD_MIN_HEIGHT;
+}
+
+#ifdef ENABLE_KEYNAV
+
+static gboolean
+aisleriot_board_focus (AisleriotBoard *board,
+                       int count)
+{
+  AisleriotBoardPrivate *priv = board->priv;
+
+  if (!priv->focus_slot) {
+    return aisleriot_board_move_cursor_start_end_by_slot (board, -count);
+  }
+
+#if 0
+  if (aisleriot_board_move_cursor_left_right_by_slot (board, count, FALSE))
+    return TRUE;
+#endif
+
+  return FALSE;
+}
+
+static gboolean
+aisleriot_board_key_press (ClutterActor *actor,
+                           ClutterKeyEvent *event)
+{
+  ClutterBindingPool *pool;
+
+  _games_debug_print (GAMES_DEBUG_GAME_EVENTS,
+                      "board ::key-press keyval %x modifiers %x\n",
+                      event->keyval, event->modifier_state);
+
+  pool = clutter_binding_pool_get_for_class (CLUTTER_ACTOR_GET_CLASS (actor));
+  g_assert (pool != NULL);
+
+  return clutter_binding_pool_activate (pool,
+                                        event->keyval,
+                                        event->modifier_state,
+                                        G_OBJECT (actor));
+}
+
+#endif /* ENABLE_KEYNAV */
+
+#ifdef FIXMEchpe
+/* The gtkwidget.c focus in/out handlers queue a shallow draw;
+ * that's ok for us but maybe we want to optimise this a bit to
+ * only do it if we have a focus to draw/erase?
+ */
+static gboolean
+aisleriot_board_focus_in (GtkWidget *widget,
+                          GdkEventFocus *event)
+{
+#ifdef ENABLE_KEYNAV
+  AisleriotBoard *board = AISLERIOT_BOARD (widget);
+  AisleriotBoardPrivate *priv = board->priv;
+
+  /* Paint focus */
+  if (priv->show_focus &&
+      priv->focus_slot != NULL) {
+    gdk_window_invalidate_rect (widget->window, &priv->focus_rect, FALSE);
+  }
+#endif /* ENABLE_KEYNAV */
+
+  return FALSE;
+}
+
+static gboolean
+aisleriot_board_focus_out (GtkWidget *widget,
+                           GdkEventFocus *event)
+{
+  AisleriotBoard *board = AISLERIOT_BOARD (widget);
+#ifdef ENABLE_KEYNAV
+  AisleriotBoardPrivate *priv = board->priv;
+#endif /* ENABLE_KEYNAV */
+
+  clear_state (board);
+
+#ifdef ENABLE_KEYNAV
+  /* Hide focus */
+  if (priv->show_focus &&
+      priv->focus_slot != NULL) {
+    gdk_window_invalidate_rect (widget->window, &priv->focus_rect, FALSE);
+  }
+#endif /* ENABLE_KEYNAV */
+
+  return FALSE;
+}
+#endif /* FIXMEchpe */
+
+static gboolean
+aisleriot_board_button_press (ClutterActor *actor,
+                              ClutterButtonEvent *event)
+{
+  AisleriotBoard *board = AISLERIOT_BOARD (actor);
+  AisleriotBoardPrivate *priv = board->priv;
+  ArSlot *hslot;
+  int cardid;
+  guint32 button;
+  gboolean drag_valid;
+  guint state;
+  gboolean is_double_click, show_focus;
+
+  _games_debug_print (GAMES_DEBUG_GAME_EVENTS,
+                      "board ::button-press @(%f / %f) button %d click-count %d modifiers %x\n",
+                      event->x, event->y,
+                      event->button, event->click_count,
+                      event->modifier_state);
+
+  /* NOTE: It's ok to just return instead of chaining up, since the
+   * parent classes have no class closure for this event.
+   */
+
+  /* FIXMEchpe: check event coordinate handling (float vs int!) */
+
+#ifdef FIXMEchpe
+  /* FIXMEchpe: we might be able to use ClutterButtonEvent::click_count for double-click detection! */
+  /* ignore the gdk synthetic double/triple click events */
+  if (event->type != GDK_BUTTON_PRESS)
+    return FALSE;
+#endif
+
+  /* Don't do anything if a modifier is pressed */
+  /* FIXMEchpe: is there anything like gtk_accelerator_get_default_mod_mask() in clutter? */
+  state = event->modifier_state & CLUTTER_DEFAULT_MOD_MASK;
+  if (state != 0)
+    return FALSE;
+
+  button = event->button;
+
+  /* We're only interested in left, middle and right-clicks */
+  if (button < 1 || button > 3)
+    return FALSE;
+
+  /* If we already have a click, ignore this new one */
+  if (priv->click_status != STATUS_NONE) {
+    return FALSE;
+  }
+
+  /* If the game hasn't started yet, start it now */
+  aisleriot_game_start (priv->game);
+
+  get_slot_and_card_from_point (board, event->x, event->y, &hslot, &cardid);
+
+  is_double_click = button == 2 ||
+                    (priv->last_click_left_click &&
+                     (event->time - priv->last_click_time <= ar_style_get_double_click_time (priv->style)) &&
+                     priv->last_clicked_slot == hslot &&
+                     priv->last_clicked_card_id == cardid);
+
+  priv->last_click_x = event->x;
+  priv->last_click_y = event->y;
+  priv->last_clicked_slot = hslot;
+  priv->last_clicked_card_id = cardid;
+  priv->last_click_time = event->time;
+  priv->last_click_left_click = button == 1;
+
+  if (!hslot) {
+    set_focus (board, NULL, -1, FALSE);
+    set_selection (board, NULL, -1, FALSE);
+
+    priv->click_status = STATUS_NONE;
+
+    return FALSE;
+  }
+
+  set_cursor (board, AR_CURSOR_CLOSED);
+
+  /* First check if it's a right-click: if so, we reveal the card and do nothing else */
+  if (button == 3) {
+    /* Don't change the selection here! */
+    reveal_card (board, hslot, cardid);
+
+    return TRUE;
+  }
+
+  /* Clear revealed card */
+  reveal_card (board, NULL, -1);
+
+  /* We can't let Gdk do the double-click detection; since the entire playing
+   * area is one big widget it can't distinguish between single-clicks on two
+   * different cards and a double-click on one card.
+   */
+  if (is_double_click) {
+    ArSlot *clicked_slot = hslot;
+
+    priv->click_status = STATUS_NONE;
+
+    /* Reset this since otherwise 3 clicks will be interpreted as 2 double-clicks */
+    priv->last_click_left_click = FALSE;
+
+    aisleriot_game_record_move (priv->game, -1, NULL, 0);
+    if (aisleriot_game_button_double_clicked_lambda (priv->game, clicked_slot->id)) {
+      aisleriot_game_end_move (priv->game);
+    } else {
+      aisleriot_game_discard_move (priv->game);
+    }
+
+    aisleriot_game_test_end_of_game (priv->game);
+
+    set_cursor (board, AR_CURSOR_OPEN);
+
+    return TRUE;
+  }
+
+  /* button == 1 from now on */
+
+  if (priv->selection_slot == NULL)
+    goto set_selection;
+
+  /* In click-to-move mode, we need to test whether moving the selected cards
+   * to this slot does a move. Note that it is necessary to do this both if
+   * the clicked slot is the selection_slot (and the clicked card the topmost
+   * card below the selection), and if it's not the selection_slot, since some
+   * games depend on this behaviour (e.g. Treize). See bug #565560.
+   *
+   * Note that aisleriot_board_move_selected_cards_to_slot unsets the selection,
+   * so we need to fall through to set_selection if no move was done.
+    */
+  if (priv->click_to_move &&
+      priv->selection_start_card_id >= 0 &&
+      (hslot != priv->selection_slot || cardid + 1 == priv->selection_start_card_id)) {
+
+    /* Try to move the selected cards to the clicked slot */
+    if (aisleriot_board_move_selected_cards_to_slot (board, hslot))
+      return TRUE;
+
+    /* Move failed if this wasn't the selection_slot slot */
+    if (hslot != priv->selection_slot) {
+      aisleriot_board_error_bell (board);
+    }
+  }
+
+  if (hslot != priv->selection_slot ||
+      cardid != priv->selection_start_card_id)
+    goto set_selection;
+    
+  /* Single click on the selected slot & card, we take that to mean to deselect,
+   * but only in click-to-move mode.
+   */
+  if (priv->click_to_move) {
+    set_selection (board, NULL, -1, FALSE);
+
+    /* Reveal the card on left click */
+    reveal_card (board, hslot, cardid);
+
+    return TRUE;
+  }
+
+set_selection:
+
+  if (cardid >= 0) {
+    drag_valid = aisleriot_game_drag_valid (priv->game,
+                                            hslot->id,
+                                            hslot->cards->data + cardid,
+                                            hslot->cards->len - cardid);
+  } else {
+    drag_valid = FALSE;
+  }
+
+  if (drag_valid) {
+    set_selection (board, hslot, cardid, priv->click_to_move);
+    priv->click_status = priv->click_to_move ? STATUS_NOT_DRAG : STATUS_MAYBE_DRAG;
+  } else {
+    set_selection (board, NULL, -1, FALSE);
+    priv->click_status = STATUS_NOT_DRAG;
+  }
+
+  /* If we're already showing focus or just clicked on the
+   * card with the (hidden) focus, show the focus on the
+   * clicked card.
+   */
+  show_focus = priv->show_focus ||
+               (hslot == priv->focus_slot &&
+                cardid == priv->focus_card_id);
+  set_focus (board, hslot, cardid, show_focus);
+
+  /* Reveal the card on left click */
+  if (priv->click_to_move) {
+    reveal_card (board, hslot, cardid);
+  }
+
+  return FALSE;
+}
+
+static gboolean
+aisleriot_board_button_release (ClutterActor *actor,
+                                ClutterButtonEvent *event)
+{
+  AisleriotBoard *board = AISLERIOT_BOARD (actor);
+  AisleriotBoardPrivate *priv = board->priv;
+  /* guint state; */
+
+  _games_debug_print (GAMES_DEBUG_GAME_EVENTS,
+                      "board ::button-release @(%f / %f) button %d click-count %d modifiers %x\n",
+                      event->x, event->y,
+                      event->button, event->click_count,
+                      event->modifier_state);
+
+  /* FIXMEchpe: check event coordinate handling (float vs int!) */
+
+  /* NOTE: It's ok to just return instead of chaining up, since the
+   * parent classes have no class closure for this event.
+   */
+
+  /* We just abort any action on button release, even if the button-up
+   * is not the one that started the action. This greatly simplifies the code,
+   * and is also the right thing to do, anyway.
+   */
+
+  /* state = event->state & gtk_accelerator_get_default_mod_mask (); */
+
+  switch (priv->click_status) {
+    case STATUS_SHOW:
+      reveal_card (board, NULL, -1);
+      break;
+
+    case STATUS_IS_DRAG:
+      highlight_drop_target (board, NULL);
+      drop_moving_cards (board, event->x, event->y);
+      break;
+
+    case STATUS_MAYBE_DRAG:
+    case STATUS_NOT_DRAG: {
+      ArSlot *slot;
+      int card_id;
+
+      /* Don't do the action if the mouse moved away from the clicked slot; see bug #329183 */
+      get_slot_and_card_from_point (board, event->x, event->y, &slot, &card_id);
+      if (!slot || slot != priv->last_clicked_slot)
+        break;
+
+      aisleriot_game_record_move (priv->game, -1, NULL, 0);
+      if (aisleriot_game_button_clicked_lambda (priv->game, slot->id)) {
+        aisleriot_game_end_move (priv->game);
+	games_sound_play_for_event ("click", (GdkEvent *) event);
+      } else {
+        aisleriot_game_discard_move (priv->game);
+      }
+
+      aisleriot_game_test_end_of_game (priv->game);
+
+      break;
+    }
+
+    case STATUS_NONE:
+      break;
+  }
+
+  priv->click_status = STATUS_NONE;
+
+  set_cursor_by_location (board, event->x, event->y);
+
+  return TRUE;
+}
+
+static gboolean
+aisleriot_board_motion (ClutterActor *actor,
+                        ClutterMotionEvent *event)
+{
+  AisleriotBoard *board = AISLERIOT_BOARD (actor);
+  AisleriotBoardPrivate *priv = board->priv;
+
+  _games_debug_print (GAMES_DEBUG_GAME_EVENTS,
+                      "board ::motion @(%f / %f) modifiers %x\n",
+                      event->x, event->y,
+                      event->modifier_state);
+
+  /* FIXMEchpe: check event coordinate handling (float vs int!) */
+
+  /* NOTE: It's ok to just return instead of chaining up, since the
+   * parent classes have no class closure for this event.
+   */
+
+#ifndef HAVE_HILDON
+  if (priv->show_status_messages) {
+    ArSlot *slot = NULL;
+    int cardid = -1;
+
+    get_slot_and_card_from_point (board, event->x, event->y, &slot, &cardid);
+    if (slot != NULL && ar_slot_get_slot_type (slot) != AR_SLOT_UNKNOWN) {
+      char *text;
+
+      text = ar_slot_get_hint_string (slot, cardid);
+      set_status_message (board, text);
+      g_free (text);
+    } else {
+      set_status_message (board, NULL);
+    }
+  }
+#endif /* !HAVE_HILDON */
+
+  if (priv->click_status == STATUS_IS_DRAG) {
+    ArSlot *slot;
+    int x, y;
+
+    x = event->x - priv->last_click_x;
+    y = event->y - priv->last_click_y;
+
+    slot = find_drop_target (board, x, y);
+    highlight_drop_target (board, slot);
+
+    clutter_actor_set_position (priv->moving_cards_group, x, y);
+    clutter_actor_raise_top (priv->moving_cards_group);
+
+    set_cursor (board, AR_CURSOR_CLOSED);
+  } else if (priv->click_status == STATUS_MAYBE_DRAG &&
+             ar_style_check_dnd_drag_threshold (priv->style,
+                                                priv->last_click_x,
+                                                priv->last_click_y,
+                                                event->x,
+                                                event->y)) {
+    drag_begin (board);
+  } else {
+    set_cursor_by_location (board, event->x, event->y);
+  }
+
+  return FALSE;
+}
+
+static gboolean
+aisleriot_board_enter (ClutterActor *actor,
+                       ClutterCrossingEvent *event)
+{
+  _games_debug_print (GAMES_DEBUG_GAME_EVENTS,
+                      "board ::enter @(%f / %f)\n",
+                      event->x, event->y);
+
+  /* NOTE: It's ok to just return instead of chaining up, since the
+   * parent classes have no class closure for this event.
+   */
+
+  return FALSE;
+}
+
+static gboolean
+aisleriot_board_leave (ClutterActor *actor,
+                       ClutterCrossingEvent *event)
+{
+  AisleriotBoard *board = AISLERIOT_BOARD (actor);
+
+  _games_debug_print (GAMES_DEBUG_GAME_EVENTS,
+                      "board ::leave @(%f / %f)\n",
+                      event->x, event->y);
+
+  /* NOTE: It's ok to just return instead of chaining up, since the
+   * parent classes have no class closure for this event.
+   */
+
+  set_cursor (board, AR_CURSOR_DEFAULT);
+
+  return FALSE;
+}
+
+static void
+aisleriot_board_key_focus_in (ClutterActor *actor)
+{
+  _games_debug_print (GAMES_DEBUG_GAME_EVENTS,
+                      "board ::key-focus-in\n");
+}
+
+static void
+aisleriot_board_key_focus_out (ClutterActor *actor)
+{
+  _games_debug_print (GAMES_DEBUG_GAME_EVENTS,
+                      "board ::key-focus-out\n");
+}
+
+/* GObjectClass methods */
+
+static void
+aisleriot_board_init (AisleriotBoard *board)
+{
+  ClutterActor *actor = CLUTTER_ACTOR (board);
+  AisleriotBoardPrivate *priv;
+
+  priv = board->priv = AISLERIOT_BOARD_GET_PRIVATE (board);
+
+  priv->textures = ar_card_textures_cache_new ();
+
+  memset (&priv->allocation, 0, sizeof (ClutterActorBox));
+
+  /* We want to receive events! */
+  clutter_actor_set_reactive (actor, TRUE);
+
+  priv->force_geometry_update = FALSE;
+
+  priv->click_to_move = FALSE;
+  priv->show_selection = FALSE;
+  priv->show_status_messages = FALSE;
+
+  priv->show_card_id = -1;
+
+  priv->moving_cards = g_byte_array_sized_new (SLOT_CARDS_N_PREALLOC);
+
+  priv->removed_cards = g_array_new (FALSE, FALSE, sizeof (RemovedCard));
+
+  priv->animation_layer = g_object_ref_sink (clutter_group_new ());
+  clutter_container_add (CLUTTER_CONTAINER (board),
+                         priv->animation_layer, NULL);
+}
+
+static void
+aisleriot_board_finalize (GObject *object)
+{
+  AisleriotBoard *board = AISLERIOT_BOARD (object);
+  AisleriotBoardPrivate *priv = board->priv;
+
+  g_signal_handlers_disconnect_matched (priv->game,
+                                        G_SIGNAL_MATCH_DATA,
+                                        0, 0, NULL, NULL, board);
+  g_object_unref (priv->game);
+
+  g_array_free (priv->removed_cards, TRUE);
+
+  g_byte_array_free (priv->moving_cards, TRUE);
+
+  if (priv->style != NULL) {
+    g_signal_handlers_disconnect_by_func (priv->style,
+                                          G_CALLBACK (aisleriot_board_sync_style),
+                                          board);
+
+    g_object_unref (priv->style);
+  }
+
+#if 0
+  screen = gtk_widget_get_settings (widget);
+  g_signal_handlers_disconnect_matched (settings, G_SIGNAL_MATCH_DATA,
+                                        0, 0, NULL, NULL,
+                                        widget);
+#endif
+
+  G_OBJECT_CLASS (aisleriot_board_parent_class)->finalize (object);
+}
+
+static void
+aisleriot_board_dispose (GObject *object)
+{
+  AisleriotBoard *board = AISLERIOT_BOARD (object);
+  AisleriotBoardPrivate *priv = board->priv;
+
+  if (priv->textures) {
+    g_object_unref (priv->textures);
+    priv->textures = NULL;
+  }
+
+  if (priv->animation_layer) {
+    g_object_unref (priv->animation_layer);
+    priv->animation_layer = NULL;
+  }
+
+  if (priv->check_animations_handler) {
+    g_source_remove (priv->check_animations_handler);
+    priv->check_animations_handler = 0;
+  }
+
+  G_OBJECT_CLASS (aisleriot_board_parent_class)->dispose (object);
+}
+
+static void
+aisleriot_board_set_property (GObject *object,
+                              guint prop_id,
+                              const GValue *value,
+                              GParamSpec *pspec)
+{
+  AisleriotBoard *board = AISLERIOT_BOARD (object);
+  AisleriotBoardPrivate *priv = board->priv;
+
+  switch (prop_id) {
+    case PROP_GAME:
+      priv->game = AISLERIOT_GAME (g_value_dup_object (value));
+
+      g_signal_connect (priv->game, "game-type",
+                        G_CALLBACK (game_type_changed_cb), board);
+      g_signal_connect (priv->game, "game-cleared",
+                        G_CALLBACK (game_cleared_cb), board);
+      g_signal_connect (priv->game, "game-new",
+                        G_CALLBACK (game_new_cb), board);
+      g_signal_connect (priv->game, "slot-changed",
+                        G_CALLBACK (slot_changed_cb), board);
+
+      break;
+
+    case PROP_STYLE:
+      priv->style = g_value_dup_object (value);
+
+      aisleriot_board_sync_style (priv->style, NULL, board);
+      g_signal_connect (priv->style, "notify",
+                        G_CALLBACK (aisleriot_board_sync_style), board);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+aisleriot_board_class_init (AisleriotBoardClass *klass)
+{
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+  ClutterActorClass *actor_class = CLUTTER_ACTOR_CLASS (klass);
+#ifdef ENABLE_KEYNAV
+  ClutterBindingPool *binding_pool;
+  GClosure *closure;
+#endif
+
+  g_type_class_add_private (gobject_class, sizeof (AisleriotBoardPrivate));
+
+  gobject_class->dispose = aisleriot_board_dispose;
+  gobject_class->finalize = aisleriot_board_finalize;
+  gobject_class->set_property = aisleriot_board_set_property;
+
+#ifdef FIXMEchpe
+  widget_class->realize = aisleriot_board_realize;
+  widget_class->unrealize = aisleriot_board_unrealize;
+  widget_class->focus_in_event = aisleriot_board_focus_in;
+  widget_class->focus_out_event = aisleriot_board_focus_out;
+#endif // FIXMEchpe
+
+  actor_class->allocate = aisleriot_board_allocate;
+  actor_class->get_preferred_width = aisleriot_board_get_preferred_width;
+  actor_class->get_preferred_height = aisleriot_board_get_preferred_height;
+#ifdef ENABLE_KEYNAV
+  actor_class->key_press_event = aisleriot_board_key_press;
+#endif /* ENABLE_KEYNAV */
+  actor_class->button_press_event = aisleriot_board_button_press;
+  actor_class->button_release_event = aisleriot_board_button_release;
+  actor_class->motion_event = aisleriot_board_motion;
+  actor_class->enter_event = aisleriot_board_enter;
+  actor_class->leave_event = aisleriot_board_leave;
+  actor_class->key_focus_in = aisleriot_board_key_focus_in;
+  actor_class->key_focus_out = aisleriot_board_key_focus_out;
+
+
+  signals[REQUEST_CURSOR] =
+    g_signal_new (I_("request-cursor"),
+                  G_TYPE_FROM_CLASS (gobject_class),
+                  G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                  G_STRUCT_OFFSET (AisleriotBoardClass, request_cursor),
+                  NULL, NULL,
+                  g_cclosure_marshal_VOID__INT,
+                  G_TYPE_NONE,
+                  1,
+                  G_TYPE_INT);
+
+  signals[ERROR_BELL] =
+    g_signal_new (I_("error-bell"),
+                  G_TYPE_FROM_CLASS (gobject_class),
+                  G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                  G_STRUCT_OFFSET (AisleriotBoardClass, error_bell),
+                  NULL, NULL,
+                  g_cclosure_marshal_VOID__VOID,
+                  G_TYPE_NONE,
+                  0);
+
+  signals[STATUS_MESSAGE] =
+    g_signal_new (I_("status-message"),
+                  G_TYPE_FROM_CLASS (gobject_class),
+                  G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                  G_STRUCT_OFFSET (AisleriotBoardClass, status_message),
+                  NULL, NULL,
+                  g_cclosure_marshal_VOID__STRING,
+                  G_TYPE_NONE,
+                  1,
+                  G_TYPE_STRING | G_SIGNAL_TYPE_STATIC_SCOPE);
+
+  signals[FOCUS] =
+    g_signal_new (I_("focus"),
+                  G_TYPE_FROM_CLASS (gobject_class),
+                  G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                  G_STRUCT_OFFSET (AisleriotBoardClass, focus),
+                  NULL, NULL,
+                  g_cclosure_marshal_VOID__INT,
+                  G_TYPE_BOOLEAN,
+                  1,
+                  G_TYPE_INT);
+
+#ifdef ENABLE_KEYNAV
+  klass->focus = aisleriot_board_focus;
+  klass->activate = aisleriot_board_activate;
+  klass->move_cursor = aisleriot_board_move_cursor;
+  klass->select_all = aisleriot_board_select_all;
+  klass->deselect_all = aisleriot_board_deselect_all;
+  klass->toggle_selection = aisleriot_board_toggle_selection;
+
+  /* Keybinding signals */
+#ifdef FIXMEchpe
+  widget_class->activate_signal =
+#endif
+  signals[ACTIVATE] =
+    g_signal_new (I_("activate"),
+                  G_TYPE_FROM_CLASS (gobject_class),
+                  G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                  G_STRUCT_OFFSET (AisleriotBoardClass, activate),
+                  NULL, NULL,
+                  games_marshal_BOOLEAN__STRING_UINT_ENUM,
+                  G_TYPE_BOOLEAN,
+                  3,
+                  G_TYPE_STRING | G_SIGNAL_TYPE_STATIC_SCOPE,
+                  G_TYPE_UINT,
+                  CLUTTER_TYPE_MODIFIER_TYPE);
+
+  signals[MOVE_CURSOR] =
+    g_signal_new (I_("move-cursor"),
+                  G_TYPE_FROM_CLASS (gobject_class),
+                  G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                  G_STRUCT_OFFSET (AisleriotBoardClass, move_cursor),
+                  NULL, NULL,
+                  games_marshal_BOOLEAN__STRING_UINT_ENUM,
+                  G_TYPE_BOOLEAN,
+                  3,
+                  G_TYPE_STRING | G_SIGNAL_TYPE_STATIC_SCOPE,
+                  G_TYPE_UINT,
+                  CLUTTER_TYPE_MODIFIER_TYPE);
+
+  signals[TOGGLE_SELECTION] =
+    g_signal_new (I_("toggle-selection"),
+                  G_TYPE_FROM_CLASS (gobject_class),
+                  G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                  G_STRUCT_OFFSET (AisleriotBoardClass, toggle_selection),
+                  NULL, NULL,
+                  games_marshal_BOOLEAN__STRING_UINT_ENUM,
+                  G_TYPE_BOOLEAN,
+                  3,
+                  G_TYPE_STRING | G_SIGNAL_TYPE_STATIC_SCOPE,
+                  G_TYPE_UINT,
+                  CLUTTER_TYPE_MODIFIER_TYPE);
+
+  signals[SELECT_ALL] =
+    g_signal_new (I_("select-all"),
+                  G_TYPE_FROM_CLASS (gobject_class),
+                  G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                  G_STRUCT_OFFSET (AisleriotBoardClass, select_all),
+                  NULL, NULL,
+                  games_marshal_BOOLEAN__STRING_UINT_ENUM,
+                  G_TYPE_BOOLEAN,
+                  3,
+                  G_TYPE_STRING | G_SIGNAL_TYPE_STATIC_SCOPE,
+                  G_TYPE_UINT,
+                  CLUTTER_TYPE_MODIFIER_TYPE);
+
+  signals[DESELECT_ALL] =
+    g_signal_new (I_("deselect-all"),
+                  G_TYPE_FROM_CLASS (gobject_class),
+                  G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                  G_STRUCT_OFFSET (AisleriotBoardClass, deselect_all),
+                  NULL, NULL,
+                  games_marshal_BOOLEAN__STRING_UINT_ENUM,
+                  G_TYPE_BOOLEAN,
+                  3,
+                  G_TYPE_STRING | G_SIGNAL_TYPE_STATIC_SCOPE,
+                  G_TYPE_UINT,
+                  CLUTTER_TYPE_MODIFIER_TYPE);
+#endif /* ENABLE_KEYNAV */
+
+  /* Properties */
+  g_object_class_install_property
+    (gobject_class,
+     PROP_GAME,
+     g_param_spec_object ("game", NULL, NULL,
+                          AISLERIOT_TYPE_GAME,
+                          G_PARAM_WRITABLE |
+                          G_PARAM_CONSTRUCT_ONLY |
+                          G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_property
+    (gobject_class,
+     PROP_STYLE,
+     g_param_spec_object ("style", NULL, NULL,
+                          AR_TYPE_STYLE,
+                          G_PARAM_WRITABLE |
+                          G_PARAM_CONSTRUCT_ONLY |
+                          G_PARAM_STATIC_STRINGS));
+
+#ifdef ENABLE_KEYNAV
+  /* Keybindings */
+  binding_pool = clutter_binding_pool_get_for_class (klass);
+
+  /* Cursor movement */
+  closure = g_signal_type_cclosure_new (G_TYPE_FROM_CLASS (klass),
+                                        G_STRUCT_OFFSET (AisleriotBoardClass, move_cursor));
+
+  aisleriot_board_add_move_and_select_binding (binding_pool, closure,
+                                               I_(MOVE_CURSOR_LEFT_RIGHT_S MOVE_LEFT_S),
+                                               CLUTTER_Left, 0);
+  aisleriot_board_add_move_and_select_binding (binding_pool, closure,
+                                               I_(MOVE_CURSOR_LEFT_RIGHT_S MOVE_LEFT_S),
+                                               CLUTTER_KP_Left, 0);
+
+  aisleriot_board_add_move_and_select_binding (binding_pool, closure,
+                                               I_(MOVE_CURSOR_LEFT_RIGHT_S MOVE_RIGHT_S),
+                                               CLUTTER_Right, 0);
+  aisleriot_board_add_move_and_select_binding (binding_pool, closure,
+                                               I_(MOVE_CURSOR_LEFT_RIGHT_S MOVE_RIGHT_S),
+                                               CLUTTER_KP_Right, 0);
+  
+  aisleriot_board_add_move_and_select_binding (binding_pool, closure,
+                                               I_(MOVE_CURSOR_UP_DOWN_S MOVE_LEFT_S),
+                                               CLUTTER_Up, 0);
+  aisleriot_board_add_move_and_select_binding (binding_pool, closure,
+                                               I_(MOVE_CURSOR_UP_DOWN_S MOVE_LEFT_S),
+                                               CLUTTER_KP_Up, 0);
+
+  aisleriot_board_add_move_and_select_binding (binding_pool, closure,
+                                               I_(MOVE_CURSOR_UP_DOWN_S MOVE_RIGHT_S),
+                                               CLUTTER_Down, 0);
+  aisleriot_board_add_move_and_select_binding (binding_pool, closure,
+                                               I_(MOVE_CURSOR_UP_DOWN_S MOVE_RIGHT_S),
+                                               CLUTTER_KP_Down, 0);
+
+  aisleriot_board_add_move_and_select_binding (binding_pool, closure,
+                                               I_(MOVE_CURSOR_START_END_S MOVE_LEFT_S),
+                                               CLUTTER_Home, 0);
+  aisleriot_board_add_move_and_select_binding (binding_pool, closure,
+                                               I_(MOVE_CURSOR_START_END_S MOVE_LEFT_S),
+                                               CLUTTER_KP_Home, 0);
+  aisleriot_board_add_move_and_select_binding (binding_pool, closure,
+                                               I_(MOVE_CURSOR_START_END_S MOVE_LEFT_S),
+                                               CLUTTER_Begin, 0);
+
+  aisleriot_board_add_move_and_select_binding (binding_pool, closure,
+                                               I_(MOVE_CURSOR_START_END_S MOVE_RIGHT_S),
+                                               CLUTTER_End, 0);
+  aisleriot_board_add_move_and_select_binding (binding_pool, closure,
+                                               I_(MOVE_CURSOR_START_END_S MOVE_RIGHT_S),
+                                               CLUTTER_KP_End, 0);
+
+  aisleriot_board_add_move_binding (binding_pool, closure,
+                                    I_(MOVE_CURSOR_PAGES_S MOVE_LEFT_S),
+                                    CLUTTER_Page_Up, 0);
+  aisleriot_board_add_move_binding (binding_pool, closure,
+                                    I_(MOVE_CURSOR_PAGES_S MOVE_LEFT_S),
+                                    CLUTTER_KP_Page_Up, 0);
+
+  aisleriot_board_add_move_binding (binding_pool, closure,
+                                    I_(MOVE_CURSOR_PAGES_S MOVE_RIGHT_S),
+                                    CLUTTER_Page_Down, 0);
+  aisleriot_board_add_move_binding (binding_pool, closure,
+                                    I_(MOVE_CURSOR_PAGES_S MOVE_RIGHT_S),
+                                    CLUTTER_KP_Page_Down, 0);
+
+  g_closure_unref (closure);
+
+  /* Selection */
+  closure = g_signal_type_cclosure_new (G_TYPE_FROM_CLASS (klass),
+                                        G_STRUCT_OFFSET (AisleriotBoardClass, toggle_selection));
+
+  clutter_binding_pool_install_closure (binding_pool,
+                                        I_("toggle-selection"),
+                                        CLUTTER_space,
+                                        0,
+                                       closure);
+  clutter_binding_pool_install_closure (binding_pool,
+                                        I_("toggle-selection"),
+                                        CLUTTER_KP_Space,
+                                        0,
+                                        closure);
+  g_closure_unref (closure);
+
+  closure = g_signal_type_cclosure_new (G_TYPE_FROM_CLASS (klass),
+                                        G_STRUCT_OFFSET (AisleriotBoardClass, select_all));
+  clutter_binding_pool_install_closure (binding_pool,
+                                        I_("select-all"),
+                                        CLUTTER_a,
+                                        CLUTTER_CONTROL_MASK,
+                                        closure);
+  g_closure_unref (closure);
+
+  closure = g_signal_type_cclosure_new (G_TYPE_FROM_CLASS (klass),
+                                        G_STRUCT_OFFSET (AisleriotBoardClass, deselect_all));
+  clutter_binding_pool_install_closure (binding_pool,
+                                        I_("deselect-all"),
+                                        CLUTTER_a,
+                                        CLUTTER_CONTROL_MASK | CLUTTER_SHIFT_MASK,
+                                        closure);
+  g_closure_unref (closure);
+
+  /* Activate */
+  closure = g_signal_type_cclosure_new (G_TYPE_FROM_CLASS (klass),
+                                        G_STRUCT_OFFSET (AisleriotBoardClass, activate));
+
+  aisleriot_board_add_activate_binding (binding_pool, closure, CLUTTER_Return, 0);
+  aisleriot_board_add_activate_binding (binding_pool, closure, CLUTTER_ISO_Enter, 0);
+  aisleriot_board_add_activate_binding (binding_pool, closure, CLUTTER_KP_Enter, 0);
+
+  g_closure_unref (closure);
+#endif /* ENABLE_KEYNAV */
+}
+
+/* public API */
+
+ClutterActor *
+aisleriot_board_new (ArStyle *style,
+                     AisleriotGame *game)
+{
+  return g_object_new (AISLERIOT_TYPE_BOARD,
+                       "style", style,
+                       "game", game,
+                       NULL);
+}
+
+void
+aisleriot_board_abort_move (AisleriotBoard *board)
+{
+  clear_state (board);
+}
diff --git a/aisleriot/board.h b/aisleriot/board.h
new file mode 100644
index 0000000..9af01df
--- /dev/null
+++ b/aisleriot/board.h
@@ -0,0 +1,99 @@
+/*
+ * Copyright © 1998, 2003 Jonathan Blandford <jrb mit edu>
+ * Copyright © 2007 Christian Persch
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef AISLERIOT_BOARD_H
+#define AISLERIOT_BOARD_H
+
+#ifndef HAVE_CLUTTER
+#error board.h requires clutter
+#endif
+
+#include <clutter/clutter.h>
+
+#include "ar-style.h"
+#include "ar-cursor.h"
+#include "game.h"
+
+G_BEGIN_DECLS
+
+#define AISLERIOT_TYPE_BOARD		(aisleriot_board_get_type ())
+#define AISLERIOT_BOARD(o)		(G_TYPE_CHECK_INSTANCE_CAST ((o), AISLERIOT_TYPE_BOARD, AisleriotBoard))
+#define AISLERIOT_BOARD_CLASS(k)	(G_TYPE_CHECK_CLASS_CAST((k), AISLERIOT_TYPE_BOARD, AisleriotBoardClass))
+#define AISLERIOT_IS_BOARD(o)		(G_TYPE_CHECK_INSTANCE_TYPE ((o), AISLERIOT_TYPE_BOARD))
+#define AISLERIOT_IS_BOARD_CLASS(k)	(G_TYPE_CHECK_CLASS_TYPE ((k), AISLERIOT_TYPE_BOARD))
+#define AISLERIOT_BOARD_GET_CLASS(o)	(G_TYPE_INSTANCE_GET_CLASS ((o), AISLERIOT_TYPE_BOARD, AisleriotBoardClass))
+
+typedef struct _AisleriotBoard		AisleriotBoard;
+typedef struct _AisleriotBoardPrivate	AisleriotBoardPrivate;
+typedef struct _AisleriotBoardClass	AisleriotBoardClass;
+
+struct _AisleriotBoard {
+  ClutterGroup parent_instance;
+
+  /*< private >*/
+  AisleriotBoardPrivate *priv;
+};
+
+struct _AisleriotBoardClass {
+  ClutterGroupClass parent_class;
+
+  void (* request_cursor)   (AisleriotBoard *board,
+                             ArCursorType cursor_type);
+
+  void (* error_bell)       (AisleriotBoard *board);
+
+  void (* status_message)   (AisleriotBoard *board,
+                             const char *message);
+
+  /* Focus */
+  gboolean (* focus)        (AisleriotBoard *,
+                             int direction);
+
+  /* keybinding signals */
+  gboolean (* move_cursor)  (AisleriotBoard *,
+                             const char *,
+                             guint,
+                             ClutterModifierType);
+  void (* activate)         (AisleriotBoard *,
+                             const char *,
+                             guint,
+                             ClutterModifierType);
+  void (* toggle_selection) (AisleriotBoard *,
+                             const char *,
+                             guint,
+                             ClutterModifierType);
+  void (* select_all)       (AisleriotBoard *,
+                             const char *,
+                             guint,
+                             ClutterModifierType);
+  void (* deselect_all)     (AisleriotBoard *,
+                             const char *,
+                             guint,
+                             ClutterModifierType);
+};
+
+GType aisleriot_board_get_type (void);
+
+ClutterActor *aisleriot_board_new (ArStyle *style,
+                                   AisleriotGame *game);
+
+void aisleriot_board_abort_move (AisleriotBoard *board);
+
+G_END_DECLS
+
+#endif /* !AISLERIOT_BOARD_H */
diff --git a/aisleriot/card.c b/aisleriot/card.c
new file mode 100644
index 0000000..4409b45
--- /dev/null
+++ b/aisleriot/card.c
@@ -0,0 +1,364 @@
+/*
+ * Copyright © 2008 Neil Roberts
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <config.h>
+
+#include <clutter/clutter.h>
+#include <gtk/gtk.h>
+#include <cogl/cogl.h>
+
+#include "card.h"
+
+#include <libgames-support/games-glib-compat.h>
+
+static void aisleriot_card_paint (ClutterActor *actor);
+
+static void aisleriot_card_get_preferred_width (ClutterActor *self,
+                                                gfloat        for_height,
+                                                gfloat       *min_width_p,
+                                                gfloat       *natural_width_p);
+static void aisleriot_card_get_preferred_height
+                                               (ClutterActor *self,
+                                                gfloat        for_width,
+                                                gfloat       *min_height_p,
+                                                gfloat       *natural_height_p);
+
+static void aisleriot_card_dispose (GObject *self);
+
+static void aisleriot_card_set_property (GObject      *self,
+                                         guint         property_id,
+                                         const GValue *value,
+                                         GParamSpec   *pspec);
+static void aisleriot_card_get_property (GObject    *self,
+                                         guint       property_id,
+                                         GValue     *value,
+                                         GParamSpec *pspec);
+
+static void aisleriot_card_unref_cache (AisleriotCard *card);
+
+static void aisleriot_card_set_cache (AisleriotCard *card,
+                                      ArCardTexturesCache *cache);
+
+#define ANGLE_IS_UPSIDE_DOWN(angle)             \
+  ((ABS (angle) + CLUTTER_INT_TO_FIXED (90))    \
+   % CLUTTER_INT_TO_FIXED (360)                 \
+   >= CLUTTER_INT_TO_FIXED (180))
+
+G_DEFINE_TYPE (AisleriotCard, aisleriot_card, CLUTTER_TYPE_ACTOR);
+
+#define AISLERIOT_CARD_GET_PRIVATE(obj) \
+  (G_TYPE_INSTANCE_GET_PRIVATE ((obj), AISLERIOT_TYPE_CARD, \
+                                AisleriotCardPrivate))
+
+struct _AisleriotCardPrivate
+{
+  Card bottom_card, top_card;
+
+  ArCardTexturesCache *cache;
+};
+
+enum
+{
+  PROP_0,
+
+  PROP_CACHE,
+  PROP_BOTTOM_CARD,
+  PROP_TOP_CARD
+};
+
+static void
+aisleriot_card_class_init (AisleriotCardClass *klass)
+{
+  GObjectClass *gobject_class = (GObjectClass *) klass;
+  ClutterActorClass *actor_class = (ClutterActorClass *) klass;
+  GParamSpec *pspec;
+
+  gobject_class->dispose = aisleriot_card_dispose;
+  gobject_class->set_property = aisleriot_card_set_property;
+  gobject_class->get_property = aisleriot_card_get_property;
+
+  actor_class->paint = aisleriot_card_paint;
+  actor_class->get_preferred_width = aisleriot_card_get_preferred_width;
+  actor_class->get_preferred_height = aisleriot_card_get_preferred_height;
+
+  pspec = g_param_spec_uchar ("bottom-card", NULL, NULL,
+                              0, 255, 0,
+                              G_PARAM_READWRITE |
+                              G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (gobject_class, PROP_BOTTOM_CARD, pspec);
+
+  pspec = g_param_spec_uchar ("top-card", NULL, NULL,
+                              0, 255, 0,
+                              G_PARAM_READWRITE |
+                              G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (gobject_class, PROP_TOP_CARD, pspec);
+
+  pspec = g_param_spec_object ("cache", NULL, NULL,
+                               AR_TYPE_CARD_TEXTURES_CACHE,
+                               G_PARAM_READWRITE |
+                               G_PARAM_CONSTRUCT_ONLY |
+                               G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (gobject_class, PROP_CACHE, pspec);
+
+  g_type_class_add_private (klass, sizeof (AisleriotCardPrivate));
+}
+
+static void
+aisleriot_card_init (AisleriotCard *self)
+{
+  AisleriotCardPrivate *priv;
+
+  priv = self->priv = AISLERIOT_CARD_GET_PRIVATE (self);
+}
+
+static void
+aisleriot_card_dispose (GObject *self)
+{
+  AisleriotCard *card = AISLERIOT_CARD (self);
+
+  aisleriot_card_unref_cache (card);
+
+  G_OBJECT_CLASS (aisleriot_card_parent_class)->dispose (self);
+}
+
+ClutterActor *
+aisleriot_card_new (ArCardTexturesCache *cache,
+                    Card bottom_card,
+                    Card top_card)
+{
+  return g_object_new (AISLERIOT_TYPE_CARD,
+                       "cache", cache,
+                       "bottom-card", bottom_card.value,
+                       "top-card", top_card.value,
+                       NULL);
+}
+
+static void
+aisleriot_card_get_preferred_width (ClutterActor *self,
+                                    gfloat        for_height,
+                                    gfloat       *min_width_p,
+                                    gfloat       *natural_width_p)
+{
+  AisleriotCard *card = AISLERIOT_CARD (self);
+  AisleriotCardPrivate *priv = card->priv;
+  CoglHandle tex;
+  guint width;
+
+  tex = ar_card_textures_cache_get_card_texture (priv->cache,
+                                                    priv->top_card);
+
+  if (G_UNLIKELY (tex == COGL_INVALID_HANDLE))
+    width = 0;
+  else
+    width = cogl_texture_get_width (tex);
+
+  if (min_width_p)
+    *min_width_p = 0;
+
+  if (natural_width_p)
+    *natural_width_p = width;
+}
+
+static void
+aisleriot_card_get_preferred_height (ClutterActor *self,
+                                     gfloat        for_width,
+                                     gfloat       *min_height_p,
+                                     gfloat       *natural_height_p)
+{
+  AisleriotCard *card = AISLERIOT_CARD (self);
+  AisleriotCardPrivate *priv = card->priv;
+  CoglHandle tex;
+  guint height;
+
+  tex = ar_card_textures_cache_get_card_texture (priv->cache,
+                                                    priv->top_card);
+
+  if (G_UNLIKELY (tex == COGL_INVALID_HANDLE))
+    height = 0;
+  else
+    height = cogl_texture_get_height (tex);
+
+  if (min_height_p)
+    *min_height_p = 0;
+
+  if (natural_height_p)
+    *natural_height_p = height;
+}
+
+static void
+aisleriot_card_paint (ClutterActor *actor)
+{
+  AisleriotCard *card = (AisleriotCard *) actor;
+  AisleriotCardPrivate *priv = card->priv;
+  CoglHandle top_tex, bottom_tex;
+  ClutterActorBox alloc_box;
+  gboolean backface_culling_was_enabled = cogl_get_backface_culling_enabled ();
+
+  cogl_set_backface_culling_enabled (TRUE);
+
+  top_tex = ar_card_textures_cache_get_card_texture (priv->cache,
+                                                        priv->top_card);
+  bottom_tex = ar_card_textures_cache_get_card_texture (priv->cache,
+                                                           priv->bottom_card);
+  if (G_UNLIKELY (top_tex == COGL_INVALID_HANDLE
+                  || bottom_tex == COGL_INVALID_HANDLE))
+    return;
+
+  clutter_actor_get_allocation_box (actor, &alloc_box);
+
+  /* Draw both sides of the card. Backface culling is enabled so only
+     one side will actually be rendered */
+
+  cogl_set_source_texture (top_tex);
+
+  cogl_rectangle (0.0f, 0.0f,
+                  alloc_box.x2 - alloc_box.x1,
+                  alloc_box.y2 - alloc_box.y1);
+
+  cogl_set_source_texture (bottom_tex);
+
+  /* Rotate along the y-axis about the center of the card to make the
+     bottom of the card face the other way */
+  cogl_push_matrix ();
+  cogl_translate ((alloc_box.x2 - alloc_box.x1) / 2,
+                  0, 0);
+  cogl_rotate (180, 0, 1, 0);
+  cogl_translate (-(alloc_box.x2 - alloc_box.x1) / 2,
+                  0, 0);
+  cogl_rectangle (0.0f, 0.0f,
+                  alloc_box.x2 - alloc_box.x1,
+                  alloc_box.y2 - alloc_box.y1);
+  cogl_pop_matrix ();
+
+  if (!backface_culling_was_enabled)
+    cogl_set_backface_culling_enabled (FALSE);
+}
+
+static void
+aisleriot_card_set_property (GObject *self,
+                             guint property_id,
+                             const GValue *value,
+                             GParamSpec *pspec)
+{
+  AisleriotCard *card = AISLERIOT_CARD (self);
+
+  switch (property_id) {
+    case PROP_BOTTOM_CARD:
+      {
+        Card card_num;
+
+        card_num.value = g_value_get_uchar (value);
+
+        aisleriot_card_set_card (card, card_num, card->priv->top_card);
+      }
+      break;
+
+    case PROP_TOP_CARD:
+      {
+        Card card_num;
+
+        card_num.value = g_value_get_uchar (value);
+
+        aisleriot_card_set_card (card, card->priv->bottom_card, card_num);
+      }
+      break;
+
+    case PROP_CACHE:
+      aisleriot_card_set_cache (card, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (self, property_id, pspec);
+      break;
+  }
+}
+
+static void
+aisleriot_card_get_property (GObject *self,
+                             guint property_id,
+                             GValue *value,
+                             GParamSpec *pspec)
+{
+  AisleriotCard *card = AISLERIOT_CARD (self);
+  AisleriotCardPrivate *priv = card->priv;
+
+  switch (property_id) {
+    case PROP_BOTTOM_CARD:
+      g_value_set_uchar (value, priv->bottom_card.value);
+      break;
+
+    case PROP_TOP_CARD:
+      g_value_set_uchar (value, priv->top_card.value);
+      break;
+
+    case PROP_CACHE:
+      g_value_set_object (value, priv->cache);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (self, property_id, pspec);
+      break;
+  }
+}
+
+void
+aisleriot_card_set_card (AisleriotCard *card,
+                         Card bottom_card,
+                         Card top_card)
+{
+  AisleriotCardPrivate *priv;
+
+  g_return_if_fail (AISLERIOT_IS_CARD (card));
+
+  priv = card->priv;
+
+  priv->bottom_card = bottom_card;
+  priv->top_card = top_card;
+
+  clutter_actor_queue_redraw (CLUTTER_ACTOR (card));
+
+  g_object_notify (G_OBJECT (card), "bottom-card");
+  g_object_notify (G_OBJECT (card), "top-card");
+}
+
+static void
+aisleriot_card_unref_cache (AisleriotCard *card)
+{
+  AisleriotCardPrivate *priv = card->priv;
+
+  if (priv->cache) {
+    g_object_unref (priv->cache);
+    priv->cache = NULL;
+  }
+}
+
+static void
+aisleriot_card_set_cache (AisleriotCard *card, ArCardTexturesCache *cache)
+{
+  AisleriotCardPrivate *priv = card->priv;
+
+  if (cache)
+    g_object_ref (cache);
+
+  aisleriot_card_unref_cache (card);
+
+  priv->cache = cache;
+
+  clutter_actor_queue_redraw (CLUTTER_ACTOR (card));
+
+  g_object_notify (G_OBJECT (card), "cache");
+}
diff --git a/aisleriot/card.h b/aisleriot/card.h
new file mode 100644
index 0000000..b66775a
--- /dev/null
+++ b/aisleriot/card.h
@@ -0,0 +1,79 @@
+/*
+ * Copyright © 2008 Neil Roberts
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef AISLERIOT_CARD_H
+#define AISLERIOT_CARD_H
+
+#include <clutter/clutter.h>
+#include "ar-card-textures-cache.h"
+
+G_BEGIN_DECLS
+
+#define AISLERIOT_TYPE_CARD                                             \
+  (aisleriot_card_get_type())
+#define AISLERIOT_CARD(obj)                                             \
+  (G_TYPE_CHECK_INSTANCE_CAST ((obj),                                   \
+                               AISLERIOT_TYPE_CARD,                     \
+                               AisleriotCard))
+#define AISLERIOT_CARD_CLASS(klass)                                     \
+  (G_TYPE_CHECK_CLASS_CAST ((klass),                                    \
+                            AISLERIOT_TYPE_CARD,                        \
+                            AisleriotCardClass))
+#define AISLERIOT_IS_CARD(obj)                                          \
+  (G_TYPE_CHECK_INSTANCE_TYPE ((obj),                                   \
+                               AISLERIOT_TYPE_CARD))
+#define AISLERIOT_IS_CARD_CLASS(klass)                                  \
+  (G_TYPE_CHECK_CLASS_TYPE ((klass),                                    \
+                            AISLERIOT_TYPE_CARD))
+#define AISLERIOT_CARD_GET_CLASS(obj)                                   \
+  (G_TYPE_INSTANCE_GET_CLASS ((obj),                                    \
+                              AISLERIOT_TYPE_CARD,                      \
+                              AisleriotCardClass))
+
+typedef struct _AisleriotCard        AisleriotCard;
+typedef struct _AisleriotCardClass   AisleriotCardClass;
+typedef struct _AisleriotCardPrivate AisleriotCardPrivate;
+
+struct _AisleriotCardClass
+{
+  ClutterActorClass parent_class;
+};
+
+struct _AisleriotCard
+{
+  ClutterActor parent;
+
+  AisleriotCardPrivate *priv;
+};
+
+GType aisleriot_card_get_type (void) G_GNUC_CONST;
+
+ClutterActor *aisleriot_card_new (ArCardTexturesCache *cache,
+                                  Card bottom_card,
+                                  Card top_card);
+
+void aisleriot_card_set_card (AisleriotCard *card,
+                              Card bottom_card,
+                              Card top_card);
+
+void aisleriot_card_set_highlighted (AisleriotCard *card,
+                                     gboolean highlighted);
+gboolean aisleriot_card_get_highlighted (AisleriotCard *card);
+
+G_END_DECLS
+
+#endif /* AISLERIOT_CARD_H */
diff --git a/aisleriot/game.c b/aisleriot/game.c
index 185d263..e77e316 100644
--- a/aisleriot/game.c
+++ b/aisleriot/game.c
@@ -31,6 +31,10 @@
 #include <glib.h>
 #include <glib/gi18n.h>
 
+#ifdef HAVE_CLUTTER
+#include <clutter/clutter.h>
+#endif
+
 #include <libgames-support/games-debug.h>
 #include <libgames-support/games-glib-compat.h>
 #include <libgames-support/games-runtime.h>
@@ -233,7 +237,16 @@ clear_slots (AisleriotGame *game,
   for (i = 0; i < n_slots; ++i) {
     ArSlot *slot = game->slots->pdata[i];
 
+#ifdef HAVE_CLUTTER
+    if (slot->slot_renderer) {
+      clutter_actor_destroy (slot->slot_renderer);
+      g_object_unref (slot->slot_renderer);
+    }
+
+    g_byte_array_free (slot->old_cards, TRUE);
+#else
     g_ptr_array_free (slot->card_images, TRUE);
+#endif /* HAVE_CLUTTER */
     g_byte_array_free (slot->cards, TRUE);
 
     g_slice_free (ArSlot, slot);
@@ -629,7 +642,11 @@ cscmi_add_slot (SCM slot_data)
   slot->expanded_down = expanded_down != FALSE;
   slot->expanded_right = expanded_right != FALSE;
 
+#ifdef HAVE_CLUTTER
+  slot->old_cards = g_byte_array_sized_new (SLOT_CARDS_N_PREALLOC);
+#else
   slot->card_images = g_ptr_array_sized_new (SLOT_CARDS_N_PREALLOC);
+#endif
 
   slot->needs_update = TRUE;
 
@@ -2466,3 +2483,41 @@ aisleriot_game_deal_cards (AisleriotGame *game)
   aisleriot_game_end_move (game);
   aisleriot_game_test_end_of_game (game);
 }
+
+#ifdef HAVE_CLUTTER
+
+void
+aisleriot_game_get_card_offset (ArSlot *slot,
+                                guint card_num,
+                                gboolean old_cards,
+                                gint *xoff, gint *yoff)
+{
+  gint n_cards, exposed;
+
+  if (old_cards) {
+    n_cards = (gint) slot->old_cards->len;
+    exposed = (gint) slot->old_exposed;
+  } else {
+    n_cards = (gint) slot->cards->len;
+    exposed = (gint) slot->exposed;
+  }
+
+  if (card_num >= n_cards - exposed) {
+    gint idx = card_num + exposed - n_cards;
+    *xoff = slot->pixeldx * idx;
+    *yoff = slot->pixeldy * idx;
+  } else {
+    *xoff = 0;
+    *yoff = 0;
+  }
+}
+
+void
+aisleriot_game_reset_old_cards (ArSlot *slot)
+{
+  g_byte_array_set_size (slot->old_cards, 0);
+  g_byte_array_append (slot->old_cards, slot->cards->data, slot->cards->len);
+  slot->old_exposed = slot->exposed;
+}
+
+#endif /* HAVE_CLUTTER */
diff --git a/aisleriot/game.h b/aisleriot/game.h
index 448886b..6d4a0a1 100644
--- a/aisleriot/game.h
+++ b/aisleriot/game.h
@@ -22,6 +22,10 @@
 
 #include "ar-card.h"
 
+#ifdef HAVE_CLUTTER
+#include <clutter/clutter.h>
+#endif
+
 G_BEGIN_DECLS
 
 /* A slot */
@@ -40,6 +44,11 @@ typedef struct {
   ArSlotType type;
 
   GByteArray *cards;
+#ifdef HAVE_CLUTTER
+  /* The old state of the cards so we can check for differences */
+  guint old_exposed;
+  GByteArray *old_cards;
+#endif /* HAVE_CLUTTER */
 
   /* the topmost |exposed| cards are shown on the pile */
   guint exposed;
@@ -61,8 +70,13 @@ typedef struct {
   /* The location in pixel units. Filled in by the scaling code. */
   GdkRectangle rect;
 
+#ifdef HAVE_CLUTTER
+  /* Actor for the slot */
+  ClutterActor *slot_renderer;
+#else
   /* GdkPixbuf* or GdkPixmap*, no reference owned */
   GPtrArray *card_images;
+#endif /* HAVE_CLUTTER */
 
   guint expanded_right : 1;
   guint expanded_down : 1;
diff --git a/aisleriot/lib/Makefile.am b/aisleriot/lib/Makefile.am
index a1aa528..833daba 100644
--- a/aisleriot/lib/Makefile.am
+++ b/aisleriot/lib/Makefile.am
@@ -28,6 +28,13 @@ libaisleriot_la_SOURCES += \
 	ar-card-surface-cache.h \
 	$(NULL)
 
+if HAVE_CLUTTER
+libaisleriot_la_SOURCES += \
+	ar-card-textures-cache.c \
+	ar-card-textures-cache.h \
+	$(NULL)
+endif # HAVE_CLUTTER
+
 if HAVE_RSVG
 libaisleriot_la_SOURCES += ar-card-theme-preimage.c
 
@@ -79,6 +86,11 @@ libaisleriot_la_CFLAGS += $(RSVG_CFLAGS)
 libaisleriot_la_LIBADD += $(RSVG_LIBS)
 endif # HAVE_RSVG
 
+if HAVE_CLUTTER
+libaisleriot_la_CFLAGS += $(CLUTTER_CFLAGS)
+libaisleriot_la_LIBADD += $(CLUTTER_LIBS)
+endif # HAVE_CLUTTER
+
 if HAVE_HILDON
 libaisleriot_la_CFLAGS += $(HILDON_CFLAGS)
 libaisleriot_la_LIBADD += $(HILDON_LIBS)
diff --git a/aisleriot/lib/ar-card-textures-cache.c b/aisleriot/lib/ar-card-textures-cache.c
new file mode 100644
index 0000000..b4bea44
--- /dev/null
+++ b/aisleriot/lib/ar-card-textures-cache.c
@@ -0,0 +1,374 @@
+/*
+  Copyright © 2008 Neil Roberts
+  Copyright © 2008 Christian Persch
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <config.h>
+
+#include <glib-object.h>
+#include <gtk/gtk.h>
+
+#include <cogl/cogl.h>
+
+#include <libgames-support/games-debug.h>
+
+#include "ar-card-textures-cache.h"
+#include "ar-card-private.h"
+
+struct _ArCardTexturesCachePrivate
+{
+  ArCardTheme *theme;
+  guint theme_changed_handler;
+
+  CoglHandle *cards;
+
+#ifdef GNOME_ENABLE_DEBUG
+  guint n_calls;
+  guint cache_hits;
+#endif
+};
+
+enum
+{
+  PROP_0,
+  PROP_THEME
+};
+
+/* This is an invalid value for a CoglHandle, and distinct from COGL_INVALID_HANDLE */
+#define FAILED_HANDLE ((gpointer) 0x1)
+#define IS_FAILED_HANDLE(ptr) (G_UNLIKELY ((ptr) == FAILED_HANDLE))
+
+/* Logging */
+#ifdef GNOME_ENABLE_DEBUG
+#define LOG_CALL(obj) obj->priv->n_calls++
+#define LOG_CACHE_HIT(obj) obj->priv->cache_hits++
+#define LOG_CACHE_MISS(obj)
+#else
+#define LOG_CALL(obj)
+#define LOG_CACHE_HIT(obj)
+#define LOG_CACHE_MISS(obj)
+#endif /* GNOME_ENABLE_DEBUG */
+
+static void ar_card_textures_cache_dispose (GObject *object);
+static void ar_card_textures_cache_finalize (GObject *object);
+
+G_DEFINE_TYPE (ArCardTexturesCache, ar_card_textures_cache, G_TYPE_OBJECT);
+
+#define AR_CARD_TEXTURES_CACHE_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), AR_TYPE_CARD_TEXTURES_CACHE, ArCardTexturesCachePrivate))
+
+/* Helper functions */
+
+static void
+ar_card_textures_cache_clear (ArCardTexturesCache *cache)
+{
+  ArCardTexturesCachePrivate *priv = cache->priv;
+  int i;
+
+  _games_debug_print (GAMES_DEBUG_CARD_CACHE,
+                      "ar_card_textures_cache_clear\n");
+
+  for (i = 0; i < AR_CARDS_TOTAL; i++) {
+    CoglHandle handle = priv->cards[i];
+
+    if (handle != COGL_INVALID_HANDLE &&
+        !IS_FAILED_HANDLE (handle)) {
+      cogl_handle_unref (handle);
+    }
+
+    priv->cards[i] = COGL_INVALID_HANDLE;
+  }
+}
+
+static void
+ar_card_textures_cache_unset_theme (ArCardTexturesCache *cache)
+{
+  ArCardTexturesCachePrivate *priv = cache->priv;
+
+  if (priv->theme) {
+    g_signal_handler_disconnect (priv->theme, priv->theme_changed_handler);
+    g_object_unref (priv->theme);
+    priv->theme = NULL;
+    priv->theme_changed_handler = 0;
+  }
+}
+
+/* Class implementation */
+
+static void
+ar_card_textures_cache_init (ArCardTexturesCache *self)
+{
+  ArCardTexturesCachePrivate *priv;
+
+  priv = self->priv = AR_CARD_TEXTURES_CACHE_GET_PRIVATE (self);
+
+  priv->cards = g_malloc0 (sizeof (CoglHandle) * AR_CARDS_TOTAL);
+}
+
+static void
+ar_card_textures_cache_dispose (GObject *object)
+{
+  ArCardTexturesCache *cache = AR_CARD_TEXTURES_CACHE (object);
+
+  ar_card_textures_cache_clear (cache);
+  ar_card_textures_cache_unset_theme (cache);
+
+  G_OBJECT_CLASS (ar_card_textures_cache_parent_class)->dispose (object);
+}
+
+static void
+ar_card_textures_cache_finalize (GObject *object)
+{
+  ArCardTexturesCache *cache = AR_CARD_TEXTURES_CACHE (object);
+  ArCardTexturesCachePrivate *priv = cache->priv;
+
+  g_free (priv->cards);
+
+#ifdef GNOME_ENABLE_DEBUG
+  _GAMES_DEBUG_IF (GAMES_DEBUG_CARD_CACHE) {
+    _games_debug_print (GAMES_DEBUG_CARD_CACHE,
+                        "ArCardTexturesCache %p statistics: %u calls with %u hits and %u misses for a hit/total of %.3f\n",
+                        cache, priv->n_calls, priv->cache_hits, priv->n_calls - priv->cache_hits,
+                        priv->n_calls > 0 ? (double) priv->cache_hits / (double) priv->n_calls : 0.0);
+  }
+#endif
+
+  G_OBJECT_CLASS (ar_card_textures_cache_parent_class)->finalize (object);
+}
+
+static void
+ar_card_textures_cache_set_property (GObject *self,
+                                        guint property_id,
+                                        const GValue *value,
+                                        GParamSpec *pspec)
+{
+  ArCardTexturesCache *cache = AR_CARD_TEXTURES_CACHE (self);
+
+  switch (property_id) {
+    case PROP_THEME:
+      ar_card_textures_cache_set_theme (cache, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (self, property_id, pspec);
+      break;
+    }
+}
+
+static void
+ar_card_textures_cache_get_property (GObject *self,
+                                        guint property_id,
+                                        GValue *value,
+                                        GParamSpec *pspec)
+{
+  ArCardTexturesCache *cache = AR_CARD_TEXTURES_CACHE (self);
+
+  switch (property_id) {
+    case PROP_THEME:
+      g_value_set_object (value, ar_card_textures_cache_get_theme (cache));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (self, property_id, pspec);
+      break;
+    }
+}
+
+static void
+ar_card_textures_cache_class_init (ArCardTexturesCacheClass *klass)
+{
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+  GParamSpec *pspec;
+
+  gobject_class->dispose = ar_card_textures_cache_dispose;
+  gobject_class->finalize = ar_card_textures_cache_finalize;
+  gobject_class->set_property = ar_card_textures_cache_set_property;
+  gobject_class->get_property = ar_card_textures_cache_get_property;
+
+  g_type_class_add_private (klass, sizeof (ArCardTexturesCachePrivate));
+
+  pspec = g_param_spec_object ("theme", NULL, NULL,
+                               AR_TYPE_CARD_THEME,
+                               G_PARAM_WRITABLE |
+                               G_PARAM_CONSTRUCT_ONLY |
+                               G_PARAM_STATIC_NAME |
+                               G_PARAM_STATIC_NICK |
+                               G_PARAM_STATIC_BLURB);
+  g_object_class_install_property (gobject_class, PROP_THEME, pspec);
+}
+
+/* Public API */
+
+/**
+ * ar_card_textures_cache_new:
+ *
+ * Returns: a new #ArCardTexturesCache object
+ */
+ArCardTexturesCache *
+ar_card_textures_cache_new (void)
+{
+  return g_object_new (AR_TYPE_CARD_TEXTURES_CACHE, NULL);
+}
+
+/**
+ * ar_card_textures_cache_drop:
+ * @images: a #ArCardImages
+ *
+ * Clears the image cache.
+ */
+void
+ar_card_textures_cache_drop (ArCardTexturesCache *cache)
+{
+  g_return_if_fail (AR_IS_CARD_TEXTURES_CACHE (cache));
+
+  ar_card_textures_cache_clear (cache);
+}
+
+/**
+ * ar_card_textures_cache_set_theme:
+ * @cache:
+ * @theme:
+ *
+ * Sets the card theme.
+ */
+void
+ar_card_textures_cache_set_theme (ArCardTexturesCache *cache,
+                                     ArCardTheme *theme)
+{
+  ArCardTexturesCachePrivate *priv = cache->priv;
+
+  g_return_if_fail (AR_IS_CARD_TEXTURES_CACHE (cache));
+  g_return_if_fail (theme == NULL || AR_IS_CARD_THEME (theme));
+
+  if (priv->theme == theme)
+    return;
+
+  ar_card_textures_cache_clear (cache);
+  ar_card_textures_cache_unset_theme (cache);
+
+  priv->theme = theme;
+  if (theme) {
+    g_object_ref (theme);
+
+    priv->theme_changed_handler = g_signal_connect_swapped (theme, "changed",
+                                                            G_CALLBACK (ar_card_textures_cache_clear),
+                                                            cache);
+  }
+
+  g_object_notify (G_OBJECT (cache), "theme");
+}
+
+/**
+ * ar_card_textures_cache_get_theme:
+ * @cache:
+ *
+ * Returns: the the card theme of @cache
+ */
+ArCardTheme *
+ar_card_textures_cache_get_theme (ArCardTexturesCache *cache)
+{
+  g_return_val_if_fail (AR_IS_CARD_TEXTURES_CACHE (cache), NULL);
+
+  return cache->priv->theme;
+}
+
+/**
+ * ar_card_textures_cache_get_card_texture_by_id:
+ * @cache:
+ * @card_id:
+ *
+ * Returns: a cached #CoglHandle for @card_id.
+ */
+CoglHandle
+ar_card_textures_cache_get_card_texture_by_id (ArCardTexturesCache *cache,
+                                                  guint card_id)
+{
+  ArCardTexturesCachePrivate *priv = cache->priv;
+  CoglHandle handle;
+
+  g_return_val_if_fail (card_id < AR_CARDS_TOTAL , NULL);
+
+  LOG_CALL (cache);
+
+  handle = priv->cards[card_id];
+  if (IS_FAILED_HANDLE (handle)) {
+    LOG_CACHE_HIT (cache);
+    return COGL_INVALID_HANDLE;
+  }
+
+  if (handle == COGL_INVALID_HANDLE) {
+    GdkPixbuf *pixbuf;
+
+    LOG_CACHE_MISS (cache);
+
+    pixbuf = ar_card_theme_get_card_pixbuf (priv->theme, card_id);
+    if (!pixbuf) {
+      priv->cards[card_id] = FAILED_HANDLE;
+      return COGL_INVALID_HANDLE;
+    }
+
+    handle = cogl_texture_new_from_data (gdk_pixbuf_get_width (pixbuf),
+                                         gdk_pixbuf_get_height (pixbuf),
+                                         COGL_TEXTURE_NONE,
+                                         gdk_pixbuf_get_has_alpha (pixbuf)
+                                         ? COGL_PIXEL_FORMAT_RGBA_8888
+                                         : COGL_PIXEL_FORMAT_RGB_888,
+                                         COGL_PIXEL_FORMAT_ANY,
+                                         gdk_pixbuf_get_rowstride (pixbuf),
+                                         gdk_pixbuf_get_pixels (pixbuf));
+    g_object_unref (pixbuf);
+
+    if (handle == COGL_INVALID_HANDLE) {
+      priv->cards[card_id] = FAILED_HANDLE;
+      return COGL_INVALID_HANDLE;
+    }
+
+    priv->cards[card_id] = handle;
+  } else {
+    LOG_CACHE_HIT (cache);
+  }
+
+  return handle;
+}
+
+/**
+ * ar_card_textures_cache_get_card_texture:
+ * @cache:
+ * @card:
+ * @highlighted:
+ *
+ * Returns: a cached #CoglHandle for @card.
+ */
+CoglHandle
+ar_card_textures_cache_get_card_texture (ArCardTexturesCache *cache,
+                                            Card card)
+{
+  guint card_id = _ar_card_to_index (card);
+
+  return ar_card_textures_cache_get_card_texture_by_id (cache, card_id);
+}
+
+/**
+ * ar_card_textures_cache_get_slot_texture:
+ * @cache:
+ * @highlighted:
+ *
+ * Returns: a cached #CoglHandle for the slot.
+ */
+CoglHandle
+ar_card_textures_cache_get_slot_texture (ArCardTexturesCache *cache)
+{
+  return ar_card_textures_cache_get_card_texture_by_id (cache, AR_CARD_SLOT);
+}
diff --git a/aisleriot/lib/ar-card-textures-cache.h b/aisleriot/lib/ar-card-textures-cache.h
new file mode 100644
index 0000000..3f281c4
--- /dev/null
+++ b/aisleriot/lib/ar-card-textures-cache.h
@@ -0,0 +1,74 @@
+/*
+  Copyright © 2008 Neil Roberts
+  Copyright © 2008 Christian Persch
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef AR_CARD_TEXTURES_CACHE_H
+#define AR_CARD_TEXTURES_CACHE_H
+
+#include <glib-object.h>
+#include <cogl/cogl.h>
+
+#include "ar-card.h"
+#include "ar-card-theme.h"
+
+G_BEGIN_DECLS
+
+#define AR_TYPE_CARD_TEXTURES_CACHE            (ar_card_textures_cache_get_type())
+#define AR_CARD_TEXTURES_CACHE(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), AR_TYPE_CARD_TEXTURES_CACHE, ArCardTexturesCache))
+#define AR_CARD_TEXTURES_CACHE_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass), AR_TYPE_CARD_TEXTURES_CACHE, ArCardTexturesCacheClass))
+#define AR_IS_CARD_TEXTURES_CACHE(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), AR_TYPE_CARD_TEXTURES_CACHE))
+#define AR_IS_CARD_TEXTURES_CACHE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), AR_TYPE_CARD_TEXTURES_CACHE))
+#define AR_CARD_TEXTURES_CACHE_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj), AR_TYPE_CARD_TEXTURES_CACHE, ArCardTexturesCacheClass))
+
+typedef struct _ArCardTexturesCache        ArCardTexturesCache;
+typedef struct _ArCardTexturesCacheClass   ArCardTexturesCacheClass;
+typedef struct _ArCardTexturesCachePrivate ArCardTexturesCachePrivate;
+
+struct _ArCardTexturesCacheClass
+{
+  GObjectClass parent_class;
+};
+
+struct _ArCardTexturesCache
+{
+  GObject parent;
+
+  ArCardTexturesCachePrivate *priv;
+};
+
+GType ar_card_textures_cache_get_type (void);
+
+ArCardTexturesCache *ar_card_textures_cache_new (void);
+
+void ar_card_textures_cache_drop (ArCardTexturesCache *cache);
+
+void ar_card_textures_cache_set_theme (ArCardTexturesCache *cache,
+                                          ArCardTheme *theme);
+
+ArCardTheme *ar_card_textures_cache_get_theme (ArCardTexturesCache *cache);
+
+CoglHandle ar_card_textures_cache_get_card_texture (ArCardTexturesCache *cache,
+                                                       Card card);
+
+CoglHandle ar_card_textures_cache_get_card_texture_by_id (ArCardTexturesCache *cache,
+                                                             guint card_id);
+
+CoglHandle ar_card_textures_cache_get_slot_texture (ArCardTexturesCache *cache);
+
+G_END_DECLS
+
+#endif /* AR_CARD_TEXTURES_CACHE_H */
diff --git a/aisleriot/prop-editor.c b/aisleriot/prop-editor.c
index 323b920..11429c8 100644
--- a/aisleriot/prop-editor.c
+++ b/aisleriot/prop-editor.c
@@ -27,6 +27,10 @@
 
 #include "prop-editor.h"
 
+#ifdef HAVE_CLUTTER
+#include <clutter/clutter.h>
+#endif
+
 #ifdef HAVE_MAEMO_5
 #include <hildon/hildon-gtk.h>
 #include <hildon/hildon-pannable-area.h>
@@ -712,6 +716,81 @@ color_changed (GObject *object, GParamSpec *pspec, gpointer data)
   g_value_unset (&val);
 }
 
+#ifdef HAVE_CLUTTER
+
+static void
+_clutter_color_from_gdk_color (ClutterColor *clutter_color,
+                               const GdkColor *gdk_color)
+{
+  clutter_color->red   = gdk_color->red   >> 8;
+  clutter_color->green = gdk_color->green >> 8;
+  clutter_color->blue  = gdk_color->blue  >> 8;
+  clutter_color->alpha = 0xff;
+}
+
+static void
+_clutter_color_to_gdk_color (GdkColor *gdk_color,
+                             const ClutterColor *clutter_color)
+{
+  gdk_color->red   = clutter_color->red   << 8;
+  gdk_color->green = clutter_color->green << 8;
+  gdk_color->blue  = clutter_color->blue  << 8;
+  gdk_color->pixel = 0;
+}
+
+static void
+cluttercolor_modified (GtkColorButton *cb, gpointer data)
+{
+  ObjectProperty *p = data;
+  GdkColor color;
+  ClutterColor ccolor;
+
+  gtk_color_button_get_color (cb, &color);
+  _clutter_color_from_gdk_color (&ccolor, &color);
+
+  if (is_child_property (p->spec))
+    {
+      GtkWidget *widget = GTK_WIDGET (p->obj);
+      GtkWidget *parent = gtk_widget_get_parent (widget);
+
+      gtk_container_child_set (GTK_CONTAINER (parent),
+			       widget, p->spec->name, &ccolor, NULL);
+    }
+  else
+    g_object_set (p->obj, p->spec->name, &ccolor, NULL);
+}
+
+static void
+cluttercolor_changed (GObject *object, GParamSpec *pspec, gpointer data)
+{
+  GtkColorButton *cb = GTK_COLOR_BUTTON (data);
+  GValue val = { 0, };
+  GdkColor cb_color;
+  ClutterColor *color;
+  ClutterColor cb_ccolor;
+
+  g_assert (G_PARAM_SPEC_VALUE_TYPE (pspec) == CLUTTER_TYPE_COLOR);
+
+  g_value_init (&val, CLUTTER_TYPE_COLOR);
+  get_property_value (object, pspec, &val);
+  color = g_value_get_boxed (&val);
+
+  gtk_color_button_get_color (cb, &cb_color);
+  _clutter_color_from_gdk_color (&cb_ccolor, &cb_color);
+
+  if (color != NULL && !clutter_color_equal (color, &cb_ccolor))
+    {
+      block_controller (G_OBJECT (cb));
+      _clutter_color_to_gdk_color (&cb_color, color);
+      gtk_color_button_set_color (cb, &cb_color);
+      unblock_controller (G_OBJECT (cb));
+    }
+
+  g_value_unset (&val);
+}
+
+#endif /* HAVE_CLUTTER */
+
 static GtkWidget *
 property_widget (GObject    *object, 
 		 GParamSpec *spec, 
@@ -950,6 +1029,22 @@ property_widget (GObject    *object,
 	connect_controller (G_OBJECT (prop_edit), "color-set",
 			    object, spec, G_CALLBACK (color_modified));
     }
+#ifdef HAVE_CLUTTER
+  else if ((type == G_TYPE_PARAM_BOXED &&
+            G_PARAM_SPEC_VALUE_TYPE (spec) == CLUTTER_TYPE_COLOR) ||
+           type == CLUTTER_TYPE_PARAM_COLOR)
+    {
+      prop_edit = gtk_color_button_new ();
+      
+      g_object_connect_property (object, spec, 
+				 G_CALLBACK (cluttercolor_changed),
+				 prop_edit, G_OBJECT (prop_edit));
+      
+      if (can_modify)
+	connect_controller (G_OBJECT (prop_edit), "color-set",
+			    object, spec, G_CALLBACK (cluttercolor_modified));
+    }
+#endif /* HAVE_CLUTTER */
   else
     {  
       msg = g_strdup_printf ("uneditable property type: %s",
diff --git a/aisleriot/slot-renderer.c b/aisleriot/slot-renderer.c
new file mode 100644
index 0000000..8e233c7
--- /dev/null
+++ b/aisleriot/slot-renderer.c
@@ -0,0 +1,691 @@
+/*
+ * Copyright © 2008 Neil Roberts
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <config.h>
+
+#include <clutter/clutter.h>
+#include <gtk/gtk.h>
+#include <cogl/cogl.h>
+#include <string.h>
+#include <math.h>
+
+#include "slot-renderer.h"
+#include "card.h"
+
+#include <libgames-support/games-glib-compat.h>
+
+static void aisleriot_slot_renderer_dispose (GObject *object);
+static void aisleriot_slot_renderer_finalize (GObject *object);
+
+static void aisleriot_slot_renderer_set_property (GObject *object,
+                                                  guint property_id,
+                                                  const GValue *value,
+                                                  GParamSpec *pspec);
+static void aisleriot_slot_renderer_get_property (GObject *object,
+                                                  guint property_id,
+                                                  GValue *value,
+                                                  GParamSpec *pspec);
+
+static void aisleriot_slot_renderer_paint (ClutterActor *actor);
+
+static void aisleriot_slot_renderer_set_cache (AisleriotSlotRenderer *srend,
+                                               ArCardTexturesCache *cache);
+
+static void completed_cb (AisleriotSlotRenderer *srend);
+
+
+G_DEFINE_TYPE (AisleriotSlotRenderer, aisleriot_slot_renderer,
+               CLUTTER_TYPE_ACTOR);
+
+#define AISLERIOT_SLOT_RENDERER_GET_PRIVATE(obj) \
+  (G_TYPE_INSTANCE_GET_PRIVATE ((obj), AISLERIOT_TYPE_SLOT_RENDERER, \
+                                AisleriotSlotRendererPrivate))
+
+typedef struct _AnimationData AnimationData;
+
+struct _AisleriotSlotRendererPrivate
+{
+  ArStyle *style;
+
+  ArCardTexturesCache *cache;
+
+  ArSlot *slot;
+
+  CoglHandle material;
+
+  ClutterColor highlight_color;
+  gboolean show_highlight;
+  gint highlight_start;
+
+  gint revealed_card;
+
+  ClutterTimeline *timeline;
+  guint completed_handler;
+  GArray *animations;
+  guint n_unexposed_animated_cards;
+
+  ClutterContainer *animation_layer;
+};
+
+struct _AnimationData
+{
+  ClutterActor *card_tex;
+  ClutterBehaviour *move, *rotate, *depth;
+};
+
+enum
+{
+  PROP_0,
+
+  PROP_CACHE,
+  PROP_SLOT,
+  PROP_STYLE,
+  PROP_HIGHLIGHT,
+  PROP_REVEALED_CARD,
+  PROP_ANIMATION_LAYER
+};
+
+static void
+sync_style_selection_color (ArStyle *style,
+                            GParamSpec *pspec,
+                            AisleriotSlotRenderer *srend)
+{
+  AisleriotSlotRendererPrivate *priv = srend->priv;
+  ClutterColor color;
+
+  ar_style_get_selection_color (style, &color);
+  if (clutter_color_equal (&color, &priv->highlight_color))
+    return;
+
+  priv->highlight_color = color;
+
+  if (priv->show_highlight)
+    clutter_actor_queue_redraw (CLUTTER_ACTOR (srend));
+}
+
+static void
+aisleriot_slot_renderer_class_init (AisleriotSlotRendererClass *klass)
+{
+  GObjectClass *gobject_class = (GObjectClass *) klass;
+  ClutterActorClass *actor_class = (ClutterActorClass *) klass;
+  GParamSpec *pspec;
+
+  gobject_class->dispose = aisleriot_slot_renderer_dispose;
+  gobject_class->finalize = aisleriot_slot_renderer_finalize;
+
+  gobject_class->set_property = aisleriot_slot_renderer_set_property;
+  gobject_class->get_property = aisleriot_slot_renderer_get_property;
+
+  actor_class->paint = aisleriot_slot_renderer_paint;
+
+  g_object_class_install_property
+    (gobject_class,
+     PROP_STYLE,
+     g_param_spec_object ("style", NULL, NULL,
+                          AR_TYPE_STYLE,
+                          G_PARAM_WRITABLE |
+                          G_PARAM_CONSTRUCT_ONLY |
+                          G_PARAM_STATIC_STRINGS));
+
+  pspec = g_param_spec_object ("cache", NULL, NULL,
+                               AR_TYPE_CARD_TEXTURES_CACHE,
+                               G_PARAM_WRITABLE |
+                               G_PARAM_CONSTRUCT_ONLY |
+                               G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (gobject_class, PROP_CACHE, pspec);
+
+  pspec = g_param_spec_pointer ("slot", NULL, NULL,
+                                G_PARAM_WRITABLE |
+                                G_PARAM_CONSTRUCT_ONLY |
+                                G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (gobject_class, PROP_SLOT, pspec);
+
+  pspec = g_param_spec_int ("highlight", NULL, NULL,
+                            -1, G_MAXINT, G_MAXINT,
+                            G_PARAM_READWRITE |
+                            G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (gobject_class, PROP_HIGHLIGHT, pspec);
+
+  pspec = g_param_spec_int ("revealed-card", NULL, NULL,
+                            -1, G_MAXINT, -1,
+                            G_PARAM_READWRITE |
+                            G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (gobject_class, PROP_REVEALED_CARD, pspec);
+
+  pspec = g_param_spec_object ("animation-layer", NULL, NULL,
+                               CLUTTER_TYPE_CONTAINER,
+                               G_PARAM_READWRITE |
+                               G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (gobject_class, PROP_ANIMATION_LAYER, pspec);
+
+  g_type_class_add_private (klass, sizeof (AisleriotSlotRendererPrivate));
+}
+
+static void
+aisleriot_slot_renderer_init (AisleriotSlotRenderer *self)
+{
+  AisleriotSlotRendererPrivate *priv;
+
+  priv = self->priv = AISLERIOT_SLOT_RENDERER_GET_PRIVATE (self);
+
+  priv->revealed_card = -1;
+  priv->highlight_start = G_MAXINT;
+
+  priv->animations = g_array_new (FALSE, FALSE, sizeof (AnimationData));
+  priv->timeline = clutter_timeline_new (500);
+  g_signal_connect_swapped (priv->timeline, "completed",
+                            G_CALLBACK (completed_cb), self);
+}
+
+static void
+aisleriot_slot_renderer_dispose (GObject *object)
+{
+  AisleriotSlotRenderer *self = (AisleriotSlotRenderer *) object;
+  AisleriotSlotRendererPrivate *priv = self->priv;
+
+  aisleriot_slot_renderer_set_cache (self, NULL);
+
+  /* Get rid of any running animations */
+  aisleriot_slot_renderer_set_animations (self, 0, NULL, 0);
+
+  if (priv->timeline) {
+    g_object_unref (priv->timeline);
+    priv->timeline = NULL;
+  }
+
+  if (priv->material != COGL_INVALID_HANDLE) {
+    cogl_handle_unref (priv->material);
+    priv->material = COGL_INVALID_HANDLE;
+  }
+
+  aisleriot_slot_renderer_set_animation_layer (self, NULL);
+
+  G_OBJECT_CLASS (aisleriot_slot_renderer_parent_class)->dispose (object);
+}
+
+static void
+aisleriot_slot_renderer_finalize (GObject *object)
+{
+  AisleriotSlotRenderer *srend = AISLERIOT_SLOT_RENDERER (object);
+  AisleriotSlotRendererPrivate *priv = srend->priv;
+
+  g_array_free (priv->animations, TRUE);
+
+  g_signal_handlers_disconnect_by_func (priv->style,
+                                        G_CALLBACK (sync_style_selection_color),
+                                        srend);
+  g_object_unref (priv->style);
+
+  G_OBJECT_CLASS (aisleriot_slot_renderer_parent_class)->finalize (object);
+}
+
+ClutterActor *
+aisleriot_slot_renderer_new (ArStyle *style,
+                             ArCardTexturesCache *cache,
+                             ArSlot *slot)
+{
+  return g_object_new (AISLERIOT_TYPE_SLOT_RENDERER,
+                       "style", style,
+                       "cache", cache,
+                       "slot", slot,
+                       NULL);
+}
+
+static void
+aisleriot_slot_renderer_set_cache (AisleriotSlotRenderer *srend,
+                                   ArCardTexturesCache *cache)
+{
+  AisleriotSlotRendererPrivate *priv = srend->priv;
+
+  if (cache == priv->cache)
+    return;
+
+  if (cache)
+    g_object_ref (cache);
+
+  if (priv->cache)
+    g_object_unref (priv->cache);
+
+  priv->cache = cache;
+}
+
+static void
+aisleriot_slot_renderer_set_property (GObject *object,
+                                      guint property_id,
+                                      const GValue *value,
+                                      GParamSpec *pspec)
+{
+  AisleriotSlotRenderer *srend = AISLERIOT_SLOT_RENDERER (object);
+  AisleriotSlotRendererPrivate *priv = srend->priv;
+
+  switch (property_id) {
+    case PROP_CACHE:
+      aisleriot_slot_renderer_set_cache (srend, g_value_get_object (value));
+      break;
+
+    case PROP_SLOT:
+      priv->slot = g_value_get_pointer (value);
+      break;
+
+    case PROP_STYLE:
+      priv->style = g_value_dup_object (value);
+
+      sync_style_selection_color (priv->style, NULL, srend);
+      g_signal_connect (priv->style, "notify::" AR_STYLE_PROP_SELECTION_COLOR,
+                        G_CALLBACK (sync_style_selection_color), srend);
+      break;
+
+    case PROP_HIGHLIGHT:
+      aisleriot_slot_renderer_set_highlight (srend,
+                                             g_value_get_int (value));
+      break;
+
+    case PROP_REVEALED_CARD:
+      aisleriot_slot_renderer_set_revealed_card (srend,
+                                                 g_value_get_int (value));
+      break;
+
+    case PROP_ANIMATION_LAYER:
+      aisleriot_slot_renderer_set_animation_layer (srend,
+                                                   g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+      break;
+  }
+}
+
+static void
+aisleriot_slot_renderer_get_property (GObject *object,
+                                      guint property_id,
+                                      GValue *value,
+                                      GParamSpec *pspec)
+{
+  AisleriotSlotRenderer *srend = AISLERIOT_SLOT_RENDERER (object);
+
+  switch (property_id) {
+    case PROP_HIGHLIGHT:
+      g_value_set_int (value,
+                        aisleriot_slot_renderer_get_highlight (srend));
+      break;
+
+    case PROP_REVEALED_CARD:
+      g_value_set_int (value,
+                       aisleriot_slot_renderer_get_revealed_card (srend));
+      break;
+
+    case PROP_ANIMATION_LAYER:
+      g_value_set_object (value,
+                          aisleriot_slot_renderer_get_animation_layer (srend));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+      break;
+  }
+}
+
+static void
+aisleriot_slot_renderer_set_material_for_card (AisleriotSlotRenderer *srend,
+                                               CoglHandle tex,
+                                               gboolean show_highlight)
+{
+  AisleriotSlotRendererPrivate *priv = srend->priv;
+  guint8 opacity = clutter_actor_get_paint_opacity (CLUTTER_ACTOR (srend));
+
+  if (priv->material == COGL_INVALID_HANDLE)
+    priv->material = cogl_material_new ();
+
+  if (show_highlight)
+    {
+      CoglColor color;
+
+      /* The previous code for drawing the highlight rendered the
+         normal card texture and then rendered the card again
+         multiplied by the highlight color but with 50%
+         transparency. The blend function is alpha*src+(1-alpha*dst)
+         where src is effectively the tex times the highlight color
+         and the dst is the original tex. Therefore the final color is
+         0.5*tex+0.5*tex*highlight which is the same as
+         (0.5+highlight/2)*tex. We can precompute that value to avoid
+         having to draw the card twice */
+      cogl_color_set_from_4ub (&color,
+                               MIN (priv->highlight_color.red
+                                    / 2 + 128, 0xff),
+                               MIN (priv->highlight_color.green
+                                    / 2 + 128, 0xff),
+                               MIN (priv->highlight_color.blue
+                                    / 2 + 128, 0xff),
+                               opacity);
+      cogl_color_premultiply (&color);
+      cogl_material_set_color (priv->material, &color);
+    }
+  else
+    cogl_material_set_color4ub (priv->material,
+                                opacity, opacity, opacity, opacity);
+
+  cogl_material_set_layer (priv->material, 0, tex);
+  cogl_set_source (priv->material);
+}
+
+static void
+aisleriot_slot_renderer_paint_card (AisleriotSlotRenderer *srend,
+                                    guint card_num)
+{
+  AisleriotSlotRendererPrivate *priv = srend->priv;
+  Card card = CARD (priv->slot->cards->data[card_num]);
+  CoglHandle cogl_tex;
+  guint tex_width, tex_height;
+  int cardx, cardy;
+
+  cogl_tex = ar_card_textures_cache_get_card_texture (priv->cache, card);
+  if (G_UNLIKELY (cogl_tex == COGL_INVALID_HANDLE))
+    return;
+
+  tex_width = cogl_texture_get_width (cogl_tex);
+  tex_height = cogl_texture_get_height (cogl_tex);
+
+  aisleriot_game_get_card_offset (priv->slot, card_num,
+                                  FALSE,
+                                  &cardx, &cardy);
+
+  aisleriot_slot_renderer_set_material_for_card
+    (srend, cogl_tex,
+     priv->show_highlight && card_num >= priv->highlight_start);
+
+  cogl_rectangle (cardx, cardy, cardx + tex_width, cardy + tex_height);
+}
+
+static void
+aisleriot_slot_renderer_paint (ClutterActor *actor)
+{
+  AisleriotSlotRenderer *srend = (AisleriotSlotRenderer *) actor;
+  AisleriotSlotRendererPrivate *priv = srend->priv;
+  guint n_cards, n_animated_cards;
+  guint8 *cards;
+  guint i;
+
+  g_return_if_fail (priv->cache != NULL);
+  g_return_if_fail (priv->slot != NULL);
+
+  cards = priv->slot->cards->data;
+  n_cards = priv->slot->cards->len;
+  n_animated_cards = priv->animations->len + priv->n_unexposed_animated_cards;
+
+  g_assert (n_cards >= priv->slot->exposed);
+  g_assert (priv->n_unexposed_animated_cards == 0 || priv->animations->len > 0);
+  g_assert (n_cards >= n_animated_cards);
+
+  if (n_cards <= n_animated_cards) {
+    CoglHandle cogl_tex;
+    guint tex_width, tex_height;
+
+    cogl_tex = ar_card_textures_cache_get_slot_texture (priv->cache);
+    if (G_UNLIKELY (cogl_tex == COGL_INVALID_HANDLE))
+      return;
+
+    tex_width = cogl_texture_get_width (cogl_tex);
+    tex_height = cogl_texture_get_height (cogl_tex);
+
+    aisleriot_slot_renderer_set_material_for_card (srend, cogl_tex,
+                                                   priv->show_highlight);
+    cogl_rectangle (0, 0, tex_width, tex_height);
+  } else {
+    guint first_card, last_card;
+
+    first_card = MIN (n_cards - n_animated_cards - 1,
+                      n_cards - priv->slot->exposed);
+    last_card = n_cards - n_animated_cards;
+
+    for (i = first_card; i < last_card; i++)
+      if (i != priv->revealed_card)
+        aisleriot_slot_renderer_paint_card (srend, i);
+
+    /* Paint the revealed card after all of the other cards so that it
+     * will appeear on top.
+     */
+    if (priv->revealed_card >= first_card && priv->revealed_card < last_card)
+      aisleriot_slot_renderer_paint_card (srend, priv->revealed_card);
+  }
+}
+
+guint
+aisleriot_slot_renderer_get_highlight (AisleriotSlotRenderer *srend)
+{
+  g_return_val_if_fail (AISLERIOT_IS_SLOT_RENDERER (srend), 0);
+
+  return srend->priv->highlight_start;
+}
+
+void
+aisleriot_slot_renderer_set_highlight (AisleriotSlotRenderer *srend,
+                                       gint highlight)
+{
+  AisleriotSlotRendererPrivate *priv;
+
+  g_return_if_fail (AISLERIOT_IS_SLOT_RENDERER (srend));
+
+  priv = srend->priv;
+
+  priv->highlight_start = highlight;
+  priv->show_highlight = priv->highlight_start != G_MAXINT;
+
+  clutter_actor_queue_redraw (CLUTTER_ACTOR (srend));
+
+  g_object_notify (G_OBJECT (srend), "highlight");
+}
+
+gint
+aisleriot_slot_renderer_get_revealed_card (AisleriotSlotRenderer *srend)
+{
+  g_return_val_if_fail (AISLERIOT_IS_SLOT_RENDERER (srend), 0);
+
+  return srend->priv->revealed_card;
+}
+
+void
+aisleriot_slot_renderer_set_revealed_card (AisleriotSlotRenderer *srend,
+                                           gint revealed_card)
+{
+  AisleriotSlotRendererPrivate *priv;
+
+  g_return_if_fail (AISLERIOT_IS_SLOT_RENDERER (srend));
+
+  priv = srend->priv;
+
+  priv->revealed_card = revealed_card;
+
+  clutter_actor_queue_redraw (CLUTTER_ACTOR (srend));
+
+  g_object_notify (G_OBJECT (srend), "revealed-card");
+}
+
+ClutterContainer *
+aisleriot_slot_renderer_get_animation_layer (AisleriotSlotRenderer *srend)
+{
+  AisleriotSlotRendererPrivate *priv;
+
+  g_return_val_if_fail (AISLERIOT_IS_SLOT_RENDERER (srend), NULL);
+
+  priv = srend->priv;
+
+  return priv->animation_layer;
+}
+
+void
+aisleriot_slot_renderer_set_animation_layer (AisleriotSlotRenderer *srend,
+                                             ClutterContainer *animation_layer)
+{
+  AisleriotSlotRendererPrivate *priv;
+
+  g_return_if_fail (AISLERIOT_IS_SLOT_RENDERER (srend));
+
+  priv = srend->priv;
+
+  if (animation_layer)
+    g_object_ref (animation_layer);
+
+  if (priv->animation_layer)
+    g_object_unref (priv->animation_layer);
+
+  priv->animation_layer = animation_layer;
+
+  g_object_notify (G_OBJECT (srend), "animation-layer");
+}
+
+static gdouble
+aisleriot_slot_sine_animation_mode (ClutterAlpha *alpha,
+                                    gpointer data)
+{
+  ClutterTimeline *tl = clutter_alpha_get_timeline (alpha);
+
+  return sin (clutter_timeline_get_progress (tl) * G_PI);
+}
+
+void
+aisleriot_slot_renderer_set_animations (AisleriotSlotRenderer *srend,
+                                        guint n_anims,
+                                        const AisleriotAnimStart *anims,
+                                        guint n_unexposed_animated_cards)
+{
+  AisleriotSlotRendererPrivate *priv;
+  guint i;
+  gint card_num;
+
+  g_return_if_fail (AISLERIOT_IS_SLOT_RENDERER (srend));
+
+  priv = srend->priv;
+
+  g_return_if_fail (n_anims <= priv->slot->exposed);
+
+  /* Destroy the current animations */
+  for (i = 0; i < priv->animations->len; i++) {
+    AnimationData *anim_data;
+
+    anim_data = &g_array_index (priv->animations, AnimationData, i);
+
+    if (anim_data->move)
+      g_object_unref (anim_data->move);
+    if (anim_data->rotate)
+      g_object_unref (anim_data->rotate);
+    if (anim_data->depth)
+      g_object_unref (anim_data->depth);
+
+    clutter_actor_destroy (anim_data->card_tex);
+    g_object_unref (anim_data->card_tex);
+  }
+
+  g_array_set_size (priv->animations, 0);
+
+  card_num = priv->slot->cards->len - n_anims;
+
+  for (i = 0; i < n_anims; i++) {
+    AnimationData anim_data;
+    ClutterAlpha *alpha;
+    ClutterKnot knots[2];
+    Card card = CARD (priv->slot->cards->data[card_num]);
+    guint card_width, card_height;
+
+    memset (&anim_data, 0, sizeof (anim_data));
+
+    anim_data.card_tex = aisleriot_card_new (priv->cache,
+                                             anims[i].old_card,
+                                             card);
+
+    card_width = clutter_actor_get_width (anim_data.card_tex);
+    card_height = clutter_actor_get_height (anim_data.card_tex);
+
+    g_object_ref_sink (anim_data.card_tex);
+    if (priv->animation_layer)
+      clutter_container_add (priv->animation_layer,
+                             CLUTTER_ACTOR (anim_data.card_tex), NULL);
+
+    clutter_actor_set_position (anim_data.card_tex,
+                                anims[i].cardx, anims[i].cardy);
+
+    knots[0].x = anims[i].cardx;
+    knots[0].y = anims[i].cardy;
+
+    aisleriot_game_get_card_offset (priv->slot, card_num, FALSE,
+                                    &knots[1].x, &knots[1].y);
+    knots[1].x += priv->slot->rect.x;
+    knots[1].y += priv->slot->rect.y;
+
+    alpha = clutter_alpha_new_full (priv->timeline, CLUTTER_LINEAR);
+
+    anim_data.move
+      = clutter_behaviour_path_new_with_knots (alpha, knots,
+                                               G_N_ELEMENTS (knots));
+    clutter_behaviour_apply (anim_data.move, anim_data.card_tex);
+
+    if (anims[i].old_card.value != card.value) {
+      int center_x, center_y;
+
+      center_x = card_width / 2;
+      center_y = card_height / 2;
+
+      clutter_actor_set_rotation (anim_data.card_tex, CLUTTER_Y_AXIS,
+                                  180.0,
+                                  center_x, center_y, 0);
+
+      anim_data.rotate = clutter_behaviour_rotate_new (alpha,
+                                                       CLUTTER_Y_AXIS,
+                                                       CLUTTER_ROTATE_CW,
+                                                       180.0, 0.0);
+      clutter_behaviour_rotate_set_center (CLUTTER_BEHAVIOUR_ROTATE
+                                           (anim_data.rotate),
+                                           center_x, center_y, 0);
+
+      clutter_behaviour_apply (anim_data.rotate, anim_data.card_tex);
+    }
+
+    if (anims[i].raise) {
+      alpha = clutter_alpha_new_with_func (priv->timeline,
+                                           aisleriot_slot_sine_animation_mode,
+                                           NULL, NULL);
+
+      anim_data.depth = clutter_behaviour_depth_new (alpha,
+                                                     0, card_height);
+      clutter_behaviour_apply (anim_data.depth, anim_data.card_tex);
+    }
+
+    g_array_append_val (priv->animations, anim_data);
+
+    card_num++;
+  }
+
+  if (n_anims > 0) {
+    clutter_timeline_rewind (priv->timeline);
+    clutter_timeline_start (priv->timeline);
+  }
+
+  priv->n_unexposed_animated_cards = n_unexposed_animated_cards;
+
+  clutter_actor_queue_redraw (CLUTTER_ACTOR (srend));
+}
+
+static void
+completed_cb (AisleriotSlotRenderer *srend)
+{
+  /* Get rid of all animation actors */
+  aisleriot_slot_renderer_set_animations (srend, 0, NULL, 0);
+
+  /* Redraw so that the animated actors will be drawn as part of the
+     renderer instead */
+  clutter_actor_queue_redraw (CLUTTER_ACTOR (srend));
+}
diff --git a/aisleriot/slot-renderer.h b/aisleriot/slot-renderer.h
new file mode 100644
index 0000000..930abd5
--- /dev/null
+++ b/aisleriot/slot-renderer.h
@@ -0,0 +1,101 @@
+/*
+ * Copyright © 2008 Neil Roberts
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __AISLERIOT_SLOT_RENDERER_H__
+#define __AISLERIOT_SLOT_RENDERER_H__
+
+#include <clutter/clutter.h>
+
+#include "game.h"
+#include "ar-card-textures-cache.h"
+#include "ar-style.h"
+
+G_BEGIN_DECLS
+
+#define AISLERIOT_TYPE_SLOT_RENDERER                                    \
+  (aisleriot_slot_renderer_get_type())
+#define AISLERIOT_SLOT_RENDERER(obj)                                    \
+  (G_TYPE_CHECK_INSTANCE_CAST ((obj),                                   \
+                               AISLERIOT_TYPE_SLOT_RENDERER,            \
+                               AisleriotSlotRenderer))
+#define AISLERIOT_SLOT_RENDERER_CLASS(klass)                            \
+  (G_TYPE_CHECK_CLASS_CAST ((klass),                                    \
+                            AISLERIOT_TYPE_SLOT_RENDERER,               \
+                            AisleriotSlotRendererClass))
+#define AISLERIOT_IS_SLOT_RENDERER(obj)                                 \
+  (G_TYPE_CHECK_INSTANCE_TYPE ((obj),                                   \
+                               AISLERIOT_TYPE_SLOT_RENDERER))
+#define AISLERIOT_IS_SLOT_RENDERER_CLASS(klass)                         \
+  (G_TYPE_CHECK_CLASS_TYPE ((klass),                                    \
+                            AISLERIOT_TYPE_SLOT_RENDERER))
+#define AISLERIOT_SLOT_RENDERER_GET_CLASS(obj)                          \
+  (G_TYPE_INSTANCE_GET_CLASS ((obj),                                    \
+                              AISLERIOT_TYPE_SLOT_RENDERER,             \
+                              AisleriotSlotRendererClass))
+
+typedef struct _AisleriotSlotRenderer        AisleriotSlotRenderer;
+typedef struct _AisleriotSlotRendererClass   AisleriotSlotRendererClass;
+typedef struct _AisleriotSlotRendererPrivate AisleriotSlotRendererPrivate;
+typedef struct _AisleriotAnimStart           AisleriotAnimStart;
+
+struct _AisleriotSlotRendererClass
+{
+  ClutterActorClass parent_class;
+};
+
+struct _AisleriotSlotRenderer
+{
+  ClutterActor parent;
+
+  AisleriotSlotRendererPrivate *priv;
+};
+
+struct _AisleriotAnimStart
+{
+  gint cardx, cardy;
+  Card old_card;
+  gboolean raise;
+};
+
+GType aisleriot_slot_renderer_get_type (void) G_GNUC_CONST;
+
+ClutterActor *aisleriot_slot_renderer_new (ArStyle *style,
+                                           ArCardTexturesCache *cache,
+                                           ArSlot *slot);
+
+void aisleriot_slot_renderer_set_highlight (AisleriotSlotRenderer *srend,
+                                            gint hightlight_start);
+guint aisleriot_slot_renderer_get_highlight (AisleriotSlotRenderer *srend);
+
+void aisleriot_slot_renderer_set_revealed_card (AisleriotSlotRenderer *srend,
+                                                gint revealed_card);
+gint aisleriot_slot_renderer_get_revealed_card (AisleriotSlotRenderer *srend);
+
+ClutterContainer *aisleriot_slot_renderer_get_animation_layer
+                                  (AisleriotSlotRenderer *srend);
+void aisleriot_slot_renderer_set_animation_layer
+                                  (AisleriotSlotRenderer *srend,
+                                   ClutterContainer *animation_layer);
+
+void aisleriot_slot_renderer_set_animations (AisleriotSlotRenderer *srend,
+                                             guint n_anims,
+                                             const AisleriotAnimStart *anims,
+                                             guint n_unexposed_animated_cards);
+
+G_END_DECLS
+
+#endif /* __AISLERIOT_SLOT_RENDERER_H__ */
diff --git a/aisleriot/sol.c b/aisleriot/sol.c
index 28da985..fe3bf46 100644
--- a/aisleriot/sol.c
+++ b/aisleriot/sol.c
@@ -28,6 +28,16 @@
 
 #include <gtk/gtk.h>
 
+#ifdef HAVE_CLUTTER
+#include <cogl/cogl.h>
+#include <clutter/clutter.h>
+#include <clutter-gtk/clutter-gtk.h>
+
+#ifndef CLUTTER_GTK_CHECK_VERSION
+#define CLUTTER_GTK_CHECK_VERSION(a,b,c) (0)
+#endif
+#endif
+
 #ifdef HAVE_HILDON
 #include <libosso.h>
 
@@ -262,6 +272,14 @@ main_prog (void *closure, int argc, char *argv[])
   g_option_context_add_group (option_context, egg_sm_client_get_option_group ());
 #endif /* WITH_SMCLIENT */
 
+#ifdef HAVE_CLUTTER
+  g_option_context_add_group (option_context, cogl_get_option_group ());
+  g_option_context_add_group (option_context, clutter_get_option_group_without_init ());
+#if CLUTTER_GTK_CHECK_VERSION (0, 90, 0)
+  g_option_context_add_group (option_context, gtk_clutter_get_option_group ());
+#endif
+#endif /* HAVE_CLUTTER */
+
 #if defined(HAVE_HILDON) && defined(HAVE_MAEMO_5)
   {
     char *rc_file;
@@ -285,6 +303,16 @@ main_prog (void *closure, int argc, char *argv[])
     goto cleanup;
   }
 
+#ifdef HAVE_CLUTTER
+#if !CLUTTER_GTK_CHECK_VERSION (0, 90, 0)
+  if (gtk_clutter_init_with_args (NULL, NULL, NULL, NULL, NULL, &error) != CLUTTER_INIT_SUCCESS) {
+    g_printerr ("Failed to initialise clutter: %s\n", error->message);
+    g_error_free (error);
+    goto cleanup;
+  }
+#endif
+#endif /* HAVE_CLUTTER */
+
 #ifdef HAVE_MAEMO
   data.program = HILDON_PROGRAM (hildon_program_get_instance ());
 
diff --git a/aisleriot/window.c b/aisleriot/window.c
index 1af44ec..03eeed2 100644
--- a/aisleriot/window.c
+++ b/aisleriot/window.c
@@ -54,7 +54,14 @@
 #include <libgames-support/games-settings.h>
 #endif
 
+#ifdef HAVE_CLUTTER
+#include "ar-clutter-embed.h"
+#include "ar-style.h"
+#include "baize.h"
+#include "board.h"
+#else
 #include "board-noclutter.h"
+#endif
 
 #include "ar-card-theme.h"
 #include "ar-card-themes.h"
@@ -124,7 +131,13 @@ struct _AisleriotWindowPrivate
 {
   AisleriotGame *game;
   ArStyle *board_style;
+#ifdef HAVE_CLUTTER
+  ArClutterEmbed *board;
+  ClutterActor *baize_actor;
+  ClutterActor *board_actor;
+#else
   AisleriotBoard *board;
+#endif
 
   ArCardThemes *theme_manager;
   ArCardTheme *theme;
@@ -366,7 +379,11 @@ undo_cb (GtkAction *action,
   AisleriotWindowPrivate *priv = window->priv;
 
   /* If a move is in progress, cancel it before changing the game! */
+#ifdef HAVE_CLUTTER
+  aisleriot_board_abort_move (AISLERIOT_BOARD (priv->board_actor));
+#else
   aisleriot_board_abort_move (priv->board);
+#endif
 
   aisleriot_game_undo_move (priv->game);
 }
@@ -377,7 +394,11 @@ redo_cb (GtkAction *action,
 {
   AisleriotWindowPrivate *priv = window->priv;
 
+#ifdef HAVE_CLUTTER
+  aisleriot_board_abort_move (AISLERIOT_BOARD (priv->board_actor));
+#else
   aisleriot_board_abort_move (priv->board);
+#endif
 
   aisleriot_game_redo_move (priv->game);
 }
@@ -1011,6 +1032,24 @@ sound_toggle_cb (GtkToggleAction *action,
 
 #endif /* ENABLE_SOUND */
 
+#ifdef HAVE_CLUTTER
+
+static void
+animations_toggle_cb (GtkToggleAction *action,
+                      AisleriotWindow *window)
+{
+  AisleriotWindowPrivate *priv = window->priv;
+  gboolean enabled;
+
+  enabled = gtk_toggle_action_get_active (action);
+
+  ar_style_set_enable_animations (priv->board_style, enabled);
+  
+  games_conf_set_boolean (NULL, aisleriot_conf_get_key (CONF_ANIMATIONS), enabled);
+}
+
+#endif /* HAVE_CLUTTER */
+
 static void
 show_hint_cb (GtkAction *action,
               AisleriotWindow *window)
@@ -1901,7 +1940,7 @@ game_exception_cb (AisleriotGame *game,
   gtk_widget_show (dialog);
 }
 
-#if defined(ENABLE_SOUND) && GTK_CHECK_VERSION (2, 14, 0)
+#if defined(HAVE_CLUTTER) || (defined(ENABLE_SOUND) && GTK_CHECK_VERSION (2, 14, 0))
 
 static void
 settings_changed_cb (GtkSettings *settings,
@@ -1918,12 +1957,23 @@ settings_changed_cb (GtkSettings *settings,
   else
     name = NULL;
 
+#ifdef HAVE_CLUTTER
+  if (name == NULL || strcmp (name, "gtk-enable-animations") == 0) {
+    g_object_get (settings, "gtk-enable-animations", &enabled, NULL);
+
+    action = gtk_action_group_get_action (priv->action_group, "Animations");
+    gtk_action_set_visible (action, enabled);
+  }
+#endif /* HAVE_CLUTTER */
+
+#if defined(ENABLE_SOUND) && GTK_CHECK_VERSION (2, 14, 0)
   if (name == NULL || strcmp (name, "gtk-enable-event-sounds") == 0) {
     g_object_get (settings, "gtk-enable-event-sounds", &enabled, NULL);
 
     action = gtk_action_group_get_action (priv->action_group, "Sound");
     gtk_action_set_visible (action, enabled);
   }
+#endif /* ENABLE_SOUND && GTK >= 2.14 */
 }
 
 static void
@@ -1951,11 +2001,17 @@ screen_changed_cb (GtkWidget *widget,
 
   settings = gtk_widget_get_settings (widget);
   settings_changed_cb (settings, NULL, window);
+#ifdef HAVE_CLUTTER
+  g_signal_connect (settings, "notify::gtk-enable-animations",
+                    G_CALLBACK (settings_changed_cb), window);
+#endif
+#if defined (ENABLE_SOUND) && GTK_CHECK_VERSION (2, 14, 0)
   g_signal_connect (settings, "notify::gtk-enable-event-sounds",
                     G_CALLBACK (settings_changed_cb), window);
+#endif
 }
 
-#endif /* ENABLE_SOUND && GTK+ >= 2.14.0 */
+#endif /* HAVE_CLUTTER || ENABLE_SOUND && GTK+ >= 2.14.0 */
 
 /*
  * aisleriot_window_set_freecell_mode:
@@ -2005,6 +2061,37 @@ board_status_message_cb (AisleriotBoard *board,
 
 #endif /* !HAVE_HILDON */
 
+#ifdef HAVE_CLUTTER
+
+static void
+board_cursor_cb (AisleriotBoard *board,
+                 int cursor_type,
+                 ArClutterEmbed *embed)
+{
+  ar_clutter_embed_set_cursor (embed, (ArCursorType) cursor_type);
+}
+
+static void
+board_error_bell_cb (AisleriotBoard *board,
+                     ArClutterEmbed *embed)
+{
+#if GTK_CHECK_VERSION (2, 12, 0) || (defined (HAVE_HILDON) && !defined(HAVE_MAEMO_3))
+  gtk_widget_error_bell (GTK_WIDGET (embed));
+#endif
+}
+
+static void
+embed_size_allocate_cb (ArClutterEmbed *embed,
+                        GtkAllocation *allocation,
+                        AisleriotWindow *window)
+{
+  AisleriotWindowPrivate *priv = window->priv;
+
+  clutter_actor_set_size (priv->board_actor, allocation->width, allocation->height);
+}
+
+#endif /* HAVE_CLUTTER */
+
 /* Class implementation */
 
 #ifdef HAVE_HILDON
@@ -2237,6 +2324,12 @@ aisleriot_window_init (AisleriotWindow *window)
       G_CALLBACK (sound_toggle_cb),
       FALSE /* not active by default */ },
 #endif /* ENABLE_SOUND */
+#ifdef HAVE_CLUTTER
+   { "Animations", NULL, N_("_Animations"), NULL,
+      ACTION_TOOLTIP (N_("Whether or not to animate card moves")),
+      G_CALLBACK (animations_toggle_cb),
+      FALSE /* not active by default */ },
+#endif /* HAVE_CLUTTER */
   };
 
   static const char names[][16] = {
@@ -2334,6 +2427,9 @@ aisleriot_window_init (AisleriotWindow *window)
 #ifdef ENABLE_SOUND
           "<menuitem action='Sound'/>"
 #endif
+#ifdef HAVE_CLUTTER
+          "<menuitem action='Animations'/>"
+#endif
         "</menu>"
         "<menu action='OptionsMenu'/>"
         "<menu action='HelpMenu'>"
@@ -2398,6 +2494,9 @@ aisleriot_window_init (AisleriotWindow *window)
   GtkStatusbar *statusbar;
   GtkWidget *statusbar_hbox, *label, *time_box;
 #endif
+#ifdef HAVE_CLUTTER
+  ClutterContainer *stage;
+#endif
 
   g_assert (G_N_ELEMENTS (names) == LAST_ACTION);
 
@@ -2411,7 +2510,34 @@ aisleriot_window_init (AisleriotWindow *window)
 
   priv->board_style = ar_style_new ();
 
+#ifdef HAVE_CLUTTER
+  priv->board = ar_clutter_embed_new (priv->board_style);
+
+  priv->baize_actor = aisleriot_baize_new ();
+
+  stage = CLUTTER_CONTAINER (gtk_clutter_embed_get_stage (GTK_CLUTTER_EMBED (priv->board)));
+  clutter_container_add (stage, priv->baize_actor, NULL);
+  /* FIXMEchpe: how to ensure this is ALWAYS the lowest actor? */
+  clutter_actor_lower_bottom (priv->baize_actor);
+
+  priv->board_actor = aisleriot_board_new (priv->board_style, priv->game);
+  clutter_container_add (stage, priv->board_actor, NULL);
+
+  /* FIXMEchpe */
+  clutter_stage_set_key_focus (CLUTTER_STAGE (stage), priv->board_actor);
+
+  g_signal_connect_after (priv->board, "size-allocate",
+                          G_CALLBACK (embed_size_allocate_cb), window);
+
+  g_signal_connect (priv->board_actor, "request-cursor",
+                    G_CALLBACK (board_cursor_cb), priv->board);
+  g_signal_connect (priv->board_actor, "error-bell",
+                    G_CALLBACK (board_error_bell_cb), priv->board);
+
+  /* FIXMEchpe: unref baize & board_actor here? */
+#else
   priv->board = AISLERIOT_BOARD (aisleriot_board_new (priv->board_style, priv->game));
+#endif /* HAVE_CLUTTER */
 
   theme_name = games_conf_get_string (NULL, aisleriot_conf_get_key (CONF_THEME), NULL);
   theme = ar_card_themes_get_theme_by_name (priv->theme_manager, theme_name);
@@ -2463,8 +2589,13 @@ aisleriot_window_init (AisleriotWindow *window)
 
   priv->game_message_id = gtk_statusbar_get_context_id (priv->statusbar, "board-message");
 
+#ifdef HAVE_CLUTTER
+  g_signal_connect (priv->board_actor, "status-message",
+                    G_CALLBACK (board_status_message_cb), window);
+#else
   g_signal_connect (priv->board, "status-message",
                     G_CALLBACK (board_status_message_cb), window);
+#endif
 
 #if GTK_CHECK_VERSION (2, 91, 0)
   gtk_window_set_has_resize_grip (GTK_WINDOW (window), TRUE);
@@ -2611,12 +2742,19 @@ aisleriot_window_init (AisleriotWindow *window)
 
   set_fullscreen_actions (window, FALSE);
 
-#if defined(ENABLE_SOUND) && GTK_CHECK_VERSION (2, 14, 0)
+#ifdef HAVE_CLUTTER
+  action = gtk_action_group_get_action (priv->action_group, "Animations");
+  gtk_toggle_action_set_active (GTK_TOGGLE_ACTION (action),
+                                games_conf_get_boolean (NULL, aisleriot_conf_get_key (CONF_ANIMATIONS), NULL));
+
+#endif /* HAVE_CLUTTER */
+
+#if defined(HAVE_CLUTTER) || (defined(ENABLE_SOUND) && GTK_CHECK_VERSION (2, 14, 0))
   /* Set the action visibility and listen for animation and sound mode changes */
   screen_changed_cb (GTK_WIDGET (window), NULL, window);
   g_signal_connect (window, "screen-changed",
                     G_CALLBACK (screen_changed_cb), window);
-#endif /* ENABLE_SOUND && GTK+ >= 2.14.0 */
+#endif /* HAVE_CLUTTER || ENABLE_SOUND && GTK+ >= 2.14.0 */
 
   /* Now set up the widgets */
   main_vbox = gtk_vbox_new (FALSE, 0);
@@ -2702,6 +2840,12 @@ aisleriot_window_dispose (GObject *object)
   AisleriotWindow *window = AISLERIOT_WINDOW (object);
   AisleriotWindowPrivate *priv = window->priv;
   
+#ifdef HAVE_CLUTTER
+  g_signal_handlers_disconnect_by_func (gtk_widget_get_settings (GTK_WIDGET (window)),
+                                        G_CALLBACK (settings_changed_cb),
+                                        window);
+#endif /* HAVE_CLUTTER */
+
 #ifndef HAVE_HILDON
   if (priv->hint_dialog) {
     gtk_widget_destroy (priv->hint_dialog);
@@ -2742,6 +2886,10 @@ aisleriot_window_finalize (GObject *object)
   AisleriotWindow *window = AISLERIOT_WINDOW (object);
   AisleriotWindowPrivate *priv = window->priv;
 
+#ifdef HAVE_CLUTTER
+  g_object_unref (priv->board_style);
+#endif /* HAVE_CLUTTER */
+
   if (priv->theme) {
     g_object_unref (priv->theme);
   }
diff --git a/configure.in b/configure.in
index a2fc4cc..904df12 100644
--- a/configure.in
+++ b/configure.in
@@ -430,10 +430,27 @@ fi
 # Clutter
 # *******
 
+# We're using need_guile as a quick way to check whether building aisleriot
+if test "$need_guile" = "yes"; then
+  AC_MSG_CHECKING([whether to enable aisleriot/clutter])
+  AC_ARG_ENABLE([aisleriot-clutter],
+    [AS_HELP_STRING([--enable-aisleriot-clutter],[Whether to enable the clutter version of aisleriot (default: disabled)])],
+    [],[enable_aisleriot_clutter=no])
+  AC_MSG_RESULT([$enable_aisleriot_clutter])
+
+  if test "$enable_aisleriot_clutter" = "yes"; then
+    # Distro packagers: DO NOT ENABLE AISLERIOT/CLUTTER IN YOUR DISTRO PACKAGES, OR ELSE!
+    AC_MSG_NOTICE([Aisleriot/Clutter is experimental; do not enable this for distribution packages!])
+    need_clutter=yes
+  fi
+fi
+
+AM_CONDITIONAL([ENABLE_AISLERIOT_CLUTTER],[test "$enable_aisleriot_clutter" = "yes"])
+
 case "$with_platform" in
   gnome|gtk-only) ;;
   hildon) if test "$need_clutter" = "yes"; then
-            AC_MSG_ERROR([Clutter is not supported on hildon; disable Gnometris, Lights Off])
+            AC_MSG_ERROR([Clutter is not supported on hildon; disable Gnometris, Lights Off and Aisleriot/Clutter])
           fi
           ;;
 esac
diff --git a/po/POTFILES.in b/po/POTFILES.in
index f1e99c9..35f8dc5 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -2,7 +2,9 @@
 # Please keep this file in alphabetical order.
 [encoding: UTF-8]
 aisleriot/aisleriot.schemas.in
+aisleriot/ar-clutter-embed.c
 aisleriot/ar-game-chooser.c
+aisleriot/board.c
 aisleriot/board-noclutter.c
 aisleriot/conf.c
 aisleriot/freecell.desktop.in.in



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