[epiphany/wip/chergert/completion] wip on new completion bar



commit 5245681c2a1aaf54415207d13f97d73782a036aa
Author: Christian Hergert <chergert redhat com>
Date:   Fri Apr 7 01:44:30 2017 -0700

    wip on new completion bar

 build-aux/Makefile.am.gresources      |   62 ++
 configure.ac                          |    3 +-
 lib/egg/Makefile.am                   |   37 +-
 lib/egg/egg-animation.c               | 1247 +++++++++++++++++++++++++++++++++
 lib/egg/egg-animation.h               |   81 +++
 lib/egg/egg-elastic-bin.c             |  278 ++++++++
 lib/egg/egg-elastic-bin.h             |   44 ++
 lib/egg/egg-frame-source.c            |  130 ++++
 lib/egg/egg-frame-source.h            |   32 +
 lib/egg/egg-suggestion-entry-buffer.c |  408 +++++++++++
 lib/egg/egg-suggestion-entry-buffer.h |   52 ++
 lib/egg/egg-suggestion-entry.c        |  663 ++++++++++++++++++
 lib/egg/egg-suggestion-entry.css      |   53 ++
 lib/egg/egg-suggestion-entry.h        |   64 ++
 lib/egg/egg-suggestion-popover.c      |  796 +++++++++++++++++++++
 lib/egg/egg-suggestion-popover.h      |   49 ++
 lib/egg/egg-suggestion-popover.ui     |   42 ++
 lib/egg/egg-suggestion-row.c          |  209 ++++++
 lib/egg/egg-suggestion-row.h          |   49 ++
 lib/egg/egg-suggestion-row.ui         |   53 ++
 lib/egg/egg-suggestion.c              |  351 +++++++++
 lib/egg/egg-suggestion.h              |   66 ++
 lib/egg/egg.gresources.xml            |    8 +
 lib/egg/eggtreemultidnd.c             |    2 +
 lib/widgets/ephy-location-entry.c     |    4 +-
 lib/widgets/ephy-location-entry.h     |    4 +-
 src/Makefile.am                       |    2 +
 src/ephy-location-controller.c        |  133 +----
 src/ephy-suggestion-model.c           |  417 +++++++++++
 src/ephy-suggestion-model.h           |   46 ++
 30 files changed, 5253 insertions(+), 132 deletions(-)
---
diff --git a/build-aux/Makefile.am.gresources b/build-aux/Makefile.am.gresources
new file mode 100644
index 0000000..d1b849b
--- /dev/null
+++ b/build-aux/Makefile.am.gresources
@@ -0,0 +1,62 @@
+# Rules for generating gresources using glib-compile-resources
+#
+# Define:
+#      glib_resources_h = header template file
+#      glib_resources_c = source template file
+#      glib_resources_xml = path to *.gresource.xml
+#      glib_resources_namespace = c prefix for resources
+#
+# before including Makefile.am.resources. You will also need to have
+# the following targets already defined:
+#
+#      CLEANFILES
+#      DISTCLEANFILES
+#      BUILT_SOURCES
+#      EXTRA_DIST
+#
+# Author: Christian Hergert <christian hergert me>
+
+# Basic sanity checks
+$(if $(GLIB_COMPILE_RESOURCES),,$(error Need to define GLIB_COMPILE_RESOURCES))
+
+$(if $(or $(glib_resources_h), \
+          $(glib_resources_c)),, \
+    $(error Need to define glib_resources_h and glib_resources_c))
+
+$(if $(glib_resources_xml),,$(error Need to define glib_resources_xml))
+$(if $(glib_resources_namespace),,$(error Need to define glib_resources_namespace))
+
+resources_xml=$(addprefix $(srcdir)/,$(glib_resources_xml))
+resources_srcdir=$(dir $(resources_xml))
+
+DISTCLEANFILES += $(glib_resources_h) $(glib_resources_c)
+BUILT_SOURCES += $(glib_resources_h) $(glib_resources_c)
+CLEANFILES += stamp-resources $(glib_resources_c) $(glib_resources_h)
+EXTRA_DIST += \
+       $(glib_resources_xml) \
+       $(shell $(GLIB_COMPILE_RESOURCES) --sourcedir=$(resources_srcdir) --generate-dependencies 
$(resources_xml)) \
+       $(NULL)
+
+stamp-resources: $(glib_resources_c) $(resources_xml)
+       $(AM_V_GEN)$(GLIB_COMPILE_RESOURCES) \
+               --target=xgen-gr.h \
+               --sourcedir=$(resources_srcdir) \
+               --generate-header \
+               --c-name $(glib_resources_namespace) \
+               $(resources_xml) \
+       && (cmp -s xgen-gr.h $(glib_resources_h) || cp -f xgen-gr.h $(glib_resources_h)) \
+       && rm -f xgen-gr.h \
+       && echo timestamp > $(@F)
+
+$(glib_resources_h): stamp-resources
+       @true
+
+$(glib_resources_c): $(resources_xml) $(shell $(GLIB_COMPILE_RESOURCES) --sourcedir=$(resources_srcdir) 
--generate-dependencies $(resources_xml))
+       $(AM_V_GEN)$(GLIB_COMPILE_RESOURCES) \
+               --target=xgen-gr.c \
+               --sourcedir=$(resources_srcdir) \
+               --generate-source \
+               --c-name $(glib_resources_namespace) \
+               $(resources_xml) \
+       && (cmp -s xgen-gr.c $(glib_resources_c) || cp -f xgen-gr.c $(glib_resources_c)) \
+       && rm -f xgen-gr.c
diff --git a/configure.ac b/configure.ac
index f7976ce..88fe2ff 100644
--- a/configure.ac
+++ b/configure.ac
@@ -50,6 +50,7 @@ AC_SUBST([EPIPHANY_CHANGELOG_START],[epiphany_changelog_start])
 AC_PROG_CC
 
 AC_PATH_PROG([GLIB_MKENUMS],[glib-mkenums])
+AC_PATH_PROG([GLIB_COMPILE_RESOURCES], [glib-compile-resources])
 
 dnl Note: I want to enable subdir-objects here, to silence Automake's many
 dnl warnings about not using it. But we can't do this until we require Automake
@@ -62,7 +63,7 @@ dnl it is a bug after all, and updating for bugfixes is just how software works.
 dnl Note also: the issue is that a directory _literally_ named $(top_srcdir)
 dnl will be created under embed/web-extension, and the build will fail due to
 dnl misplaced .Plo files (used for dependency tracking).
-AM_INIT_AUTOMAKE([1.11 foreign dist-xz no-dist-gzip tar-ustar])
+AM_INIT_AUTOMAKE([1.11 foreign dist-xz no-dist-gzip tar-ustar subdir-objects])
 AM_SILENT_RULES([yes])
 AM_MAINTAINER_MODE([enable])
 
diff --git a/lib/egg/Makefile.am b/lib/egg/Makefile.am
index f56b24e..bf9c143 100644
--- a/lib/egg/Makefile.am
+++ b/lib/egg/Makefile.am
@@ -1,13 +1,42 @@
 noinst_LTLIBRARIES = libegg.la
 
 libegg_la_SOURCES = \
-       eggtreemultidnd.c       \
+       egg-animation.c                 \
+       egg-animation.h                 \
+       egg-elastic-bin.c               \
+       egg-elastic-bin.h               \
+       egg-frame-source.c              \
+       egg-frame-source.h              \
+       egg-suggestion.c                \
+       egg-suggestion-entry-buffer.c   \
+       egg-suggestion-entry-buffer.h   \
+       egg-suggestion-entry.c          \
+       egg-suggestion-entry.h          \
+       egg-suggestion.h                \
+       egg-suggestion-popover.c        \
+       egg-suggestion-popover.h        \
+       egg-suggestion-row.c            \
+       egg-suggestion-row.h            \
+       eggtreemultidnd.c               \
        eggtreemultidnd.h
 
-libegg_la_CPPFLAGS = \
-       -DG_LOG_DOMAIN=\""Egg"\"        \
-       $(GTK_CFLAGS)
+nodist_libegg_la_SOURCES = \
+       egg-resources.c \
+       egg-resources.h
+
+libegg_la_CPPFLAGS = $(GTK_CFLAGS)
 
 libegg_la_LIBADD = $(GTK_LIBS)
 
+BUILT_SOURCES =
+CLEANFILES =
+DISTCLEANFILES =
+EXTRA_DIST =
+
+glib_resources_xml = egg.gresources.xml
+glib_resources_c = egg-resources.c
+glib_resources_h = egg-resources.h
+glib_resources_namespace = egg
+include $(top_srcdir)/build-aux/Makefile.am.gresources
+
 -include $(top_srcdir)/git.mk
diff --git a/lib/egg/egg-animation.c b/lib/egg/egg-animation.c
new file mode 100644
index 0000000..ad01b16
--- /dev/null
+++ b/lib/egg/egg-animation.c
@@ -0,0 +1,1247 @@
+/* egg-animation.c
+ *
+ * Copyright (C) 2010-2016 Christian Hergert <christian hergert me>
+ *
+ * This file is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU Lesser General Public License as published by the Free
+ * Software Foundation; either version 2.1 of the License, or (at your option)
+ * any later version.
+ *
+ * This file 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 Lesser 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 <glib/gi18n.h>
+#include <gobject/gvaluecollector.h>
+#include <gdk/gdk.h>
+#include <gtk/gtk.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "egg-animation.h"
+#include "egg-frame-source.h"
+
+#define FALLBACK_FRAME_RATE 60
+
+typedef gdouble (*AlphaFunc) (gdouble       offset);
+typedef void    (*TweenFunc) (const GValue *begin,
+                              const GValue *end,
+                              GValue       *value,
+                              gdouble       offset);
+
+typedef struct
+{
+  gboolean    is_child;  /* Does GParamSpec belong to parent widget */
+  GParamSpec *pspec;     /* GParamSpec of target property */
+  GValue      begin;     /* Begin value in animation */
+  GValue      end;       /* End value in animation */
+} Tween;
+
+
+struct _EggAnimation
+{
+  GInitiallyUnowned  parent_instance;
+
+  gpointer           target;              /* Target object to animate */
+  guint64            begin_msec;          /* Time in which animation started */
+  guint              duration_msec;       /* Duration of animation */
+  guint              mode;                /* Tween mode */
+  gulong             tween_handler;       /* GSource or signal handler */
+  gulong             after_paint_handler; /* signal handler */
+  gdouble            last_offset;         /* Track our last offset */
+  GArray            *tweens;              /* Array of tweens to perform */
+  GdkFrameClock     *frame_clock;         /* An optional frame-clock for sync. */
+  GDestroyNotify     notify;              /* Notify callback */
+  gpointer           notify_data;         /* Data for notify */
+};
+
+G_DEFINE_TYPE (EggAnimation, egg_animation, G_TYPE_INITIALLY_UNOWNED)
+
+enum {
+  PROP_0,
+  PROP_DURATION,
+  PROP_FRAME_CLOCK,
+  PROP_MODE,
+  PROP_TARGET,
+  LAST_PROP
+};
+
+
+enum {
+  TICK,
+  LAST_SIGNAL
+};
+
+
+/*
+ * Helper macros.
+ */
+#define LAST_FUNDAMENTAL 64
+#define TWEEN(type)                                       \
+  static void                                             \
+  tween_ ## type (const GValue * begin,                   \
+                  const GValue * end,                     \
+                  GValue * value,                         \
+                  gdouble offset)                         \
+  {                                                       \
+    g ## type x = g_value_get_ ## type (begin);           \
+    g ## type y = g_value_get_ ## type (end);             \
+    g_value_set_ ## type (value, x + ((y - x) * offset)); \
+  }
+
+
+/*
+ * Globals.
+ */
+static AlphaFunc   alpha_funcs[EGG_ANIMATION_LAST];
+static gboolean    debug;
+static GParamSpec *properties[LAST_PROP];
+static guint       signals[LAST_SIGNAL];
+static TweenFunc   tween_funcs[LAST_FUNDAMENTAL];
+static guint       slow_down_factor = 1;
+
+
+/*
+ * Tweeners for basic types.
+ */
+TWEEN (int);
+TWEEN (uint);
+TWEEN (long);
+TWEEN (ulong);
+TWEEN (float);
+TWEEN (double);
+
+
+/**
+ * egg_animation_alpha_ease_in_cubic:
+ * @offset: (in): The position within the animation; 0.0 to 1.0.
+ *
+ * An alpha function to transform the offset within the animation.
+ * @EGG_ANIMATION_CUBIC means the valu ewill be transformed into
+ * cubic acceleration (x * x * x).
+ */
+static gdouble
+egg_animation_alpha_ease_in_cubic (gdouble offset)
+{
+  return offset * offset * offset;
+}
+
+
+static gdouble
+egg_animation_alpha_ease_out_cubic (gdouble offset)
+{
+  gdouble p = offset - 1.0;
+
+  return p * p * p + 1.0;
+}
+
+static gdouble
+egg_animation_alpha_ease_in_out_cubic (gdouble offset)
+{
+  if (offset < .5)
+    return egg_animation_alpha_ease_in_cubic (offset * 2.0) / 2.0;
+  else
+    return .5 + egg_animation_alpha_ease_out_cubic ((offset - .5) * 2.0) / 2.0;
+}
+
+
+/**
+ * egg_animation_alpha_linear:
+ * @offset: (in): The position within the animation; 0.0 to 1.0.
+ *
+ * An alpha function to transform the offset within the animation.
+ * @EGG_ANIMATION_LINEAR means no tranformation will be made.
+ *
+ * Returns: @offset.
+ * Side effects: None.
+ */
+static gdouble
+egg_animation_alpha_linear (gdouble offset)
+{
+  return offset;
+}
+
+
+/**
+ * egg_animation_alpha_ease_in_quad:
+ * @offset: (in): The position within the animation; 0.0 to 1.0.
+ *
+ * An alpha function to transform the offset within the animation.
+ * @EGG_ANIMATION_EASE_IN_QUAD means that the value will be transformed
+ * into a quadratic acceleration.
+ *
+ * Returns: A tranformation of @offset.
+ * Side effects: None.
+ */
+static gdouble
+egg_animation_alpha_ease_in_quad (gdouble offset)
+{
+  return offset * offset;
+}
+
+
+/**
+ * egg_animation_alpha_ease_out_quad:
+ * @offset: (in): The position within the animation; 0.0 to 1.0.
+ *
+ * An alpha function to transform the offset within the animation.
+ * @EGG_ANIMATION_EASE_OUT_QUAD means that the value will be transformed
+ * into a quadratic deceleration.
+ *
+ * Returns: A tranformation of @offset.
+ * Side effects: None.
+ */
+static gdouble
+egg_animation_alpha_ease_out_quad (gdouble offset)
+{
+  return -1.0 * offset * (offset - 2.0);
+}
+
+
+/**
+ * egg_animation_alpha_ease_in_out_quad:
+ * @offset: (in): The position within the animation; 0.0 to 1.0.
+ *
+ * An alpha function to transform the offset within the animation.
+ * @EGG_ANIMATION_EASE_IN_OUT_QUAD means that the value will be transformed
+ * into a quadratic acceleration for the first half, and quadratic
+ * deceleration the second half.
+ *
+ * Returns: A tranformation of @offset.
+ * Side effects: None.
+ */
+static gdouble
+egg_animation_alpha_ease_in_out_quad (gdouble offset)
+{
+  offset *= 2.0;
+  if (offset < 1.0)
+    return 0.5 * offset * offset;
+  offset -= 1.0;
+  return -0.5 * (offset * (offset - 2.0) - 1.0);
+}
+
+
+/**
+ * egg_animation_load_begin_values:
+ * @animation: (in): A #EggAnimation.
+ *
+ * Load the begin values for all the properties we are about to
+ * animate.
+ *
+ * Side effects: None.
+ */
+static void
+egg_animation_load_begin_values (EggAnimation *animation)
+{
+  GtkContainer *container;
+  Tween *tween;
+  guint i;
+
+  g_return_if_fail (EGG_IS_ANIMATION (animation));
+
+  for (i = 0; i < animation->tweens->len; i++)
+    {
+      tween = &g_array_index (animation->tweens, Tween, i);
+      g_value_reset (&tween->begin);
+      if (tween->is_child)
+        {
+          container = GTK_CONTAINER (gtk_widget_get_parent (animation->target));
+          gtk_container_child_get_property (container,
+                                            animation->target,
+                                            tween->pspec->name,
+                                            &tween->begin);
+        }
+      else
+        {
+          g_object_get_property (animation->target,
+                                 tween->pspec->name,
+                                 &tween->begin);
+        }
+    }
+}
+
+
+/**
+ * egg_animation_unload_begin_values:
+ * @animation: (in): A #EggAnimation.
+ *
+ * Unloads the begin values for the animation. This might be particularly
+ * useful once we support pointer types.
+ *
+ * Side effects: None.
+ */
+static void
+egg_animation_unload_begin_values (EggAnimation *animation)
+{
+  Tween *tween;
+  guint i;
+
+  g_return_if_fail (EGG_IS_ANIMATION (animation));
+
+  for (i = 0; i < animation->tweens->len; i++)
+    {
+      tween = &g_array_index (animation->tweens, Tween, i);
+      g_value_reset (&tween->begin);
+    }
+}
+
+
+/**
+ * egg_animation_get_offset:
+ * @animation: A #EggAnimation.
+ * @frame_time: the time to present the frame, or 0 for current timing.
+ *
+ * Retrieves the position within the animation from 0.0 to 1.0. This
+ * value is calculated using the msec of the beginning of the animation
+ * and the current time.
+ *
+ * Returns: The offset of the animation from 0.0 to 1.0.
+ */
+static gdouble
+egg_animation_get_offset (EggAnimation *animation,
+                          gint64        frame_time)
+{
+  gdouble offset;
+  gint64 frame_msec;
+
+  g_return_val_if_fail (EGG_IS_ANIMATION (animation), 0.0);
+
+  if (frame_time == 0)
+    {
+      if (animation->frame_clock != NULL)
+        frame_time = gdk_frame_clock_get_frame_time (animation->frame_clock);
+      else
+        frame_time = g_get_monotonic_time ();
+    }
+
+  frame_msec = frame_time / 1000L;
+
+  offset = (gdouble) (frame_msec - animation->begin_msec) /
+           (gdouble) MAX (animation->duration_msec, 1);
+
+  return CLAMP (offset, 0.0, 1.0);
+}
+
+
+/**
+ * egg_animation_update_property:
+ * @animation: (in): A #EggAnimation.
+ * @target: (in): A #GObject.
+ * @tween: (in): a #Tween containing the property.
+ * @value: (in): The new value for the property.
+ *
+ * Updates the value of a property on an object using @value.
+ *
+ * Side effects: The property of @target is updated.
+ */
+static void
+egg_animation_update_property (EggAnimation  *animation,
+                              gpointer      target,
+                              Tween        *tween,
+                              const GValue *value)
+{
+  g_assert (EGG_IS_ANIMATION (animation));
+  g_assert (G_IS_OBJECT (target));
+  g_assert (tween);
+  g_assert (value);
+
+  g_object_set_property (target, tween->pspec->name, value);
+}
+
+
+/**
+ * egg_animation_update_child_property:
+ * @animation: (in): A #EggAnimation.
+ * @target: (in): A #GObject.
+ * @tween: (in): A #Tween containing the property.
+ * @value: (in): The new value for the property.
+ *
+ * Updates the value of the parent widget of the target to @value.
+ *
+ * Side effects: The property of @target<!-- -->'s parent widget is updated.
+ */
+static void
+egg_animation_update_child_property (EggAnimation *animation,
+                                     gpointer      target,
+                                     Tween        *tween,
+                                     const GValue *value)
+{
+  GtkWidget *parent;
+
+  g_assert (EGG_IS_ANIMATION (animation));
+  g_assert (G_IS_OBJECT (target));
+  g_assert (tween);
+  g_assert (value);
+
+  parent = gtk_widget_get_parent (GTK_WIDGET (target));
+  gtk_container_child_set_property (GTK_CONTAINER (parent),
+                                    target,
+                                    tween->pspec->name,
+                                    value);
+}
+
+
+/**
+ * egg_animation_get_value_at_offset:
+ * @animation: (in): A #EggAnimation.
+ * @offset: (in): The offset in the animation from 0.0 to 1.0.
+ * @tween: (in): A #Tween containing the property.
+ * @value: (out): A #GValue in which to store the property.
+ *
+ * Retrieves a value for a particular position within the animation.
+ *
+ * Side effects: None.
+ */
+static void
+egg_animation_get_value_at_offset (EggAnimation *animation,
+                                   gdouble       offset,
+                                   Tween        *tween,
+                                   GValue       *value)
+{
+  g_return_if_fail (EGG_IS_ANIMATION (animation));
+  g_return_if_fail (tween != NULL);
+  g_return_if_fail (value != NULL);
+  g_return_if_fail (value->g_type == tween->pspec->value_type);
+
+  if (value->g_type < LAST_FUNDAMENTAL)
+    {
+      /*
+       * If you hit the following assertion, you need to add a function
+       * to create the new value at the given offset.
+       */
+      g_assert (tween_funcs[value->g_type]);
+      tween_funcs[value->g_type](&tween->begin, &tween->end, value, offset);
+    }
+  else
+    {
+      /*
+       * TODO: Support complex transitions.
+       */
+      if (offset >= 1.0)
+        g_value_copy (&tween->end, value);
+    }
+}
+
+static void
+egg_animation_set_frame_clock (EggAnimation  *animation,
+                               GdkFrameClock *frame_clock)
+{
+  if (animation->frame_clock != frame_clock)
+    {
+      g_clear_object (&animation->frame_clock);
+      animation->frame_clock = frame_clock ? g_object_ref (frame_clock) : NULL;
+    }
+}
+
+static void
+egg_animation_set_target (EggAnimation *animation,
+                          gpointer      target)
+{
+  g_assert (!animation->target);
+
+  animation->target = g_object_ref (target);
+
+  if (GTK_IS_WIDGET (animation->target))
+    egg_animation_set_frame_clock (animation,
+                                  gtk_widget_get_frame_clock (animation->target));
+}
+
+
+/**
+ * egg_animation_tick:
+ * @animation: (in): A #EggAnimation.
+ *
+ * Moves the object properties to the next position in the animation.
+ *
+ * Returns: %TRUE if the animation has not completed; otherwise %FALSE.
+ * Side effects: None.
+ */
+static gboolean
+egg_animation_tick (EggAnimation *animation,
+                    gdouble       offset)
+{
+  gdouble alpha;
+  GValue value = { 0 };
+  Tween *tween;
+  guint i;
+
+  g_return_val_if_fail (EGG_IS_ANIMATION (animation), FALSE);
+
+  if (offset == animation->last_offset)
+    return offset < 1.0;
+
+  alpha = alpha_funcs[animation->mode](offset);
+
+  /*
+   * Update property values.
+   */
+  for (i = 0; i < animation->tweens->len; i++)
+    {
+      tween = &g_array_index (animation->tweens, Tween, i);
+      g_value_init (&value, tween->pspec->value_type);
+      egg_animation_get_value_at_offset (animation, alpha, tween, &value);
+      if (!tween->is_child)
+        {
+          egg_animation_update_property (animation,
+                                        animation->target,
+                                        tween,
+                                        &value);
+        }
+      else
+        {
+          egg_animation_update_child_property (animation,
+                                              animation->target,
+                                              tween,
+                                              &value);
+        }
+      g_value_unset (&value);
+    }
+
+  /*
+   * Notify anyone interested in the tick signal.
+   */
+  g_signal_emit (animation, signals[TICK], 0);
+
+  /*
+   * Flush any outstanding events to the graphics server (in the case of X).
+   */
+#if !GTK_CHECK_VERSION (3, 13, 0)
+  if (GTK_IS_WIDGET (animation->target))
+    {
+      GdkWindow *window;
+
+      if ((window = gtk_widget_get_window (GTK_WIDGET (animation->target))))
+        gdk_window_flush (window);
+    }
+#endif
+
+  animation->last_offset = offset;
+
+  return offset < 1.0;
+}
+
+
+/**
+ * egg_animation_timeout_cb:
+ * @user_data: (in): A #EggAnimation.
+ *
+ * Timeout from the main loop to move to the next step of the animation.
+ *
+ * Returns: %TRUE until the animation has completed; otherwise %FALSE.
+ * Side effects: None.
+ */
+static gboolean
+egg_animation_timeout_cb (gpointer user_data)
+{
+  EggAnimation *animation = user_data;
+  gboolean ret;
+  gdouble offset;
+
+  offset = egg_animation_get_offset (animation, 0);
+
+  if (!(ret = egg_animation_tick (animation, offset)))
+    egg_animation_stop (animation);
+
+  return ret;
+}
+
+
+static gboolean
+egg_animation_widget_tick_cb (GdkFrameClock *frame_clock,
+                              EggAnimation  *animation)
+{
+  gboolean ret = G_SOURCE_REMOVE;
+
+  g_assert (GDK_IS_FRAME_CLOCK (frame_clock));
+  g_assert (EGG_IS_ANIMATION (animation));
+
+  if (animation->tween_handler)
+    {
+      gdouble offset;
+
+      offset = egg_animation_get_offset (animation, 0);
+
+      if (!(ret = egg_animation_tick (animation, offset)))
+        egg_animation_stop (animation);
+    }
+
+  return ret;
+}
+
+
+static void
+egg_animation_widget_after_paint_cb (GdkFrameClock *frame_clock,
+                                     EggAnimation  *animation)
+{
+  gint64 base_time;
+  gint64 interval;
+  gint64 next_frame_time;
+  gdouble offset;
+
+  g_assert (GDK_IS_FRAME_CLOCK (frame_clock));
+  g_assert (EGG_IS_ANIMATION (animation));
+
+  base_time = gdk_frame_clock_get_frame_time (frame_clock);
+  gdk_frame_clock_get_refresh_info (frame_clock, base_time, &interval, &next_frame_time);
+
+  offset = egg_animation_get_offset (animation, next_frame_time);
+
+  egg_animation_tick (animation, offset);
+}
+
+
+/**
+ * egg_animation_start:
+ * @animation: (in): A #EggAnimation.
+ *
+ * Start the animation. When the animation stops, the internal reference will
+ * be dropped and the animation may be finalized.
+ *
+ * Side effects: None.
+ */
+void
+egg_animation_start (EggAnimation *animation)
+{
+  g_return_if_fail (EGG_IS_ANIMATION (animation));
+  g_return_if_fail (!animation->tween_handler);
+
+  g_object_ref_sink (animation);
+  egg_animation_load_begin_values (animation);
+
+  if (animation->frame_clock)
+    {
+      animation->begin_msec = gdk_frame_clock_get_frame_time (animation->frame_clock) / 1000UL;
+      animation->tween_handler =
+        g_signal_connect (animation->frame_clock,
+                          "update",
+                          G_CALLBACK (egg_animation_widget_tick_cb),
+                          animation);
+      animation->after_paint_handler =
+        g_signal_connect (animation->frame_clock,
+                          "after-paint",
+                          G_CALLBACK (egg_animation_widget_after_paint_cb),
+                          animation);
+      gdk_frame_clock_begin_updating (animation->frame_clock);
+    }
+  else
+    {
+      animation->begin_msec = g_get_monotonic_time () / 1000UL;
+      animation->tween_handler = egg_frame_source_add (FALLBACK_FRAME_RATE,
+                                                       egg_animation_timeout_cb,
+                                                       animation);
+    }
+}
+
+
+static void
+egg_animation_notify (EggAnimation *self)
+{
+  g_assert (EGG_IS_ANIMATION (self));
+
+  if (self->notify != NULL)
+    {
+      GDestroyNotify notify = self->notify;
+      gpointer data = self->notify_data;
+
+      self->notify = NULL;
+      self->notify_data = NULL;
+
+      notify (data);
+    }
+}
+
+
+/**
+ * egg_animation_stop:
+ * @animation: (in): A #EggAnimation.
+ *
+ * Stops a running animation. The internal reference to the animation is
+ * dropped and therefore may cause the object to finalize.
+ *
+ * Side effects: None.
+ */
+void
+egg_animation_stop (EggAnimation *animation)
+{
+  g_return_if_fail (EGG_IS_ANIMATION (animation));
+
+  if (animation->tween_handler)
+    {
+      if (animation->frame_clock)
+        {
+          gdk_frame_clock_end_updating (animation->frame_clock);
+          g_signal_handler_disconnect (animation->frame_clock, animation->tween_handler);
+          g_signal_handler_disconnect (animation->frame_clock, animation->after_paint_handler);
+          animation->tween_handler = 0;
+        }
+      else
+        {
+          g_source_remove (animation->tween_handler);
+          animation->tween_handler = 0;
+        }
+      egg_animation_unload_begin_values (animation);
+      egg_animation_notify (animation);
+      g_object_unref (animation);
+    }
+}
+
+
+/**
+ * egg_animation_add_property:
+ * @animation: (in): A #EggAnimation.
+ * @pspec: (in): A #ParamSpec of @target or a #GtkWidget<!-- -->'s parent.
+ * @value: (in): The new value for the property at the end of the animation.
+ *
+ * Adds a new property to the set of properties to be animated during the
+ * lifetime of the animation.
+ *
+ * Side effects: None.
+ */
+void
+egg_animation_add_property (EggAnimation *animation,
+                            GParamSpec   *pspec,
+                            const GValue *value)
+{
+  Tween tween = { 0 };
+  GType type;
+
+  g_return_if_fail (EGG_IS_ANIMATION (animation));
+  g_return_if_fail (pspec != NULL);
+  g_return_if_fail (value != NULL);
+  g_return_if_fail (value->g_type);
+  g_return_if_fail (animation->target);
+  g_return_if_fail (!animation->tween_handler);
+
+  type = G_TYPE_FROM_INSTANCE (animation->target);
+  tween.is_child = !g_type_is_a (type, pspec->owner_type);
+  if (tween.is_child)
+    {
+      if (!GTK_IS_WIDGET (animation->target))
+        {
+          g_critical (_("Cannot locate property %s in class %s"),
+                      pspec->name, g_type_name (type));
+          return;
+        }
+    }
+
+  tween.pspec = g_param_spec_ref (pspec);
+  g_value_init (&tween.begin, pspec->value_type);
+  g_value_init (&tween.end, pspec->value_type);
+  g_value_copy (value, &tween.end);
+  g_array_append_val (animation->tweens, tween);
+}
+
+
+/**
+ * egg_animation_dispose:
+ * @object: (in): A #EggAnimation.
+ *
+ * Releases any object references the animation contains.
+ *
+ * Side effects: None.
+ */
+static void
+egg_animation_dispose (GObject *object)
+{
+  EggAnimation *self = EGG_ANIMATION (object);
+
+  g_clear_object (&self->target);
+  g_clear_object (&self->frame_clock);
+
+  G_OBJECT_CLASS (egg_animation_parent_class)->dispose (object);
+}
+
+
+/**
+ * egg_animation_finalize:
+ * @object: (in): A #EggAnimation.
+ *
+ * Finalizes the object and releases any resources allocated.
+ *
+ * Side effects: None.
+ */
+static void
+egg_animation_finalize (GObject *object)
+{
+  EggAnimation *self = EGG_ANIMATION (object);
+  Tween *tween;
+  guint i;
+
+  for (i = 0; i < self->tweens->len; i++)
+    {
+      tween = &g_array_index (self->tweens, Tween, i);
+      g_value_unset (&tween->begin);
+      g_value_unset (&tween->end);
+      g_param_spec_unref (tween->pspec);
+    }
+
+  g_array_unref (self->tweens);
+
+  G_OBJECT_CLASS (egg_animation_parent_class)->finalize (object);
+}
+
+
+/**
+ * egg_animation_set_property:
+ * @object: (in): A #GObject.
+ * @prop_id: (in): The property identifier.
+ * @value: (in): The given property.
+ * @pspec: (in): A #ParamSpec.
+ *
+ * Set a given #GObject property.
+ */
+static void
+egg_animation_set_property (GObject      *object,
+                            guint         prop_id,
+                            const GValue *value,
+                            GParamSpec   *pspec)
+{
+  EggAnimation *animation = EGG_ANIMATION (object);
+
+  switch (prop_id)
+    {
+    case PROP_DURATION:
+      animation->duration_msec = g_value_get_uint (value) * slow_down_factor;
+      break;
+
+    case PROP_FRAME_CLOCK:
+      egg_animation_set_frame_clock (animation, g_value_get_object (value));
+      break;
+
+    case PROP_MODE:
+      animation->mode = g_value_get_enum (value);
+      break;
+
+    case PROP_TARGET:
+      egg_animation_set_target (animation, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+
+/**
+ * egg_animation_class_init:
+ * @klass: (in): A #EggAnimationClass.
+ *
+ * Initializes the GObjectClass.
+ *
+ * Side effects: Properties, signals, and vtables are initialized.
+ */
+static void
+egg_animation_class_init (EggAnimationClass *klass)
+{
+  GObjectClass *object_class;
+  const gchar *slow_down_factor_env;
+
+  debug = !!g_getenv ("EGG_ANIMATION_DEBUG");
+  slow_down_factor_env = g_getenv ("EGG_ANIMATION_SLOW_DOWN_FACTOR");
+
+  if (slow_down_factor_env)
+    slow_down_factor = MAX (1, atoi (slow_down_factor_env));
+
+  object_class = G_OBJECT_CLASS (klass);
+  object_class->dispose = egg_animation_dispose;
+  object_class->finalize = egg_animation_finalize;
+  object_class->set_property = egg_animation_set_property;
+
+  /**
+   * EggAnimation:duration:
+   *
+   * The "duration" property is the total number of milliseconds that the
+   * animation should run before being completed.
+   */
+  properties[PROP_DURATION] =
+    g_param_spec_uint ("duration",
+                       "Duration",
+                       "The duration of the animation",
+                       0,
+                       G_MAXUINT,
+                       250,
+                       (G_PARAM_WRITABLE |
+                        G_PARAM_CONSTRUCT_ONLY |
+                        G_PARAM_STATIC_STRINGS));
+
+  properties[PROP_FRAME_CLOCK] =
+    g_param_spec_object ("frame-clock",
+                         "Frame Clock",
+                         "An optional frame-clock to synchronize with.",
+                         GDK_TYPE_FRAME_CLOCK,
+                         (G_PARAM_WRITABLE |
+                          G_PARAM_CONSTRUCT_ONLY |
+                          G_PARAM_STATIC_STRINGS));
+
+  /**
+   * EggAnimation:mode:
+   *
+   * The "mode" property is the Alpha function that should be used to
+   * determine the offset within the animation based on the current
+   * offset in the animations duration.
+   */
+  properties[PROP_MODE] =
+    g_param_spec_enum ("mode",
+                       "Mode",
+                       "The animation mode",
+                       EGG_TYPE_ANIMATION_MODE,
+                       EGG_ANIMATION_LINEAR,
+                       (G_PARAM_WRITABLE |
+                        G_PARAM_CONSTRUCT_ONLY |
+                        G_PARAM_STATIC_STRINGS));
+
+  /**
+   * EggAnimation:target:
+   *
+   * The "target" property is the #GObject that should have its properties
+   * animated.
+   */
+  properties[PROP_TARGET] =
+    g_param_spec_object ("target",
+                         "Target",
+                         "The target of the animation",
+                         G_TYPE_OBJECT,
+                         (G_PARAM_WRITABLE |
+                          G_PARAM_CONSTRUCT_ONLY |
+                          G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+
+  /**
+   * EggAnimation::tick:
+   *
+   * The "tick" signal is emitted on each frame in the animation.
+   */
+  signals[TICK] = g_signal_new ("tick",
+                                 EGG_TYPE_ANIMATION,
+                                 G_SIGNAL_RUN_FIRST,
+                                 0,
+                                 NULL, NULL, NULL,
+                                 G_TYPE_NONE,
+                                 0);
+
+#define SET_ALPHA(_T, _t) \
+  alpha_funcs[EGG_ANIMATION_ ## _T] = egg_animation_alpha_ ## _t
+
+  SET_ALPHA (LINEAR, linear);
+  SET_ALPHA (EASE_IN_QUAD, ease_in_quad);
+  SET_ALPHA (EASE_OUT_QUAD, ease_out_quad);
+  SET_ALPHA (EASE_IN_OUT_QUAD, ease_in_out_quad);
+  SET_ALPHA (EASE_IN_CUBIC, ease_in_cubic);
+  SET_ALPHA (EASE_OUT_CUBIC, ease_out_cubic);
+  SET_ALPHA (EASE_IN_OUT_CUBIC, ease_in_out_cubic);
+
+#define SET_TWEEN(_T, _t) \
+  G_STMT_START { \
+    guint idx = G_TYPE_ ## _T; \
+    tween_funcs[idx] = tween_ ## _t; \
+  } G_STMT_END
+
+  SET_TWEEN (INT, int);
+  SET_TWEEN (UINT, uint);
+  SET_TWEEN (LONG, long);
+  SET_TWEEN (ULONG, ulong);
+  SET_TWEEN (FLOAT, float);
+  SET_TWEEN (DOUBLE, double);
+}
+
+
+/**
+ * egg_animation_init:
+ * @animation: (in): A #EggAnimation.
+ *
+ * Initializes the #EggAnimation instance.
+ *
+ * Side effects: Everything.
+ */
+static void
+egg_animation_init (EggAnimation *animation)
+{
+  animation->duration_msec = 250;
+  animation->mode = EGG_ANIMATION_EASE_IN_OUT_QUAD;
+  animation->tweens = g_array_new (FALSE, FALSE, sizeof (Tween));
+  animation->last_offset = -G_MINDOUBLE;
+}
+
+
+/**
+ * egg_animation_mode_get_type:
+ *
+ * Retrieves the GType for #EggAnimationMode.
+ *
+ * Returns: A GType.
+ * Side effects: GType registered on first call.
+ */
+GType
+egg_animation_mode_get_type (void)
+{
+  static GType type_id = 0;
+  static const GEnumValue values[] = {
+    { EGG_ANIMATION_LINEAR, "EGG_ANIMATION_LINEAR", "linear" },
+    { EGG_ANIMATION_EASE_IN_QUAD, "EGG_ANIMATION_EASE_IN_QUAD", "ease-in-quad" },
+    { EGG_ANIMATION_EASE_IN_OUT_QUAD, "EGG_ANIMATION_EASE_IN_OUT_QUAD", "ease-in-out-quad" },
+    { EGG_ANIMATION_EASE_OUT_QUAD, "EGG_ANIMATION_EASE_OUT_QUAD", "ease-out-quad" },
+    { EGG_ANIMATION_EASE_IN_CUBIC, "EGG_ANIMATION_EASE_IN_CUBIC", "ease-in-cubic" },
+    { EGG_ANIMATION_EASE_OUT_CUBIC, "EGG_ANIMATION_EASE_OUT_CUBIC", "ease-out-cubic" },
+    { EGG_ANIMATION_EASE_IN_OUT_CUBIC, "EGG_ANIMATION_EASE_IN_OUT_CUBIC", "ease-in-out-cubic" },
+    { 0 }
+  };
+
+  if (G_UNLIKELY (!type_id))
+    type_id = g_enum_register_static ("EggAnimationMode", values);
+  return type_id;
+}
+
+/**
+ * egg_object_animatev:
+ * @object: A #GObject.
+ * @mode: The animation mode.
+ * @duration_msec: The duration in milliseconds.
+ * @frame_clock: (nullable): The #GdkFrameClock to synchronize to.
+ * @first_property: The first property to animate.
+ * @args: A variadac list of arguments
+ *
+ * Returns: (transfer none): A #EggAnimation.
+ */
+EggAnimation *
+egg_object_animatev (gpointer          object,
+                     EggAnimationMode  mode,
+                     guint             duration_msec,
+                     GdkFrameClock    *frame_clock,
+                     const gchar      *first_property,
+                     va_list           args)
+{
+  EggAnimation *animation;
+  GObjectClass *klass;
+  GObjectClass *pklass;
+  const gchar *name;
+  GParamSpec *pspec;
+  GtkWidget *parent;
+  GValue value = { 0 };
+  gchar *error = NULL;
+  GType type;
+  GType ptype;
+  gboolean enable_animations;
+
+  g_return_val_if_fail (first_property != NULL, NULL);
+  g_return_val_if_fail (mode < EGG_ANIMATION_LAST, NULL);
+
+  if ((frame_clock == NULL) && GTK_IS_WIDGET (object))
+    frame_clock = gtk_widget_get_frame_clock (GTK_WIDGET (object));
+
+  /*
+   * If we have a frame clock, then we must be in the gtk thread and we
+   * should check GtkSettings for disabled animations. If we are disabled,
+   * we will just make the timeout immediate.
+   */
+  if (frame_clock != NULL)
+    {
+      g_object_get (gtk_settings_get_default (),
+                    "gtk-enable-animations", &enable_animations,
+                    NULL);
+
+      if (enable_animations == FALSE)
+        duration_msec = 0;
+    }
+
+  name = first_property;
+  type = G_TYPE_FROM_INSTANCE (object);
+  klass = G_OBJECT_GET_CLASS (object);
+  animation = g_object_new (EGG_TYPE_ANIMATION,
+                            "duration", duration_msec,
+                            "frame-clock", frame_clock,
+                            "mode", mode,
+                            "target", object,
+                            NULL);
+
+  do
+    {
+      /*
+       * First check for the property on the object. If that does not exist
+       * then check if the object has a parent and look at its child
+       * properties (if it's a GtkWidget).
+       */
+      if (!(pspec = g_object_class_find_property (klass, name)))
+        {
+          if (!g_type_is_a (type, GTK_TYPE_WIDGET))
+            {
+              g_critical (_("Failed to find property %s in %s"),
+                          name, g_type_name (type));
+              goto failure;
+            }
+          if (!(parent = gtk_widget_get_parent (object)))
+            {
+              g_critical (_("Failed to find property %s in %s"),
+                          name, g_type_name (type));
+              goto failure;
+            }
+          pklass = G_OBJECT_GET_CLASS (parent);
+          ptype = G_TYPE_FROM_INSTANCE (parent);
+          if (!(pspec = gtk_container_class_find_child_property (pklass, name)))
+            {
+              g_critical (_("Failed to find property %s in %s or parent %s"),
+                          name, g_type_name (type), g_type_name (ptype));
+              goto failure;
+            }
+        }
+
+      g_value_init (&value, pspec->value_type);
+      G_VALUE_COLLECT (&value, args, 0, &error);
+      if (error != NULL)
+        {
+          g_critical (_("Failed to retrieve va_list value: %s"), error);
+          g_free (error);
+          goto failure;
+        }
+
+      egg_animation_add_property (animation, pspec, &value);
+      g_value_unset (&value);
+    }
+  while ((name = va_arg (args, const gchar *)));
+
+  egg_animation_start (animation);
+
+  return animation;
+
+failure:
+  g_object_ref_sink (animation);
+  g_object_unref (animation);
+  return NULL;
+}
+
+/**
+ * egg_object_animate:
+ * @object: (in): A #GObject.
+ * @mode: (in): The animation mode.
+ * @duration_msec: (in): The duration in milliseconds.
+ * @first_property: (in): The first property to animate.
+ *
+ * Animates the properties of @object. The can be set in a similar manner to g_object_set(). They
+ * will be animated from their current value to the target value over the time period.
+ *
+ * Return value: (transfer none): A #EggAnimation.
+ * Side effects: None.
+ */
+EggAnimation*
+egg_object_animate (gpointer        object,
+                    EggAnimationMode mode,
+                    guint           duration_msec,
+                    GdkFrameClock  *frame_clock,
+                    const gchar    *first_property,
+                    ...)
+{
+  EggAnimation *animation;
+  va_list args;
+
+  va_start (args, first_property);
+  animation = egg_object_animatev (object,
+                                   mode,
+                                   duration_msec,
+                                   frame_clock,
+                                   first_property,
+                                   args);
+  va_end (args);
+
+  return animation;
+}
+
+/**
+ * egg_object_animate_full:
+ *
+ * Return value: (transfer none): A #EggAnimation.
+ */
+EggAnimation*
+egg_object_animate_full (gpointer        object,
+                         EggAnimationMode mode,
+                         guint           duration_msec,
+                         GdkFrameClock  *frame_clock,
+                         GDestroyNotify  notify,
+                         gpointer        notify_data,
+                         const gchar    *first_property,
+                         ...)
+{
+  EggAnimation *animation;
+  va_list args;
+
+  va_start (args, first_property);
+  animation = egg_object_animatev (object,
+                                   mode,
+                                   duration_msec,
+                                   frame_clock,
+                                   first_property,
+                                   args);
+  va_end (args);
+
+  animation->notify = notify;
+  animation->notify_data = notify_data;
+
+  return animation;
+}
+
+guint
+egg_animation_calculate_duration (GdkMonitor *monitor,
+                                  gdouble     from_value,
+                                  gdouble     to_value)
+{
+  GdkRectangle geom;
+  gdouble distance_units;
+  gdouble distance_mm;
+  gdouble mm_per_frame;
+  gint height_mm;
+  gint refresh_rate;
+  gint n_frames;
+  guint ret;
+
+#define MM_PER_SECOND       (100.0)
+#define MIN_FRAMES_PER_ANIM (5)
+#define MAX_FRAMES_PER_ANIM (500)
+
+  g_assert (GDK_IS_MONITOR (monitor));
+  g_assert (from_value >= 0.0);
+  g_assert (to_value >= 0.0);
+
+  /*
+   * Get various monitor information we'll need to calculate the duration of
+   * the animation. We need the physical space of the monitor, the refresh
+   * rate, and geometry so that we can limit how many device units we will
+   * traverse per-frame of the animation. Failure to deal with the physical
+   * space results in jittery animations to the user.
+   *
+   * It would also be nice to take into account the acceleration curve so that
+   * we know the max amount of jump per frame, but that is getting into
+   * diminishing returns since we can just average it out.
+   */
+  height_mm = gdk_monitor_get_height_mm (monitor);
+  gdk_monitor_get_geometry (monitor, &geom);
+  refresh_rate = gdk_monitor_get_refresh_rate (monitor);
+  if (refresh_rate == 0)
+    refresh_rate = 60000;
+
+  /*
+   * The goal here is to determine the number of millimeters that we need to
+   * animate given a transition of distance_unit pixels. Since we are dealing
+   * with physical units (mm), we don't need to take into account the device
+   * scale underneath the widget. The equation comes out the same.
+   */
+
+  distance_units = ABS (from_value - to_value);
+  distance_mm = distance_units / (gdouble)geom.height * height_mm;
+  mm_per_frame = MM_PER_SECOND / (refresh_rate / 1000.0);
+  n_frames = (distance_mm / mm_per_frame) + 1;
+
+  ret = n_frames * (1000.0 / (refresh_rate / 1000.0));
+  ret = CLAMP (ret,
+               MIN_FRAMES_PER_ANIM * (1000000.0 / refresh_rate),
+               MAX_FRAMES_PER_ANIM * (1000000.0 / refresh_rate));
+
+  return ret;
+
+#undef MM_PER_SECOND
+#undef MIN_FRAMES_PER_ANIM
+#undef MAX_FRAMES_PER_ANIM
+}
diff --git a/lib/egg/egg-animation.h b/lib/egg/egg-animation.h
new file mode 100644
index 0000000..0f52782
--- /dev/null
+++ b/lib/egg/egg-animation.h
@@ -0,0 +1,81 @@
+/* egg-animation.h
+ *
+ * Copyright (C) 2010-2016 Christian Hergert <christian hergert me>
+ *
+ * This file is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU Lesser General Public License as published by the Free
+ * Software Foundation; either version 2.1 of the License, or (at your option)
+ * any later version.
+ *
+ * This file 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 Lesser 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 EGG_ANIMATION_H
+#define EGG_ANIMATION_H
+
+#include <gdk/gdk.h>
+
+G_BEGIN_DECLS
+
+#define EGG_TYPE_ANIMATION      (egg_animation_get_type())
+#define EGG_TYPE_ANIMATION_MODE (egg_animation_mode_get_type())
+
+G_DECLARE_FINAL_TYPE (EggAnimation, egg_animation,
+                      EGG, ANIMATION, GInitiallyUnowned)
+
+typedef enum   _EggAnimationMode    EggAnimationMode;
+
+enum _EggAnimationMode
+{
+  EGG_ANIMATION_LINEAR,
+  EGG_ANIMATION_EASE_IN_QUAD,
+  EGG_ANIMATION_EASE_OUT_QUAD,
+  EGG_ANIMATION_EASE_IN_OUT_QUAD,
+  EGG_ANIMATION_EASE_IN_CUBIC,
+  EGG_ANIMATION_EASE_OUT_CUBIC,
+  EGG_ANIMATION_EASE_IN_OUT_CUBIC,
+
+  EGG_ANIMATION_LAST
+};
+
+GType         egg_animation_mode_get_type (void);
+void          egg_animation_start         (EggAnimation     *animation);
+void          egg_animation_stop          (EggAnimation     *animation);
+void          egg_animation_add_property  (EggAnimation     *animation,
+                                           GParamSpec       *pspec,
+                                           const GValue     *value);
+
+EggAnimation *egg_object_animatev         (gpointer          object,
+                                           EggAnimationMode  mode,
+                                           guint             duration_msec,
+                                           GdkFrameClock    *frame_clock,
+                                           const gchar      *first_property,
+                                           va_list           args);
+EggAnimation* egg_object_animate          (gpointer          object,
+                                           EggAnimationMode  mode,
+                                           guint             duration_msec,
+                                           GdkFrameClock    *frame_clock,
+                                           const gchar      *first_property,
+                                           ...) G_GNUC_NULL_TERMINATED;
+EggAnimation* egg_object_animate_full     (gpointer          object,
+                                           EggAnimationMode  mode,
+                                           guint             duration_msec,
+                                           GdkFrameClock    *frame_clock,
+                                           GDestroyNotify    notify,
+                                           gpointer          notify_data,
+                                           const gchar      *first_property,
+                                           ...) G_GNUC_NULL_TERMINATED;
+
+guint egg_animation_calculate_duration (GdkMonitor *monitor,
+                                        gdouble     from_value,
+                                        gdouble     to_value);
+
+G_END_DECLS
+
+#endif /* EGG_ANIMATION_H */
diff --git a/lib/egg/egg-elastic-bin.c b/lib/egg/egg-elastic-bin.c
new file mode 100644
index 0000000..3e90c36
--- /dev/null
+++ b/lib/egg/egg-elastic-bin.c
@@ -0,0 +1,278 @@
+/* egg-elastic-bin.c
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU Lesser General Public License as published by the Free
+ * Software Foundation; either version 2.1 of the License, or (at your option)
+ * any later version.
+ *
+ * This file 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 Lesser 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/>.
+ */
+
+#define G_LOG_DOMAIN "egg-elastic-bin"
+
+#include "egg-animation.h"
+#include "egg-elastic-bin.h"
+
+#define MM_PER_SECOND       (100.0)
+#define MIN_FRAMES_PER_ANIM (5)
+#define MAX_FRAMES_PER_ANIM (500)
+
+#if 0
+# define _TRACE_LEVEL (1<<G_LOG_LEVEL_USER_SHIFT)
+# define _TRACE(...) do { g_log(G_LOG_DOMAIN, _TRACE_LEVEL, __VA_ARGS__); } while (0)
+# define TRACE_MSG(m,...) _TRACE("   MSG: %s():%u: "m, G_STRFUNC, __LINE__, __VA_ARGS__)
+# define ENTRY _TRACE(" ENTRY: %s(): %u", G_STRFUNC, __LINE__)
+# define EXIT do { _TRACE("  EXIT: %s(): %u", G_STRFUNC, __LINE__); return; } while (0)
+# define RETURN(r) do { _TRACE("  EXIT: %s(): %u", G_STRFUNC, __LINE__); return (r); } while (0)
+#else
+# define TRACE_MSG(m,...) do { } while (0)
+# define ENTRY            do { } while (0)
+# define EXIT             return
+# define RETURN(r)        return (r)
+#endif
+
+typedef struct
+{
+  GtkAdjustment *hadj;
+  EggAnimation  *hanim;
+  gint           cached_min_height;
+  gint           cached_nat_height;
+} EggElasticBinPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (EggElasticBin, egg_elastic_bin, GTK_TYPE_BIN)
+
+static void
+egg_elastic_bin_cancel_animation (EggElasticBin *self)
+{
+  EggElasticBinPrivate *priv = egg_elastic_bin_get_instance_private (self);
+
+  ENTRY;
+
+  g_assert (EGG_IS_ELASTIC_BIN (self));
+
+  if (priv->hanim != NULL)
+    {
+      EggAnimation *anim = priv->hanim;
+
+      g_object_remove_weak_pointer (G_OBJECT (priv->hanim), (gpointer *)&priv->hanim);
+      priv->hanim = NULL;
+
+      egg_animation_stop (anim);
+    }
+
+  EXIT;
+}
+
+static guint
+egg_elastic_bin_calculate_duration (EggElasticBin *self,
+                                    gdouble        from_value,
+                                    gdouble        to_value)
+{
+  GdkDisplay *display;
+  GdkMonitor *monitor;
+  GdkWindow *window;
+
+  g_assert (EGG_IS_ELASTIC_BIN (self));
+  g_assert (from_value >= 0.0);
+  g_assert (to_value >= 0.0);
+
+  if (NULL != (display = gtk_widget_get_display (GTK_WIDGET (self))) &&
+      NULL != (window = gtk_widget_get_window (GTK_WIDGET (self))) &&
+      NULL != (monitor = gdk_display_get_monitor_at_window (display, window)))
+    return egg_animation_calculate_duration (monitor, from_value, to_value);
+
+  return 0;
+}
+
+static void
+egg_elastic_bin_animate_to (EggElasticBin *self,
+                            gdouble        value)
+{
+  EggElasticBinPrivate *priv = egg_elastic_bin_get_instance_private (self);
+  guint duration;
+
+  ENTRY;
+
+  g_assert (EGG_IS_ELASTIC_BIN (self));
+
+  egg_elastic_bin_cancel_animation (self);
+
+  duration = egg_elastic_bin_calculate_duration (self,
+                                                 gtk_adjustment_get_value (priv->hadj),
+                                                 value);
+
+  TRACE_MSG ("Duration is %u milliseconds", duration);
+
+  priv->hanim = egg_object_animate (priv->hadj,
+                                    EGG_ANIMATION_EASE_OUT_CUBIC,
+                                    duration,
+                                    gtk_widget_get_frame_clock (GTK_WIDGET (self)),
+                                    "value", value,
+                                    NULL);
+  g_object_add_weak_pointer (G_OBJECT (priv->hanim), (gpointer *)&priv->hanim);
+
+  EXIT;
+}
+
+static void
+egg_elastic_bin_get_preferred_height_for_width (GtkWidget *widget,
+                                                gint       width,
+                                                gint      *min_height,
+                                                gint      *nat_height)
+{
+  EggElasticBin *self = (EggElasticBin *)widget;
+  EggElasticBinPrivate *priv = egg_elastic_bin_get_instance_private (self);
+
+  ENTRY;
+
+  TRACE_MSG ("width=%d", width);
+
+  g_assert (EGG_IS_ELASTIC_BIN (self));
+
+  /*
+   * We must always chain up to the parent get_preferred_height_for_width()
+   * so that we can detect changes while we are animating.
+   */
+
+  GTK_WIDGET_CLASS (egg_elastic_bin_parent_class)->get_preferred_height_for_width (widget, width, 
min_height, nat_height);
+
+  /*
+   * If we are animating the widget, and the size request hasn't changed since
+   * our last animation frame, go ahead and process that now.
+   */
+
+  if (*min_height == priv->cached_min_height &&
+      *nat_height == priv->cached_nat_height &&
+      priv->hanim != NULL)
+    {
+      *min_height = priv->cached_min_height;
+      *nat_height = (gint)gtk_adjustment_get_value (priv->hadj);
+
+      TRACE_MSG ("Fast path min=%d nat=%d", *min_height, *nat_height);
+
+      if (*nat_height == priv->cached_nat_height)
+        egg_elastic_bin_cancel_animation (self);
+
+      EXIT;
+    }
+
+  if (*min_height != priv->cached_min_height || *nat_height != priv->cached_nat_height)
+    {
+      priv->cached_min_height = *min_height;
+      priv->cached_nat_height = *nat_height;
+
+      TRACE_MSG ("New requested height is min=%d nat=%d",
+                 *min_height, *nat_height);
+
+      if (*min_height > (gint)gtk_adjustment_get_value (priv->hadj))
+        gtk_adjustment_set_value (priv->hadj, *min_height);
+
+      *min_height = priv->cached_min_height;
+      *nat_height = (gint)gtk_adjustment_get_value (priv->hadj);
+
+      egg_elastic_bin_animate_to (self, priv->cached_nat_height);
+
+      TRACE_MSG ("!!! min=%d nat=%d", *min_height, *nat_height);
+
+      EXIT;
+    }
+
+  TRACE_MSG ("*** min=%d nat=%d", *min_height, *nat_height);
+
+  EXIT;
+}
+
+static void
+egg_elastic_bin_hadj_value_changed (EggElasticBin *self,
+                                    GtkAdjustment *adj)
+{
+  ENTRY;
+
+  g_assert (EGG_IS_ELASTIC_BIN (self));
+  g_assert (GTK_IS_ADJUSTMENT (adj));
+
+  gtk_widget_queue_resize (GTK_WIDGET (self));
+
+  EXIT;
+}
+
+static void
+egg_elastic_bin_size_allocate (GtkWidget     *widget,
+                               GtkAllocation *allocation)
+{
+  g_assert (GTK_IS_WIDGET (widget));
+  g_assert (allocation != NULL);
+
+  TRACE_MSG ("Allocating %d,%d %dx%d",
+             allocation->x, allocation->y,
+             allocation->width, allocation->height);
+
+  GTK_WIDGET_CLASS (egg_elastic_bin_parent_class)->size_allocate (widget, allocation);
+}
+
+static GtkSizeRequestMode
+egg_elastic_bin_get_request_mode (GtkWidget *widget)
+{
+  return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
+}
+
+static void
+egg_elastic_bin_destroy (GtkWidget *widget)
+{
+  EggElasticBin *self = (EggElasticBin *)widget;
+
+  g_assert (EGG_IS_ELASTIC_BIN (self));
+
+  egg_elastic_bin_cancel_animation (self);
+
+  GTK_WIDGET_CLASS (egg_elastic_bin_parent_class)->destroy (widget);
+}
+
+static void
+egg_elastic_bin_finalize (GObject *object)
+{
+  EggElasticBin *self = (EggElasticBin *)object;
+  EggElasticBinPrivate *priv = egg_elastic_bin_get_instance_private (self);
+
+  g_clear_object (&priv->hadj);
+
+  G_OBJECT_CLASS (egg_elastic_bin_parent_class)->finalize (object);
+}
+
+static void
+egg_elastic_bin_class_init (EggElasticBinClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = egg_elastic_bin_finalize;
+
+  widget_class->destroy = egg_elastic_bin_destroy;
+  widget_class->get_preferred_height_for_width = egg_elastic_bin_get_preferred_height_for_width;
+  widget_class->size_allocate = egg_elastic_bin_size_allocate;
+  widget_class->get_request_mode = egg_elastic_bin_get_request_mode;
+
+  gtk_widget_class_set_css_name (widget_class, "elastic");
+}
+
+static void
+egg_elastic_bin_init (EggElasticBin *self)
+{
+  EggElasticBinPrivate *priv = egg_elastic_bin_get_instance_private (self);
+
+  priv->hadj = gtk_adjustment_new (0, 0, G_MAXINT, 1, 1, 1);
+
+  g_signal_connect_object (priv->hadj,
+                           "value-changed",
+                           G_CALLBACK (egg_elastic_bin_hadj_value_changed),
+                           self,
+                           G_CONNECT_SWAPPED);
+}
diff --git a/lib/egg/egg-elastic-bin.h b/lib/egg/egg-elastic-bin.h
new file mode 100644
index 0000000..dcd0805
--- /dev/null
+++ b/lib/egg/egg-elastic-bin.h
@@ -0,0 +1,44 @@
+/* egg-elastic-bin.h
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU Lesser General Public License as published by the Free
+ * Software Foundation; either version 2.1 of the License, or (at your option)
+ * any later version.
+ *
+ * This file 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 Lesser 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 EGG_ELASTIC_BIN_H
+#define EGG_ELASTIC_BIN_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define EGG_TYPE_ELASTIC_BIN (egg_elastic_bin_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (EggElasticBin, egg_elastic_bin, EGG, ELASTIC_BIN, GtkBin)
+
+struct _EggElasticBinClass
+{
+  GtkBinClass parent_class;
+
+  gpointer _reserved1;
+  gpointer _reserved2;
+  gpointer _reserved3;
+  gpointer _reserved4;
+};
+
+GtkWidget *egg_elastic_bin_new (void);
+
+G_END_DECLS
+
+#endif /* EGG_ELASTIC_BIN_H */
diff --git a/lib/egg/egg-frame-source.c b/lib/egg/egg-frame-source.c
new file mode 100644
index 0000000..64a8593
--- /dev/null
+++ b/lib/egg/egg-frame-source.c
@@ -0,0 +1,130 @@
+/* egg-frame-source.c
+ *
+ * Copyright (C) 2010-2016 Christian Hergert <christian hergert me>
+ *
+ * This file is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU Lesser General Public License as published by the Free
+ * Software Foundation; either version 2.1 of the License, or (at your option)
+ * any later version.
+ *
+ * This file 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 Lesser 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 "egg-frame-source.h"
+
+typedef struct
+{
+   GSource parent;
+   guint   fps;
+   guint   frame_count;
+   gint64  start_time;
+} EggFrameSource;
+
+static gboolean
+egg_frame_source_prepare (GSource *source,
+                          gint    *timeout_)
+{
+   EggFrameSource *fsource = (EggFrameSource *)(gpointer)source;
+   gint64 current_time;
+   guint elapsed_time;
+   guint new_frame_num;
+   guint frame_time;
+
+   current_time = g_source_get_time(source) / 1000;
+   elapsed_time = current_time - fsource->start_time;
+   new_frame_num = elapsed_time * fsource->fps / 1000;
+
+   /* If time has gone backwards or the time since the last frame is
+    * greater than the two frames worth then reset the time and do a
+    * frame now */
+   if (new_frame_num < fsource->frame_count ||
+       new_frame_num - fsource->frame_count > 2) {
+      /* Get the frame time rounded up to the nearest ms */
+      frame_time = (1000 + fsource->fps - 1) / fsource->fps;
+
+      /* Reset the start time */
+      fsource->start_time = current_time;
+
+      /* Move the start time as if one whole frame has elapsed */
+      fsource->start_time -= frame_time;
+      fsource->frame_count = 0;
+      *timeout_ = 0;
+      return TRUE;
+   } else if (new_frame_num > fsource->frame_count) {
+      *timeout_ = 0;
+      return TRUE;
+   } else {
+      *timeout_ = (fsource->frame_count + 1) * 1000 / fsource->fps - elapsed_time;
+      return FALSE;
+   }
+}
+
+static gboolean
+egg_frame_source_check (GSource *source)
+{
+   gint timeout_;
+   return egg_frame_source_prepare(source, &timeout_);
+}
+
+static gboolean
+egg_frame_source_dispatch (GSource     *source,
+                           GSourceFunc  source_func,
+                           gpointer     user_data)
+{
+   EggFrameSource *fsource = (EggFrameSource *)(gpointer)source;
+   gboolean ret;
+
+   if ((ret = source_func(user_data)))
+      fsource->frame_count++;
+   return ret;
+}
+
+static GSourceFuncs source_funcs = {
+   egg_frame_source_prepare,
+   egg_frame_source_check,
+   egg_frame_source_dispatch,
+};
+
+/**
+ * egg_frame_source_add:
+ * @frames_per_sec: (in): Target frames per second.
+ * @callback: (in) (scope notified): A #GSourceFunc to execute.
+ * @user_data: (in): User data for @callback.
+ *
+ * Creates a new frame source that will execute when the timeout interval
+ * for the source has elapsed. The timing will try to synchronize based
+ * on the end time of the animation.
+ *
+ * Returns: A source id that can be removed with g_source_remove().
+ */
+guint
+egg_frame_source_add (guint       frames_per_sec,
+                      GSourceFunc callback,
+                      gpointer    user_data)
+{
+   EggFrameSource *fsource;
+   GSource *source;
+   guint ret;
+
+   g_return_val_if_fail (frames_per_sec > 0, 0);
+   g_return_val_if_fail (frames_per_sec <= 120, 0);
+
+   source = g_source_new(&source_funcs, sizeof(EggFrameSource));
+   fsource = (EggFrameSource *)(gpointer)source;
+   fsource->fps = frames_per_sec;
+   fsource->frame_count = 0;
+   fsource->start_time = g_get_monotonic_time() / 1000;
+   g_source_set_callback(source, callback, user_data, NULL);
+   g_source_set_name(source, "EggFrameSource");
+
+   ret = g_source_attach(source, NULL);
+   g_source_unref(source);
+
+   return ret;
+}
diff --git a/lib/egg/egg-frame-source.h b/lib/egg/egg-frame-source.h
new file mode 100644
index 0000000..3ba234f
--- /dev/null
+++ b/lib/egg/egg-frame-source.h
@@ -0,0 +1,32 @@
+/* egg-frame-source.h
+ *
+ * Copyright (C) 2010-2016 Christian Hergert <christian hergert me>
+ *
+ * This file is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU Lesser General Public License as published by the Free
+ * Software Foundation; either version 2.1 of the License, or (at your option)
+ * any later version.
+ *
+ * This file 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 Lesser 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 EGG_FRAME_SOURCE_H
+#define EGG_FRAME_SOURCE_H
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+guint egg_frame_source_add (guint       frames_per_sec,
+                            GSourceFunc callback,
+                            gpointer    user_data);
+
+G_END_DECLS
+
+#endif /* EGG_FRAME_SOURCE_H */
diff --git a/lib/egg/egg-suggestion-entry-buffer.c b/lib/egg/egg-suggestion-entry-buffer.c
new file mode 100644
index 0000000..68d3a6e
--- /dev/null
+++ b/lib/egg/egg-suggestion-entry-buffer.c
@@ -0,0 +1,408 @@
+/* egg-suggestion-entry-buffer.c
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU Lesser General Public License as published by the Free
+ * Software Foundation; either version 2.1 of the License, or (at your option)
+ * any later version.
+ *
+ * This file 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 Lesser 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/>.
+ */
+
+#define G_LOG_DOMAIN "egg-suggestion-entry-buffer"
+
+#include <string.h>
+
+#include "egg-suggestion-entry-buffer.h"
+
+typedef struct
+{
+  EggSuggestion *suggestion;
+  gchar         *text;
+  gchar         *suffix;
+  guint          in_insert : 1;
+  guint          in_delete : 1;
+} EggSuggestionEntryBufferPrivate;
+
+enum {
+  PROP_0,
+  PROP_SUGGESTION,
+  N_PROPS
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (EggSuggestionEntryBuffer, egg_suggestion_entry_buffer, GTK_TYPE_ENTRY_BUFFER)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+egg_suggestion_entry_buffer_drop_suggestion (EggSuggestionEntryBuffer *self)
+{
+  EggSuggestionEntryBufferPrivate *priv = egg_suggestion_entry_buffer_get_instance_private (self);
+
+  g_assert (EGG_IS_SUGGESTION_ENTRY_BUFFER (self));
+
+  if (priv->suffix != NULL)
+    {
+      guint length = GTK_ENTRY_BUFFER_CLASS (egg_suggestion_entry_buffer_parent_class)->get_length 
(GTK_ENTRY_BUFFER (self));
+      guint suffix_len = strlen (priv->suffix);
+
+      g_clear_pointer (&priv->suffix, g_free);
+      gtk_entry_buffer_emit_deleted_text (GTK_ENTRY_BUFFER (self), length, suffix_len);
+    }
+}
+
+static void
+egg_suggestion_entry_buffer_insert_suggestion (EggSuggestionEntryBuffer *self)
+{
+  EggSuggestionEntryBufferPrivate *priv = egg_suggestion_entry_buffer_get_instance_private (self);
+
+  g_assert (EGG_IS_SUGGESTION_ENTRY_BUFFER (self));
+
+  if (priv->suggestion != NULL)
+    {
+      g_autofree gchar *suffix = NULL;
+      const gchar *text;
+      guint length;
+
+      length = GTK_ENTRY_BUFFER_CLASS (egg_suggestion_entry_buffer_parent_class)->get_length 
(GTK_ENTRY_BUFFER (self));
+      text = GTK_ENTRY_BUFFER_CLASS (egg_suggestion_entry_buffer_parent_class)->get_text (GTK_ENTRY_BUFFER 
(self), NULL);
+      suffix = egg_suggestion_suggest_suffix (priv->suggestion, text);
+
+      if (suffix != NULL)
+        {
+          priv->suffix = g_steal_pointer (&suffix);
+          gtk_entry_buffer_emit_inserted_text (GTK_ENTRY_BUFFER (self),
+                                               length,
+                                               priv->suffix,
+                                               g_utf8_strlen (priv->suffix, -1));
+        }
+    }
+}
+
+const gchar *
+egg_suggestion_entry_buffer_get_typed_text (EggSuggestionEntryBuffer *self)
+{
+  g_return_val_if_fail (EGG_IS_SUGGESTION_ENTRY_BUFFER (self), NULL);
+
+  return GTK_ENTRY_BUFFER_CLASS (egg_suggestion_entry_buffer_parent_class)->get_text (GTK_ENTRY_BUFFER 
(self), NULL);
+}
+
+static const gchar *
+egg_suggestion_entry_buffer_get_text (GtkEntryBuffer *buffer,
+                                      gsize          *n_bytes)
+{
+  EggSuggestionEntryBuffer *self = (EggSuggestionEntryBuffer *)buffer;
+  EggSuggestionEntryBufferPrivate *priv = egg_suggestion_entry_buffer_get_instance_private (self);
+
+  g_assert (EGG_IS_SUGGESTION_ENTRY_BUFFER (self));
+
+  if (priv->text == NULL)
+    {
+      const gchar *text;
+      GString *str = NULL;
+
+      text = GTK_ENTRY_BUFFER_CLASS (egg_suggestion_entry_buffer_parent_class)->get_text (buffer, n_bytes);
+
+      str = g_string_new (text);
+      if (priv->suffix != NULL)
+        g_string_append (str, priv->suffix);
+      priv->text = g_string_free (str, FALSE);
+    }
+
+  return priv->text;
+}
+
+static guint
+egg_suggestion_entry_buffer_get_length (GtkEntryBuffer *buffer)
+{
+  EggSuggestionEntryBuffer *self = (EggSuggestionEntryBuffer *)buffer;
+  EggSuggestionEntryBufferPrivate *priv = egg_suggestion_entry_buffer_get_instance_private (self);
+  guint ret;
+
+  g_assert (GTK_IS_ENTRY_BUFFER (buffer));
+
+  ret = GTK_ENTRY_BUFFER_CLASS (egg_suggestion_entry_buffer_parent_class)->get_length (buffer);
+
+  if (priv->suffix != NULL)
+    ret += strlen (priv->suffix);
+
+  return ret;
+}
+
+static void
+egg_suggestion_entry_buffer_inserted_text (GtkEntryBuffer *buffer,
+                                           guint           position,
+                                           const gchar    *chars,
+                                           guint           n_chars)
+{
+  EggSuggestionEntryBuffer *self = (EggSuggestionEntryBuffer *)buffer;
+  EggSuggestionEntryBufferPrivate *priv = egg_suggestion_entry_buffer_get_instance_private (self);
+
+  g_assert (GTK_IS_ENTRY_BUFFER (buffer));
+
+  g_clear_pointer (&priv->text, g_free);
+
+  GTK_ENTRY_BUFFER_CLASS (egg_suggestion_entry_buffer_parent_class)->inserted_text (buffer, position, chars, 
n_chars);
+}
+
+static void
+egg_suggestion_entry_buffer_deleted_text (GtkEntryBuffer *buffer,
+                                          guint           position,
+                                          guint           n_chars)
+{
+  EggSuggestionEntryBuffer *self = (EggSuggestionEntryBuffer *)buffer;
+  EggSuggestionEntryBufferPrivate *priv = egg_suggestion_entry_buffer_get_instance_private (self);
+
+  g_assert (GTK_IS_ENTRY_BUFFER (buffer));
+
+  g_clear_pointer (&priv->text, g_free);
+
+  GTK_ENTRY_BUFFER_CLASS (egg_suggestion_entry_buffer_parent_class)->deleted_text (buffer, position, 
n_chars);
+}
+
+static guint
+egg_suggestion_entry_buffer_insert_text (GtkEntryBuffer *buffer,
+                                         guint           position,
+                                         const gchar    *chars,
+                                         guint           n_chars)
+{
+  EggSuggestionEntryBuffer *self = (EggSuggestionEntryBuffer *)buffer;
+  EggSuggestionEntryBufferPrivate *priv = egg_suggestion_entry_buffer_get_instance_private (self);
+  guint ret = 0;
+
+  g_assert (GTK_IS_ENTRY_BUFFER (buffer));
+  g_assert (chars != NULL || n_chars == 0);
+  g_assert (priv->in_insert == FALSE);
+
+  priv->in_insert = TRUE;
+
+  if (n_chars == 0)
+    goto failure;
+
+  egg_suggestion_entry_buffer_drop_suggestion (self);
+
+  ret = GTK_ENTRY_BUFFER_CLASS (egg_suggestion_entry_buffer_parent_class)->insert_text (buffer, position, 
chars, n_chars);
+  if (ret < n_chars)
+    goto failure;
+
+  egg_suggestion_entry_buffer_insert_suggestion (self);
+
+failure:
+  priv->in_insert = FALSE;
+
+  return ret;
+}
+
+static guint
+egg_suggestion_entry_buffer_delete_text (GtkEntryBuffer *buffer,
+                                         guint           position,
+                                         guint           n_chars)
+{
+  EggSuggestionEntryBuffer *self = (EggSuggestionEntryBuffer *)buffer;
+  EggSuggestionEntryBufferPrivate *priv = egg_suggestion_entry_buffer_get_instance_private (self);
+  guint length;
+  guint ret = 0;
+
+  g_assert (GTK_IS_ENTRY_BUFFER (buffer));
+
+  priv->in_delete = TRUE;
+
+  length = GTK_ENTRY_BUFFER_CLASS (egg_suggestion_entry_buffer_parent_class)->get_length (buffer);
+
+  if (position >= length)
+    goto failure;
+
+  if (position + n_chars > length)
+    n_chars = length - position;
+
+  egg_suggestion_entry_buffer_drop_suggestion (self);
+
+  ret = GTK_ENTRY_BUFFER_CLASS (egg_suggestion_entry_buffer_parent_class)->delete_text (buffer, position, 
n_chars);
+
+  if (ret != 0 && priv->suggestion != NULL)
+    egg_suggestion_entry_buffer_insert_suggestion (self);
+
+failure:
+  priv->in_delete = FALSE;
+
+  return ret;
+}
+
+static void
+egg_suggestion_entry_buffer_finalize (GObject *object)
+{
+  EggSuggestionEntryBuffer *self = (EggSuggestionEntryBuffer *)object;
+  EggSuggestionEntryBufferPrivate *priv = egg_suggestion_entry_buffer_get_instance_private (self);
+
+  g_clear_object (&priv->suggestion);
+  g_clear_pointer (&priv->text, g_free);
+  g_clear_pointer (&priv->suffix, g_free);
+
+  G_OBJECT_CLASS (egg_suggestion_entry_buffer_parent_class)->finalize (object);
+}
+
+static void
+egg_suggestion_entry_buffer_get_property (GObject    *object,
+                                          guint       prop_id,
+                                          GValue     *value,
+                                          GParamSpec *pspec)
+{
+  EggSuggestionEntryBuffer *self = EGG_SUGGESTION_ENTRY_BUFFER (object);
+
+  switch (prop_id)
+    {
+    case PROP_SUGGESTION:
+      g_value_set_object (value, egg_suggestion_entry_buffer_get_suggestion (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+egg_suggestion_entry_buffer_set_property (GObject      *object,
+                                          guint         prop_id,
+                                          const GValue *value,
+                                          GParamSpec   *pspec)
+{
+  EggSuggestionEntryBuffer *self = EGG_SUGGESTION_ENTRY_BUFFER (object);
+
+  switch (prop_id)
+    {
+    case PROP_SUGGESTION:
+      egg_suggestion_entry_buffer_set_suggestion (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+egg_suggestion_entry_buffer_class_init (EggSuggestionEntryBufferClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkEntryBufferClass *entry_buffer_class = GTK_ENTRY_BUFFER_CLASS (klass);
+
+  object_class->finalize = egg_suggestion_entry_buffer_finalize;
+  object_class->get_property = egg_suggestion_entry_buffer_get_property;
+  object_class->set_property = egg_suggestion_entry_buffer_set_property;
+
+  entry_buffer_class->inserted_text = egg_suggestion_entry_buffer_inserted_text;
+  entry_buffer_class->deleted_text = egg_suggestion_entry_buffer_deleted_text;
+  entry_buffer_class->get_text = egg_suggestion_entry_buffer_get_text;
+  entry_buffer_class->get_length = egg_suggestion_entry_buffer_get_length;
+  entry_buffer_class->insert_text = egg_suggestion_entry_buffer_insert_text;
+  entry_buffer_class->delete_text = egg_suggestion_entry_buffer_delete_text;
+
+  properties [PROP_SUGGESTION] =
+    g_param_spec_object ("suggestion",
+                         "Suggestion",
+                         "The suggestion currently selected",
+                         EGG_TYPE_SUGGESTION,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+egg_suggestion_entry_buffer_init (EggSuggestionEntryBuffer *self)
+{
+}
+
+EggSuggestionEntryBuffer *
+egg_suggestion_entry_buffer_new (void)
+{
+  return g_object_new (EGG_TYPE_SUGGESTION_ENTRY_BUFFER, NULL);
+}
+
+/**
+ * egg_suggestion_entry_buffer_get_suggestion:
+ * @self: a #EggSuggestionEntryBuffer
+ *
+ * Gets the #EggSuggestion that is the current "preview suffix" of the
+ * text in the entry.
+ *
+ * Returns: (transfer none) (nullable): An #EggSuggestion or %NULL.
+ */
+EggSuggestion *
+egg_suggestion_entry_buffer_get_suggestion (EggSuggestionEntryBuffer *self)
+{
+  EggSuggestionEntryBufferPrivate *priv = egg_suggestion_entry_buffer_get_instance_private (self);
+
+  g_return_val_if_fail (EGG_IS_SUGGESTION_ENTRY_BUFFER (self), NULL);
+
+  return priv->suggestion;
+}
+
+/**
+ * egg_suggestion_entry_buffer_set_suggestion:
+ * @self: a #EggSuggestionEntryBuffer
+ * @suggestion: (nullable): An #EggSuggestion or %NULL
+ *
+ * Sets the current suggestion for the entry buffer.
+ *
+ * The suggestion is used to get a potential suffix for the current entry
+ * text. This allows the entry to show "preview text" after the entered
+ * text for what might be inserted should they activate the current item.
+ */
+void
+egg_suggestion_entry_buffer_set_suggestion (EggSuggestionEntryBuffer *self,
+                                            EggSuggestion            *suggestion)
+{
+  EggSuggestionEntryBufferPrivate *priv = egg_suggestion_entry_buffer_get_instance_private (self);
+
+  g_return_if_fail (EGG_IS_SUGGESTION_ENTRY_BUFFER (self));
+  g_return_if_fail (!suggestion || EGG_IS_SUGGESTION (suggestion));
+
+  if (priv->suggestion != suggestion)
+    {
+      egg_suggestion_entry_buffer_drop_suggestion (self);
+      g_set_object (&priv->suggestion, suggestion);
+      if (!priv->in_delete && !priv->in_insert)
+        egg_suggestion_entry_buffer_insert_suggestion (self);
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SUGGESTION]);
+    }
+}
+
+guint
+egg_suggestion_entry_buffer_get_typed_length (EggSuggestionEntryBuffer *self)
+{
+  const gchar *text;
+
+  g_return_val_if_fail (EGG_IS_SUGGESTION_ENTRY_BUFFER (self), 0);
+
+  text = egg_suggestion_entry_buffer_get_typed_text (self);
+
+  return text ? g_utf8_strlen (text, -1) : 0;
+}
+
+void
+egg_suggestion_entry_buffer_commit (EggSuggestionEntryBuffer *self)
+{
+  EggSuggestionEntryBufferPrivate *priv = egg_suggestion_entry_buffer_get_instance_private (self);
+
+  g_return_if_fail (EGG_IS_SUGGESTION_ENTRY_BUFFER (self));
+
+  if (priv->suffix != NULL)
+    {
+      g_autofree gchar *suffix = g_steal_pointer (&priv->suffix);
+      guint position;
+
+      g_clear_object (&priv->suggestion);
+      position = GTK_ENTRY_BUFFER_CLASS (egg_suggestion_entry_buffer_parent_class)->get_length 
(GTK_ENTRY_BUFFER (self));
+      GTK_ENTRY_BUFFER_CLASS (egg_suggestion_entry_buffer_parent_class)->insert_text (GTK_ENTRY_BUFFER 
(self),
+                                                                                      position,
+                                                                                      suffix,
+                                                                                      g_utf8_strlen (suffix, 
-1));
+    }
+}
diff --git a/lib/egg/egg-suggestion-entry-buffer.h b/lib/egg/egg-suggestion-entry-buffer.h
new file mode 100644
index 0000000..e8078b7
--- /dev/null
+++ b/lib/egg/egg-suggestion-entry-buffer.h
@@ -0,0 +1,52 @@
+/* egg-suggestion-entry-buffer.h
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU Lesser General Public License as published by the Free
+ * Software Foundation; either version 2.1 of the License, or (at your option)
+ * any later version.
+ *
+ * This file 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 Lesser 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 EGG_SUGGESTION_ENTRY_BUFFER_H
+#define EGG_SUGGESTION_ENTRY_BUFFER_H
+
+#include <gtk/gtk.h>
+
+#include "egg-suggestion.h"
+
+G_BEGIN_DECLS
+
+#define EGG_TYPE_SUGGESTION_ENTRY_BUFFER (egg_suggestion_entry_buffer_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (EggSuggestionEntryBuffer, egg_suggestion_entry_buffer, EGG, 
SUGGESTION_ENTRY_BUFFER, GtkEntryBuffer)
+
+struct _EggSuggestionEntryBufferClass
+{
+  GtkEntryBufferClass parent_class;
+
+  gpointer _reserved1;
+  gpointer _reserved2;
+  gpointer _reserved3;
+  gpointer _reserved4;
+};
+
+EggSuggestionEntryBuffer *egg_suggestion_entry_buffer_new              (void);
+EggSuggestion            *egg_suggestion_entry_buffer_get_suggestion   (EggSuggestionEntryBuffer *self);
+void                      egg_suggestion_entry_buffer_set_suggestion   (EggSuggestionEntryBuffer *self,
+                                                                        EggSuggestion            
*suggestion);
+const gchar              *egg_suggestion_entry_buffer_get_typed_text   (EggSuggestionEntryBuffer *self);
+guint                     egg_suggestion_entry_buffer_get_typed_length (EggSuggestionEntryBuffer *self);
+void                      egg_suggestion_entry_buffer_commit           (EggSuggestionEntryBuffer *self);
+
+G_END_DECLS
+
+#endif /* EGG_SUGGESTION_ENTRY_BUFFER_H */
diff --git a/lib/egg/egg-suggestion-entry.c b/lib/egg/egg-suggestion-entry.c
new file mode 100644
index 0000000..4bd8c26
--- /dev/null
+++ b/lib/egg/egg-suggestion-entry.c
@@ -0,0 +1,663 @@
+/* egg-suggestion-entry.c
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU Lesser General Public License as published by the Free
+ * Software Foundation; either version 2.1 of the License, or (at your option)
+ * any later version.
+ *
+ * This file 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 Lesser 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/>.
+ */
+
+#define G_LOG_DOMAIN "egg-suggestion-entry"
+
+#include <glib/gi18n.h>
+
+#include "egg-suggestion.h"
+#include "egg-suggestion-entry.h"
+#include "egg-suggestion-entry-buffer.h"
+#include "egg-suggestion-popover.h"
+
+#if 0
+# define _TRACE_LEVEL (1<<G_LOG_LEVEL_USER_SHIFT)
+# define _TRACE(...) do { g_log(G_LOG_DOMAIN, _TRACE_LEVEL, __VA_ARGS__); } while (0)
+# define EGG_TRACE_MSG(m,...) _TRACE("   MSG: %s():%u: "m, G_STRFUNC, __LINE__, __VA_ARGS__)
+# define EGG_ENTRY _TRACE(" ENTRY: %s(): %u", G_STRFUNC, __LINE__)
+# define EGG_EXIT do { _TRACE("  EXIT: %s(): %u", G_STRFUNC, __LINE__); return; } while (0)
+# define EGG_RETURN(r) do { _TRACE("  EXIT: %s(): %u", G_STRFUNC, __LINE__); return (r); } while (0)
+# define EGG_GOTO(_l) do { _TRACE("  GOTO: %s(): %u: %s", G_STRFUNC, __LINE__, #_l); goto _l; } while (0)
+#else
+# define EGG_TRACE_MSG(m,...) do { } while (0)
+# define EGG_ENTRY            do { } while (0)
+# define EGG_EXIT             return
+# define EGG_RETURN(r)        return (r)
+# define EGG_GOTO(_l)         goto _l
+#endif
+
+typedef struct
+{
+  EggSuggestionPopover     *popover;
+  EggSuggestionEntryBuffer *buffer;
+  GListModel               *model;
+  gulong                    changed_handler;
+} EggSuggestionEntryPrivate;
+
+enum {
+  PROP_0,
+  PROP_MODEL,
+  PROP_TYPED_TEXT,
+  N_PROPS
+};
+
+enum {
+  ACTIVATE_SUGGESTION,
+  HIDE_SUGGESTIONS,
+  MOVE_SUGGESTION,
+  SHOW_SUGGESTIONS,
+  SUGGESTION_ACTIVATED,
+  N_SIGNALS
+};
+
+static void buildable_iface_init (GtkBuildableIface    *iface);
+static void editable_iface_init  (GtkEditableInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (EggSuggestionEntry, egg_suggestion_entry, GTK_TYPE_ENTRY,
+                         G_ADD_PRIVATE (EggSuggestionEntry)
+                         G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, buildable_iface_init)
+                         G_IMPLEMENT_INTERFACE (GTK_TYPE_EDITABLE, editable_iface_init))
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+static guint changed_signal_id;
+static GtkEditableInterface *editable_parent_iface;
+
+static void
+egg_suggestion_entry_show_suggestions (EggSuggestionEntry *self)
+{
+  EggSuggestionEntryPrivate *priv = egg_suggestion_entry_get_instance_private (self);
+
+  EGG_ENTRY;
+
+  g_assert (EGG_IS_SUGGESTION_ENTRY (self));
+
+  egg_suggestion_popover_popup (priv->popover);
+
+  EGG_EXIT;
+}
+
+static void
+egg_suggestion_entry_hide_suggestions (EggSuggestionEntry *self)
+{
+  EggSuggestionEntryPrivate *priv = egg_suggestion_entry_get_instance_private (self);
+
+  EGG_ENTRY;
+
+  g_assert (EGG_IS_SUGGESTION_ENTRY (self));
+
+  egg_suggestion_popover_popdown (priv->popover);
+
+  EGG_EXIT;
+}
+
+static void
+egg_suggestion_entry_init_default_css (void)
+{
+  g_autoptr(GtkCssProvider) css_provider = NULL;
+  static gsize initialized;
+
+  if (g_once_init_enter (&initialized))
+    {
+      css_provider = gtk_css_provider_new ();
+      gtk_css_provider_load_from_resource (css_provider,
+                                           "/org/gnome/libegg-private/egg-suggestion-entry.css");
+      gtk_style_context_add_provider_for_screen (gdk_screen_get_default (),
+                                                 GTK_STYLE_PROVIDER (css_provider),
+                                                 GTK_STYLE_PROVIDER_PRIORITY_APPLICATION - 1);
+      g_once_init_leave (&initialized, TRUE);
+    }
+}
+
+static gboolean
+egg_suggestion_entry_focus_out_event (GtkWidget     *widget,
+                                      GdkEventFocus *event)
+{
+  EggSuggestionEntry *self = (EggSuggestionEntry *)widget;
+
+  g_assert (EGG_IS_SUGGESTION_ENTRY (self));
+  g_assert (event != NULL);
+
+  g_signal_emit (self, signals [HIDE_SUGGESTIONS], 0);
+
+  return GTK_WIDGET_CLASS (egg_suggestion_entry_parent_class)->focus_out_event (widget, event);
+}
+
+static void
+egg_suggestion_entry_hierarchy_changed (GtkWidget *widget,
+                                        GtkWidget *old_toplevel)
+{
+  EggSuggestionEntry *self = (EggSuggestionEntry *)widget;
+  EggSuggestionEntryPrivate *priv = egg_suggestion_entry_get_instance_private (self);
+
+  g_assert (EGG_IS_SUGGESTION_ENTRY (self));
+  g_assert (!old_toplevel || GTK_IS_WIDGET (old_toplevel));
+
+  if (priv->popover != NULL)
+    {
+      GtkWidget *toplevel = gtk_widget_get_ancestor (widget, GTK_TYPE_WINDOW);
+
+      gtk_window_set_transient_for (GTK_WINDOW (priv->popover), GTK_WINDOW (toplevel));
+    }
+}
+
+static gboolean
+egg_suggestion_entry_key_press_event (GtkWidget   *widget,
+                                      GdkEventKey *key)
+{
+  EggSuggestionEntry *self = (EggSuggestionEntry *)widget;
+  EggSuggestionEntryPrivate *priv = egg_suggestion_entry_get_instance_private (self);
+
+  g_assert (EGG_IS_SUGGESTION_ENTRY (self));
+
+  /*
+   * If Tab was pressed, and there is uncommitted suggested text,
+   * commit it and stop propagation of the key press.
+   */
+  if (key->keyval == GDK_KEY_Tab && (key->state & GDK_MODIFIER_MASK) == 0)
+    {
+      const gchar *typed_text;
+      EggSuggestion *suggestion;
+
+      typed_text = egg_suggestion_entry_buffer_get_typed_text (priv->buffer);
+      suggestion = egg_suggestion_popover_get_selected (priv->popover);
+
+      if (typed_text != NULL && suggestion != NULL)
+        {
+          g_autofree gchar *replace = egg_suggestion_replace_typed_text (suggestion, typed_text);
+
+          g_signal_handler_block (self, priv->changed_handler);
+
+          if (replace != NULL)
+            gtk_entry_set_text (GTK_ENTRY (self), replace);
+          else
+            egg_suggestion_entry_buffer_commit (priv->buffer);
+          gtk_editable_set_position (GTK_EDITABLE (self), -1);
+
+          g_signal_handler_unblock (self, priv->changed_handler);
+
+          return GDK_EVENT_STOP;
+        }
+    }
+
+  return GTK_WIDGET_CLASS (egg_suggestion_entry_parent_class)->key_press_event (widget, key);
+}
+
+static void
+egg_suggestion_entry_update_attrs (EggSuggestionEntry *self)
+{
+  PangoAttribute *attr;
+  PangoAttrList *list;
+  const gchar *typed_text;
+  const gchar *text;
+  GdkRGBA rgba;
+
+  EGG_ENTRY;
+
+  g_assert (EGG_IS_SUGGESTION_ENTRY (self));
+
+  gdk_rgba_parse (&rgba, "#666666");
+
+  text = gtk_entry_get_text (GTK_ENTRY (self));
+  typed_text = egg_suggestion_entry_get_typed_text (self);
+
+  list = pango_attr_list_new ();
+  attr = pango_attr_foreground_new (rgba.red * 0xFFFF, rgba.green * 0xFFFF, rgba.blue * 0xFFFF);
+  attr->start_index = strlen (typed_text);
+  attr->end_index = strlen (text);
+  pango_attr_list_insert (list, attr);
+  gtk_entry_set_attributes (GTK_ENTRY (self), list);
+  pango_attr_list_unref (list);
+
+  EGG_EXIT;
+}
+
+static void
+egg_suggestion_entry_changed (GtkEditable *editable)
+{
+  EggSuggestionEntry *self = (EggSuggestionEntry *)editable;
+  EggSuggestionEntryPrivate *priv = egg_suggestion_entry_get_instance_private (self);
+  EggSuggestion *suggestion;
+  const gchar *text;
+
+  EGG_ENTRY;
+
+  g_assert (EGG_IS_SUGGESTION_ENTRY (self));
+
+  /*
+   * If we aren't focused, just ignore everything. One such example might be
+   * updating an URI in a webbrowser.
+   */
+  if (!gtk_widget_has_focus (GTK_WIDGET (editable)))
+    return;
+
+  g_signal_handler_block (self, priv->changed_handler);
+
+  text = egg_suggestion_entry_buffer_get_typed_text (priv->buffer);
+
+  if (text == NULL || *text == '\0')
+    {
+      g_signal_emit (self, signals [HIDE_SUGGESTIONS], 0);
+      EGG_GOTO (finish);
+    }
+
+  g_signal_emit (self, signals [SHOW_SUGGESTIONS], 0);
+
+  suggestion = egg_suggestion_popover_get_selected (priv->popover);
+
+  if (suggestion != NULL)
+    {
+      g_object_ref (suggestion);
+      egg_suggestion_entry_buffer_set_suggestion (priv->buffer, suggestion);
+      g_object_unref (suggestion);
+    }
+
+finish:
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TYPED_TEXT]);
+
+  g_signal_handler_unblock (self, priv->changed_handler);
+
+  egg_suggestion_entry_update_attrs (self);
+
+  EGG_EXIT;
+}
+
+static void
+egg_suggestion_entry_suggestion_activated (EggSuggestionEntry   *self,
+                                           EggSuggestion        *suggestion,
+                                           EggSuggestionPopover *popover)
+{
+  EGG_ENTRY;
+
+  g_assert (EGG_IS_SUGGESTION_ENTRY (self));
+  g_assert (EGG_IS_SUGGESTION (suggestion));
+  g_assert (EGG_IS_SUGGESTION_POPOVER (popover));
+
+  g_signal_emit (self, signals [SUGGESTION_ACTIVATED], 0, suggestion);
+  g_signal_emit (self, signals [HIDE_SUGGESTIONS], 0);
+  gtk_entry_set_text (GTK_ENTRY (self), "");
+
+  EGG_EXIT;
+}
+
+static void
+egg_suggestion_entry_move_suggestion (EggSuggestionEntry *self,
+                                      gint                amount)
+{
+  EggSuggestionEntryPrivate *priv = egg_suggestion_entry_get_instance_private (self);
+
+  g_assert (EGG_IS_SUGGESTION_ENTRY (self));
+
+  egg_suggestion_popover_move_by (priv->popover, amount);
+}
+
+static void
+egg_suggestion_entry_activate_suggestion (EggSuggestionEntry *self)
+{
+  EggSuggestionEntryPrivate *priv = egg_suggestion_entry_get_instance_private (self);
+
+  EGG_ENTRY;
+
+  g_assert (EGG_IS_SUGGESTION_ENTRY (self));
+
+  egg_suggestion_popover_activate_selected (priv->popover);
+
+  EGG_EXIT;
+}
+
+static void
+egg_suggestion_entry_constructed (GObject *object)
+{
+  EggSuggestionEntry *self = (EggSuggestionEntry *)object;
+  EggSuggestionEntryPrivate *priv = egg_suggestion_entry_get_instance_private (self);
+
+  G_OBJECT_CLASS (egg_suggestion_entry_parent_class)->constructed (object);
+
+  gtk_entry_set_buffer (GTK_ENTRY (self), GTK_ENTRY_BUFFER (priv->buffer));
+}
+
+static void
+egg_suggestion_entry_destroy (GtkWidget *widget)
+{
+  EggSuggestionEntry *self = (EggSuggestionEntry *)widget;
+  EggSuggestionEntryPrivate *priv = egg_suggestion_entry_get_instance_private (self);
+
+  if (priv->popover != NULL)
+    gtk_widget_destroy (GTK_WIDGET (priv->popover));
+
+  g_clear_object (&priv->model);
+
+  GTK_WIDGET_CLASS (egg_suggestion_entry_parent_class)->destroy (widget);
+
+  g_assert (priv->popover == NULL);
+}
+
+static void
+egg_suggestion_entry_get_property (GObject    *object,
+                                   guint       prop_id,
+                                   GValue     *value,
+                                   GParamSpec *pspec)
+{
+  EggSuggestionEntry *self = EGG_SUGGESTION_ENTRY (object);
+
+  switch (prop_id)
+    {
+    case PROP_MODEL:
+      g_value_set_object (value, egg_suggestion_entry_get_model (self));
+      break;
+
+    case PROP_TYPED_TEXT:
+      g_value_set_string (value, egg_suggestion_entry_get_typed_text (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+egg_suggestion_entry_set_property (GObject      *object,
+                                   guint         prop_id,
+                                   const GValue *value,
+                                   GParamSpec   *pspec)
+{
+  EggSuggestionEntry *self = EGG_SUGGESTION_ENTRY (object);
+
+  switch (prop_id)
+    {
+    case PROP_MODEL:
+      egg_suggestion_entry_set_model (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+egg_suggestion_entry_class_init (EggSuggestionEntryClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  GtkBindingSet *bindings;
+
+  object_class->constructed = egg_suggestion_entry_constructed;
+  object_class->get_property = egg_suggestion_entry_get_property;
+  object_class->set_property = egg_suggestion_entry_set_property;
+
+  widget_class->destroy = egg_suggestion_entry_destroy;
+  widget_class->focus_out_event = egg_suggestion_entry_focus_out_event;
+  widget_class->hierarchy_changed = egg_suggestion_entry_hierarchy_changed;
+  widget_class->key_press_event = egg_suggestion_entry_key_press_event;
+
+  klass->hide_suggestions = egg_suggestion_entry_hide_suggestions;
+  klass->show_suggestions = egg_suggestion_entry_show_suggestions;
+  klass->move_suggestion = egg_suggestion_entry_move_suggestion;
+
+  properties [PROP_MODEL] =
+    g_param_spec_object ("model",
+                         "Model",
+                         "The model to be visualized",
+                         G_TYPE_LIST_MODEL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_TYPED_TEXT] =
+    g_param_spec_string ("typed-text",
+                         "Typed Text",
+                         "Typed text into the entry, does not include suggested text",
+                         NULL,
+                         (G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  signals [HIDE_SUGGESTIONS] =
+    g_signal_new ("hide-suggestions",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                  G_STRUCT_OFFSET (EggSuggestionEntryClass, hide_suggestions),
+                  NULL, NULL, NULL, G_TYPE_NONE, 0);
+
+  /**
+   * EggSuggestionEntry::move-suggestion:
+   * @self: A #EggSuggestionEntry
+   * @amount: The number of items to move
+   *
+   * This moves the selected suggestion in the popover by the value
+   * provided. -1 moves up one row, 1, moves down a row.
+   */
+  signals [MOVE_SUGGESTION] =
+    g_signal_new ("move-suggestion",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                  G_STRUCT_OFFSET (EggSuggestionEntryClass, move_suggestion),
+                  NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_INT);
+
+  signals [SHOW_SUGGESTIONS] =
+    g_signal_new ("show-suggestions",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                  G_STRUCT_OFFSET (EggSuggestionEntryClass, show_suggestions),
+                  NULL, NULL, NULL, G_TYPE_NONE, 0);
+
+  signals [SUGGESTION_ACTIVATED] =
+    g_signal_new ("suggestion-activated",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  G_STRUCT_OFFSET (EggSuggestionEntryClass, suggestion_activated),
+                  NULL, NULL, NULL, G_TYPE_NONE, 1, EGG_TYPE_SUGGESTION);
+
+  widget_class->activate_signal = signals [ACTIVATE_SUGGESTION] =
+    g_signal_new_class_handler ("activate-suggestion",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                                G_CALLBACK (egg_suggestion_entry_activate_suggestion),
+                                NULL, NULL, NULL, G_TYPE_NONE, 0);
+
+  bindings = gtk_binding_set_by_class (klass);
+  gtk_binding_entry_add_signal (bindings, GDK_KEY_Escape, 0, "hide-suggestions", 0);
+  gtk_binding_entry_add_signal (bindings, GDK_KEY_space, GDK_CONTROL_MASK, "show-suggestions", 0);
+  gtk_binding_entry_add_signal (bindings, GDK_KEY_Up, 0, "move-suggestion", 1, G_TYPE_INT, -1);
+  gtk_binding_entry_add_signal (bindings, GDK_KEY_Down, 0, "move-suggestion", 1, G_TYPE_INT, 1);
+  gtk_binding_entry_add_signal (bindings, GDK_KEY_Page_Up, 0, "move-suggestion", 1, G_TYPE_INT, -10);
+  gtk_binding_entry_add_signal (bindings, GDK_KEY_KP_Page_Up, 0, "move-suggestion", 1, G_TYPE_INT, -10);
+  gtk_binding_entry_add_signal (bindings, GDK_KEY_Prior, 0, "move-suggestion", 1, G_TYPE_INT, -10);
+  gtk_binding_entry_add_signal (bindings, GDK_KEY_Next, 0, "move-suggestion", 1, G_TYPE_INT, 10);
+  gtk_binding_entry_add_signal (bindings, GDK_KEY_Page_Down, 0, "move-suggestion", 1, G_TYPE_INT, 10);
+  gtk_binding_entry_add_signal (bindings, GDK_KEY_KP_Page_Down, 0, "move-suggestion", 1, G_TYPE_INT, 10);
+  gtk_binding_entry_add_signal (bindings, GDK_KEY_Return, 0, "activate-suggestion", 0);
+
+  changed_signal_id = g_signal_lookup ("changed", GTK_TYPE_ENTRY);
+}
+
+static void
+egg_suggestion_entry_init (EggSuggestionEntry *self)
+{
+  EggSuggestionEntryPrivate *priv = egg_suggestion_entry_get_instance_private (self);
+
+  egg_suggestion_entry_init_default_css ();
+
+  priv->changed_handler =
+    g_signal_connect_after (self,
+                            "changed",
+                            G_CALLBACK (egg_suggestion_entry_changed),
+                            NULL);
+
+  priv->popover = g_object_new (EGG_TYPE_SUGGESTION_POPOVER,
+                                "destroy-with-parent", TRUE,
+                                "modal", FALSE,
+                                "relative-to", self,
+                                "type", GTK_WINDOW_POPUP,
+                                NULL);
+  g_signal_connect_object (priv->popover,
+                           "suggestion-activated",
+                           G_CALLBACK (egg_suggestion_entry_suggestion_activated),
+                           self,
+                           G_CONNECT_SWAPPED);
+  g_signal_connect (priv->popover,
+                    "destroy",
+                    G_CALLBACK (gtk_widget_destroyed),
+                    &priv->popover);
+  gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (self)),
+                               "suggestion");
+
+  priv->buffer = egg_suggestion_entry_buffer_new ();
+}
+
+GtkWidget *
+egg_suggestion_entry_new (void)
+{
+  return g_object_new (EGG_TYPE_SUGGESTION_ENTRY, NULL);
+}
+
+static GObject *
+egg_suggestion_entry_get_internal_child (GtkBuildable *buildable,
+                                         GtkBuilder   *builder,
+                                         const gchar  *childname)
+{
+  EggSuggestionEntry *self = (EggSuggestionEntry *)buildable;
+  EggSuggestionEntryPrivate *priv = egg_suggestion_entry_get_instance_private (self);
+
+  if (g_strcmp0 (childname, "popover") == 0)
+    return G_OBJECT (priv->popover);
+
+  return NULL;
+}
+
+static void
+buildable_iface_init (GtkBuildableIface *iface)
+{
+  iface->get_internal_child = egg_suggestion_entry_get_internal_child;
+}
+
+static void
+egg_suggestion_entry_set_selection_bounds (GtkEditable *editable,
+                                           gint         start_pos,
+                                           gint         end_pos)
+{
+  EggSuggestionEntry *self = (EggSuggestionEntry *)editable;
+  EggSuggestionEntryPrivate *priv = egg_suggestion_entry_get_instance_private (self);
+
+  g_assert (EGG_IS_SUGGESTION_ENTRY (self));
+
+  g_signal_handler_block (self, priv->changed_handler);
+
+  if (end_pos < 0)
+    end_pos = gtk_entry_buffer_get_length (GTK_ENTRY_BUFFER (priv->buffer));
+
+  if (end_pos > (gint)egg_suggestion_entry_buffer_get_typed_length (priv->buffer))
+    egg_suggestion_entry_buffer_commit (priv->buffer);
+
+  editable_parent_iface->set_selection_bounds (editable, start_pos, end_pos);
+
+  g_signal_handler_unblock (self, priv->changed_handler);
+}
+
+static void
+editable_iface_init (GtkEditableInterface *iface)
+{
+  editable_parent_iface = g_type_interface_peek_parent (iface);
+
+  iface->set_selection_bounds = egg_suggestion_entry_set_selection_bounds;
+}
+
+
+/**
+ * egg_suggestion_entry_get_model:
+ * @self: a #EggSuggestionEntry
+ *
+ * Gets the model being visualized.
+ *
+ * Returns: (nullable) (transfer none): A #GListModel or %NULL.
+ */
+GListModel *
+egg_suggestion_entry_get_model (EggSuggestionEntry *self)
+{
+  EggSuggestionEntryPrivate *priv = egg_suggestion_entry_get_instance_private (self);
+
+  g_return_val_if_fail (EGG_IS_SUGGESTION_ENTRY (self), NULL);
+
+  return priv->model;
+}
+
+void
+egg_suggestion_entry_set_model (EggSuggestionEntry *self,
+                                GListModel         *model)
+{
+  EggSuggestionEntryPrivate *priv = egg_suggestion_entry_get_instance_private (self);
+
+  EGG_ENTRY;
+
+  g_return_if_fail (EGG_IS_SUGGESTION_ENTRY (self));
+  g_return_if_fail (!model || g_type_is_a (g_list_model_get_item_type (model), EGG_TYPE_SUGGESTION));
+
+  if (g_set_object (&priv->model, model))
+    {
+      EGG_TRACE_MSG ("Model has %u items",
+                     model ? g_list_model_get_n_items (model) : 0);
+      egg_suggestion_popover_set_model (priv->popover, model);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MODEL]);
+      egg_suggestion_entry_update_attrs (self);
+    }
+
+  EGG_EXIT;
+}
+
+/**
+ * egg_suggestion_entry_get_suggestion:
+ * @self: a #EggSuggestionEntry
+ *
+ * Gets the currently selected suggestion.
+ *
+ * Returns: (nullable) (transfer none): An #EggSuggestion or %NULL.
+ */
+EggSuggestion *
+egg_suggestion_entry_get_suggestion (EggSuggestionEntry *self)
+{
+  EggSuggestionEntryPrivate *priv = egg_suggestion_entry_get_instance_private (self);
+
+  g_return_val_if_fail (EGG_IS_SUGGESTION_ENTRY (self), NULL);
+
+  return egg_suggestion_popover_get_selected (priv->popover);
+}
+
+void
+egg_suggestion_entry_set_suggestion (EggSuggestionEntry *self,
+                                     EggSuggestion      *suggestion)
+{
+  EggSuggestionEntryPrivate *priv = egg_suggestion_entry_get_instance_private (self);
+
+  EGG_ENTRY;
+
+  g_return_if_fail (EGG_IS_SUGGESTION_ENTRY (self));
+  g_return_if_fail (!suggestion || EGG_IS_SUGGESTION_ENTRY (suggestion));
+
+  egg_suggestion_popover_set_selected (priv->popover, suggestion);
+  egg_suggestion_entry_buffer_set_suggestion (priv->buffer, suggestion);
+
+  EGG_EXIT;
+}
+
+const gchar *
+egg_suggestion_entry_get_typed_text (EggSuggestionEntry *self)
+{
+  EggSuggestionEntryPrivate *priv = egg_suggestion_entry_get_instance_private (self);
+
+  g_return_val_if_fail (EGG_IS_SUGGESTION_ENTRY (self), NULL);
+
+  return egg_suggestion_entry_buffer_get_typed_text (priv->buffer);
+}
diff --git a/lib/egg/egg-suggestion-entry.css b/lib/egg/egg-suggestion-entry.css
new file mode 100644
index 0000000..fe837db
--- /dev/null
+++ b/lib/egg/egg-suggestion-entry.css
@@ -0,0 +1,53 @@
+suggestionpopover {
+  background: transparent;
+}
+
+suggestionpopover > revealer > box {
+  margin: 0px 0px 60px 0px;
+  border-radius: 0 0 7px 7px;
+  border: 1px solid @borders;
+  border-top: none;
+  box-shadow: 0px -10px 30px #000;
+}
+
+suggestionpopover > revealer > box > elastic > scrolledwindow {
+  background: rgba(255,255,255,.95);
+  border-radius: 0 0 7px 7px;
+}
+
+suggestionpopover > revealer > box > elastic > scrolledwindow > viewport > list {
+  background: transparent;
+}
+
+suggestionpopover > revealer > box > elastic > scrolledwindow > viewport > list > row:first-child {
+  transition: none;
+}
+
+suggestionpopover > revealer > box > elastic > scrolledwindow > viewport > list > row {
+  transition: none;
+  border-bottom: 1px solid alpha(@borders, 0.2);
+}
+
+suggestionpopover > revealer > box > elastic > scrolledwindow > viewport > list > row:selected .title {
+  color: #fff;
+}
+
+suggestionpopover > revealer > box > elastic > scrolledwindow > viewport > list > row .title {
+  color: #4a86cf;
+}
+
+suggestionpopover > revealer > box > elastic > scrolledwindow > viewport > list > row:first-child:selected {
+  border-radius: 0;
+}
+
+suggestionpopover > revealer > box > elastic > scrolledwindow > viewport > list > 
row:last-child:selected:not(:first-child) {
+  border-radius: 0 0 7px 7px;
+}
+
+suggestionpopover > revealer > box > elastic > scrolledwindow > viewport > list > row > box {
+  margin: 4px 8px 4px 8px;
+}
+
+suggestionpopover > revealer > box > elastic > scrolledwindow > viewport > list > row:last-child {
+  border-bottom: none;
+}
diff --git a/lib/egg/egg-suggestion-entry.h b/lib/egg/egg-suggestion-entry.h
new file mode 100644
index 0000000..4c0f07b
--- /dev/null
+++ b/lib/egg/egg-suggestion-entry.h
@@ -0,0 +1,64 @@
+/* egg-suggestion-entry.h
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU Lesser General Public License as published by the Free
+ * Software Foundation; either version 2.1 of the License, or (at your option)
+ * any later version.
+ *
+ * This file 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 Lesser 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 EGG_SUGGESTION_ENTRY_H
+#define EGG_SUGGESTION_ENTRY_H
+
+#include <gtk/gtk.h>
+
+#include "egg-suggestion.h"
+
+G_BEGIN_DECLS
+
+#define EGG_TYPE_SUGGESTION_ENTRY (egg_suggestion_entry_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (EggSuggestionEntry, egg_suggestion_entry, EGG, SUGGESTION_ENTRY, GtkEntry)
+
+struct _EggSuggestionEntryClass
+{
+  GtkEntryClass parent_class;
+
+  void (*hide_suggestions)     (EggSuggestionEntry *self);
+  void (*show_suggestions)     (EggSuggestionEntry *self);
+  void (*move_suggestion )     (EggSuggestionEntry *self,
+                                gint                amount);
+  void (*suggestion_activated) (EggSuggestionEntry *self,
+                                EggSuggestion      *suggestion);
+
+  gpointer _reserved1;
+  gpointer _reserved2;
+  gpointer _reserved3;
+  gpointer _reserved4;
+  gpointer _reserved5;
+  gpointer _reserved6;
+  gpointer _reserved7;
+  gpointer _reserved8;
+};
+
+GtkWidget     *egg_suggestion_entry_new            (void);
+void           egg_suggestion_entry_set_model      (EggSuggestionEntry *self,
+                                                    GListModel         *model);
+GListModel    *egg_suggestion_entry_get_model      (EggSuggestionEntry *self);
+const gchar   *egg_suggestion_entry_get_typed_text (EggSuggestionEntry *self);
+EggSuggestion *egg_suggestion_entry_get_suggestion (EggSuggestionEntry *self);
+void           egg_suggestion_entry_set_suggestion (EggSuggestionEntry *self,
+                                                    EggSuggestion      *suggestion);
+
+G_END_DECLS
+
+#endif /* EGG_SUGGESTION_ENTRY_H */
diff --git a/lib/egg/egg-suggestion-popover.c b/lib/egg/egg-suggestion-popover.c
new file mode 100644
index 0000000..17fdbbe
--- /dev/null
+++ b/lib/egg/egg-suggestion-popover.c
@@ -0,0 +1,796 @@
+/* egg-suggestion-popover.c
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU Lesser General Public License as published by the Free
+ * Software Foundation; either version 2.1 of the License, or (at your option)
+ * any later version.
+ *
+ * This file 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 Lesser 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/>.
+ */
+
+#define G_LOG_DOMAIN "egg-suggestion-popover"
+
+#include <glib/gi18n.h>
+
+#include "egg-animation.h"
+#include "egg-elastic-bin.h"
+#include "egg-suggestion.h"
+#include "egg-suggestion-popover.h"
+#include "egg-suggestion-row.h"
+
+struct _EggSuggestionPopover
+{
+  GtkWindow           parent_instance;
+
+  GtkWidget          *relative_to;
+  GtkWindow          *transient_for;
+  GtkRevealer        *revealer;
+  GtkScrolledWindow  *scrolled_window;
+  GtkListBox         *list_box;
+
+  GListModel         *model;
+
+  GType               row_type;
+
+  gulong              delete_event_handler;
+  gulong              configure_event_handler;
+  gulong              size_allocate_handler;
+  gulong              items_changed_handler;
+};
+
+enum {
+  PROP_0,
+  PROP_MODEL,
+  PROP_RELATIVE_TO,
+  PROP_SELECTED,
+  N_PROPS
+};
+
+enum {
+  SUGGESTION_ACTIVATED,
+  N_SIGNALS
+};
+
+G_DEFINE_TYPE (EggSuggestionPopover, egg_suggestion_popover, GTK_TYPE_WINDOW)
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+static void
+egg_suggestion_popover_reposition (EggSuggestionPopover *self)
+{
+  gint width;
+  gint x;
+  gint y;
+
+  g_assert (EGG_IS_SUGGESTION_POPOVER (self));
+
+  if (self->relative_to == NULL ||
+      self->transient_for == NULL ||
+      !gtk_widget_get_mapped (self->relative_to) ||
+      !gtk_widget_get_mapped (GTK_WIDGET (self->transient_for)))
+    return;
+
+  gtk_window_get_size (self->transient_for, &width, NULL);
+  gtk_widget_set_size_request (GTK_WIDGET (self), width, -1);
+  gtk_window_get_position (self->transient_for, &x, &y);
+
+  /*
+   * XXX: This is just a hack for testing so we get the placement right.
+   *
+   *      What we should really do is allow hte EggSuggestionEntry to set our
+   *      relative-to property by wrapping it. That would all the caller to
+   *      place the popover relative to the main content area of the window
+   *      as might be desired for a URL entry or global application search.
+   */
+
+  gtk_window_move (GTK_WINDOW (self), x, y + 47);
+}
+
+/**
+ * egg_suggestion_popover_get_relative_to:
+ * @self: a #EggSuggestionPopover
+ *
+ * Returns: (transfer none) (nullable): A #GtkWidget or %NULL.
+ */
+GtkWidget *
+egg_suggestion_popover_get_relative_to (EggSuggestionPopover *self)
+{
+
+  g_return_val_if_fail (EGG_IS_SUGGESTION_POPOVER (self), NULL);
+
+  return self->relative_to;
+}
+
+void
+egg_suggestion_popover_set_relative_to (EggSuggestionPopover *self,
+                                        GtkWidget            *relative_to)
+{
+  g_return_if_fail (EGG_IS_SUGGESTION_POPOVER (self));
+  g_return_if_fail (!relative_to || GTK_IS_WIDGET (relative_to));
+
+  if (self->relative_to != relative_to)
+    {
+      if (self->relative_to != NULL)
+        {
+          g_signal_handlers_disconnect_by_func (self->relative_to,
+                                                G_CALLBACK (gtk_widget_destroyed),
+                                                &self->relative_to);
+          self->relative_to = NULL;
+        }
+
+      if (relative_to != NULL)
+        {
+          self->relative_to = relative_to;
+          g_signal_connect (self->relative_to,
+                            "destroy",
+                            G_CALLBACK (gtk_widget_destroyed),
+                            &self->relative_to);
+        }
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_RELATIVE_TO]);
+    }
+}
+
+static void
+egg_suggestion_popover_hide (GtkWidget *widget)
+{
+  EggSuggestionPopover *self = (EggSuggestionPopover *)widget;
+
+  g_return_if_fail (EGG_IS_SUGGESTION_POPOVER (self));
+
+  if (self->transient_for != NULL)
+    gtk_window_group_remove_window (gtk_window_get_group (self->transient_for),
+                                    GTK_WINDOW (self));
+
+  g_signal_handler_disconnect (self->transient_for, self->delete_event_handler);
+  g_signal_handler_disconnect (self->transient_for, self->size_allocate_handler);
+  g_signal_handler_disconnect (self->transient_for, self->configure_event_handler);
+
+  self->delete_event_handler = 0;
+  self->size_allocate_handler = 0;
+  self->configure_event_handler = 0;
+
+  self->transient_for = NULL;
+
+  GTK_WIDGET_CLASS (egg_suggestion_popover_parent_class)->hide (widget);
+}
+
+static void
+egg_suggestion_popover_transient_for_size_allocate (EggSuggestionPopover *self,
+                                                    GtkAllocation        *allocation,
+                                                    GtkWindow            *toplevel)
+{
+  g_assert (EGG_IS_SUGGESTION_POPOVER (self));
+  g_assert (allocation != NULL);
+  g_assert (GTK_IS_WINDOW (toplevel));
+
+  egg_suggestion_popover_reposition (self);
+}
+
+static gboolean
+egg_suggestion_popover_transient_for_delete_event (EggSuggestionPopover *self,
+                                                   GdkEvent             *event,
+                                                   GtkWindow            *toplevel)
+{
+  g_assert (EGG_IS_SUGGESTION_POPOVER (self));
+  g_assert (event != NULL);
+  g_assert (GTK_IS_WINDOW (toplevel));
+
+  gtk_widget_hide (GTK_WIDGET (self));
+
+  return FALSE;
+}
+
+static gboolean
+egg_suggestion_popover_transient_for_configure_event (EggSuggestionPopover *self,
+                                                      GdkEvent             *event,
+                                                      GtkWindow            *toplevel)
+{
+  g_assert (EGG_IS_SUGGESTION_POPOVER (self));
+  g_assert (event != NULL);
+  g_assert (GTK_IS_WINDOW (toplevel));
+
+  gtk_widget_hide (GTK_WIDGET (self));
+
+  return FALSE;
+}
+
+static void
+egg_suggestion_popover_show (GtkWidget *widget)
+{
+  EggSuggestionPopover *self = (EggSuggestionPopover *)widget;
+
+  g_return_if_fail (EGG_IS_SUGGESTION_POPOVER (self));
+
+  if (self->relative_to != NULL)
+    {
+      GtkWidget *toplevel;
+
+      toplevel = gtk_widget_get_ancestor (GTK_WIDGET (self->relative_to), GTK_TYPE_WINDOW);
+
+      if (GTK_IS_WINDOW (toplevel))
+        {
+          self->transient_for = GTK_WINDOW (toplevel);
+          gtk_window_group_add_window (gtk_window_get_group (self->transient_for),
+                                       GTK_WINDOW (self));
+          self->delete_event_handler =
+            g_signal_connect_object (toplevel,
+                                     "delete-event",
+                                     G_CALLBACK (egg_suggestion_popover_transient_for_delete_event),
+                                     self,
+                                     G_CONNECT_SWAPPED);
+          self->size_allocate_handler =
+            g_signal_connect_object (toplevel,
+                                     "size-allocate",
+                                     G_CALLBACK (egg_suggestion_popover_transient_for_size_allocate),
+                                     self,
+                                     G_CONNECT_SWAPPED | G_CONNECT_AFTER);
+          self->configure_event_handler =
+            g_signal_connect_object (toplevel,
+                                     "configure-event",
+                                     G_CALLBACK (egg_suggestion_popover_transient_for_configure_event),
+                                     self,
+                                     G_CONNECT_SWAPPED);
+          egg_suggestion_popover_reposition (self);
+        }
+    }
+
+  GTK_WIDGET_CLASS (egg_suggestion_popover_parent_class)->show (widget);
+}
+
+static void
+egg_suggestion_popover_screen_changed (GtkWidget *widget,
+                                       GdkScreen *previous_screen)
+{
+  GdkScreen *screen;
+  GdkVisual *visual;
+
+  GTK_WIDGET_CLASS (egg_suggestion_popover_parent_class)->screen_changed (widget, previous_screen);
+
+  screen = gtk_widget_get_screen (widget);
+  visual = gdk_screen_get_rgba_visual (screen);
+
+  if (visual != NULL)
+    gtk_widget_set_visual (widget, visual);
+}
+
+static void
+egg_suggestion_popover_realize (GtkWidget *widget)
+{
+  GdkScreen *screen;
+  GdkVisual *visual;
+
+  screen = gtk_widget_get_screen (widget);
+  visual = gdk_screen_get_rgba_visual (screen);
+
+  if (visual != NULL)
+    gtk_widget_set_visual (widget, visual);
+
+  GTK_WIDGET_CLASS (egg_suggestion_popover_parent_class)->realize (widget);
+}
+
+static void
+egg_suggestion_popover_notify_child_revealed (EggSuggestionPopover *self,
+                                              GParamSpec           *pspec,
+                                              GtkRevealer          *revealer)
+{
+  g_assert (EGG_IS_SUGGESTION_POPOVER (self));
+  g_assert (GTK_IS_REVEALER (revealer));
+
+  if (!gtk_revealer_get_reveal_child (self->revealer))
+    gtk_widget_hide (GTK_WIDGET (self));
+}
+
+static void
+egg_suggestion_popover_list_box_row_activated (EggSuggestionPopover *self,
+                                               EggSuggestionRow     *row,
+                                               GtkListBox           *list_box)
+{
+  EggSuggestion *suggestion;
+
+  g_assert (EGG_IS_SUGGESTION_POPOVER (self));
+  g_assert (EGG_IS_SUGGESTION_ROW (row));
+  g_assert (GTK_IS_LIST_BOX (list_box));
+
+  suggestion = egg_suggestion_row_get_suggestion (row);
+  g_signal_emit (self, signals [SUGGESTION_ACTIVATED], 0, suggestion);
+}
+
+static void
+egg_suggestion_popover_list_box_row_selected (EggSuggestionPopover *self,
+                                              EggSuggestionRow     *row,
+                                              GtkListBox           *list_box)
+{
+  g_assert (EGG_IS_SUGGESTION_POPOVER (self));
+  g_assert (!row || EGG_IS_SUGGESTION_ROW (row));
+  g_assert (GTK_IS_LIST_BOX (list_box));
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SELECTED]);
+}
+
+static void
+egg_suggestion_popover_destroy (GtkWidget *widget)
+{
+  EggSuggestionPopover *self = (EggSuggestionPopover *)widget;
+
+  if (self->transient_for != NULL)
+    {
+      g_signal_handler_disconnect (self->transient_for, self->size_allocate_handler);
+      g_signal_handler_disconnect (self->transient_for, self->configure_event_handler);
+      g_signal_handler_disconnect (self->transient_for, self->delete_event_handler);
+
+      self->size_allocate_handler = 0;
+      self->configure_event_handler = 0;
+      self->delete_event_handler = 0;
+
+      self->transient_for = NULL;
+    }
+
+  g_clear_object (&self->model);
+
+  egg_suggestion_popover_set_relative_to (self, NULL);
+
+  GTK_WIDGET_CLASS (egg_suggestion_popover_parent_class)->destroy (widget);
+}
+
+static void
+egg_suggestion_popover_get_property (GObject    *object,
+                                     guint       prop_id,
+                                     GValue     *value,
+                                     GParamSpec *pspec)
+{
+  EggSuggestionPopover *self = EGG_SUGGESTION_POPOVER (object);
+
+  switch (prop_id)
+    {
+    case PROP_MODEL:
+      g_value_set_object (value, egg_suggestion_popover_get_model (self));
+      break;
+
+    case PROP_RELATIVE_TO:
+      g_value_set_object (value, egg_suggestion_popover_get_relative_to (self));
+      break;
+
+    case PROP_SELECTED:
+      g_value_set_object (value, egg_suggestion_popover_get_selected (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+egg_suggestion_popover_set_property (GObject      *object,
+                                     guint         prop_id,
+                                     const GValue *value,
+                                     GParamSpec   *pspec)
+{
+  EggSuggestionPopover *self = EGG_SUGGESTION_POPOVER (object);
+
+  switch (prop_id)
+    {
+    case PROP_MODEL:
+      egg_suggestion_popover_set_model (self, g_value_get_object (value));
+      break;
+
+    case PROP_RELATIVE_TO:
+      egg_suggestion_popover_set_relative_to (self, g_value_get_object (value));
+      break;
+
+    case PROP_SELECTED:
+      egg_suggestion_popover_set_selected (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+egg_suggestion_popover_class_init (EggSuggestionPopoverClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->get_property = egg_suggestion_popover_get_property;
+  object_class->set_property = egg_suggestion_popover_set_property;
+
+  widget_class->destroy = egg_suggestion_popover_destroy;
+  widget_class->hide = egg_suggestion_popover_hide;
+  widget_class->screen_changed = egg_suggestion_popover_screen_changed;
+  widget_class->realize = egg_suggestion_popover_realize;
+  widget_class->show = egg_suggestion_popover_show;
+
+  properties [PROP_MODEL] =
+    g_param_spec_object ("model",
+                         "Model",
+                         "The model to be visualized",
+                         EGG_TYPE_SUGGESTION,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_RELATIVE_TO] =
+    g_param_spec_object ("relative-to",
+                         "Relative To",
+                         "The widget to be relative to",
+                         GTK_TYPE_WIDGET,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_SELECTED] =
+    g_param_spec_object ("selected",
+                         "Selected",
+                         "The selected suggestion",
+                         EGG_TYPE_SUGGESTION,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  signals [SUGGESTION_ACTIVATED] =
+    g_signal_new ("suggestion-activated",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0, NULL, NULL, NULL,
+                  G_TYPE_NONE, 1, EGG_TYPE_SUGGESTION);
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/libegg-private/egg-suggestion-popover.ui");
+  gtk_widget_class_bind_template_child (widget_class, EggSuggestionPopover, revealer);
+  gtk_widget_class_bind_template_child (widget_class, EggSuggestionPopover, list_box);
+  gtk_widget_class_bind_template_child (widget_class, EggSuggestionPopover, scrolled_window);
+
+  gtk_widget_class_set_css_name (widget_class, "suggestionpopover");
+
+  g_type_ensure (EGG_TYPE_ELASTIC_BIN);
+}
+
+static void
+egg_suggestion_popover_init (EggSuggestionPopover *self)
+{
+  self->row_type = EGG_TYPE_SUGGESTION_ROW;
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  gtk_window_set_type_hint (GTK_WINDOW (self), GDK_WINDOW_TYPE_HINT_COMBO);
+  gtk_window_set_skip_pager_hint (GTK_WINDOW (self), TRUE);
+  gtk_window_set_skip_taskbar_hint (GTK_WINDOW (self), TRUE);
+  gtk_window_set_decorated (GTK_WINDOW (self), FALSE);
+  gtk_window_set_resizable (GTK_WINDOW (self), FALSE);
+
+  g_signal_connect_object (self->revealer,
+                           "notify::child-revealed",
+                           G_CALLBACK (egg_suggestion_popover_notify_child_revealed),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->list_box,
+                           "row-activated",
+                           G_CALLBACK (egg_suggestion_popover_list_box_row_activated),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->list_box,
+                           "row-selected",
+                           G_CALLBACK (egg_suggestion_popover_list_box_row_selected),
+                           self,
+                           G_CONNECT_SWAPPED);
+}
+
+GtkWidget *
+egg_suggestion_popover_new (void)
+{
+  return g_object_new (EGG_TYPE_SUGGESTION_POPOVER, NULL);
+}
+
+void
+egg_suggestion_popover_popup (EggSuggestionPopover *self)
+{
+  guint duration = 250;
+  guint n_items;
+
+  g_assert (EGG_IS_SUGGESTION_POPOVER (self));
+
+  if (self->model == NULL || 0 == (n_items = g_list_model_get_n_items (self->model) ))
+    return;
+
+  if (self->relative_to != NULL)
+    {
+      GdkDisplay *display;
+      GdkMonitor *monitor;
+      GdkWindow *window;
+      GtkAllocation alloc;
+      gint min_height;
+      gint nat_height;
+
+      display = gtk_widget_get_display (GTK_WIDGET (self->relative_to));
+      window = gtk_widget_get_window (GTK_WIDGET (self->relative_to));
+      monitor = gdk_display_get_monitor_at_window (display, window);
+
+      gtk_widget_get_preferred_height (GTK_WIDGET (self), &min_height, &nat_height);
+      gtk_widget_get_allocation (GTK_WIDGET (self), &alloc);
+
+      duration = egg_animation_calculate_duration (monitor, alloc.height, nat_height);
+    }
+
+  gtk_revealer_set_transition_duration (self->revealer, duration);
+  gtk_widget_show (GTK_WIDGET (self));
+  gtk_revealer_set_reveal_child (self->revealer, TRUE);
+}
+
+void
+egg_suggestion_popover_popdown (EggSuggestionPopover *self)
+{
+  g_assert (EGG_IS_SUGGESTION_POPOVER (self));
+
+  gtk_revealer_set_transition_duration (self->revealer, 100);
+  gtk_revealer_set_reveal_child (self->revealer, FALSE);
+}
+
+static GtkWidget *
+egg_suggestion_popover_create_row (gpointer item,
+                                   gpointer user_data)
+{
+  EggSuggestionPopover *self = user_data;
+  EggSuggestionRow *row;
+  EggSuggestion *suggestion = item;
+
+  g_assert (EGG_IS_SUGGESTION (suggestion));
+  g_assert (EGG_IS_SUGGESTION_POPOVER (self));
+
+  row = g_object_new (self->row_type,
+                      "suggestion", suggestion,
+                      "visible", TRUE,
+                      NULL);
+
+  return GTK_WIDGET (row);
+}
+
+static void
+egg_suggestion_popover_items_changed (EggSuggestionPopover *self,
+                                      guint                 position,
+                                      guint                 removed,
+                                      guint                 added,
+                                      GListModel           *model)
+{
+  g_assert (EGG_IS_SUGGESTION_POPOVER (self));
+  g_assert (G_IS_LIST_MODEL (model));
+
+  if (g_list_model_get_n_items (model) == 0)
+    egg_suggestion_popover_popdown (self);
+}
+
+static void
+egg_suggestion_popover_connect (EggSuggestionPopover *self)
+{
+  g_assert (EGG_IS_SUGGESTION_POPOVER (self));
+
+  if (self->model == NULL)
+    return;
+
+  gtk_list_box_bind_model (self->list_box,
+                           self->model,
+                           egg_suggestion_popover_create_row,
+                           self,
+                           NULL);
+
+  self->items_changed_handler =
+    g_signal_connect_object (self->model,
+                             "items-changed",
+                             G_CALLBACK (egg_suggestion_popover_items_changed),
+                             self,
+                             G_CONNECT_SWAPPED);
+
+  if (g_list_model_get_n_items (self->model) == 0)
+    {
+      egg_suggestion_popover_popdown (self);
+      return;
+    }
+
+  /* select the first row */
+  egg_suggestion_popover_move_by (self, 1);
+}
+
+static void
+egg_suggestion_popover_disconnect (EggSuggestionPopover *self)
+{
+  g_assert (EGG_IS_SUGGESTION_POPOVER (self));
+
+  if (self->model == NULL)
+    return;
+
+  g_signal_handler_disconnect (self->model, self->items_changed_handler);
+  self->items_changed_handler = 0;
+
+  gtk_list_box_bind_model (self->list_box, NULL, NULL, NULL, NULL);
+}
+
+void
+egg_suggestion_popover_set_model (EggSuggestionPopover *self,
+                                  GListModel           *model)
+{
+  g_return_if_fail (EGG_IS_SUGGESTION_POPOVER (self));
+  g_return_if_fail (!model || G_IS_LIST_MODEL (model));
+  g_return_if_fail (!model || g_type_is_a (g_list_model_get_item_type (model), EGG_TYPE_SUGGESTION));
+
+  if (self->model != model)
+    {
+      if (self->model != NULL)
+        {
+          egg_suggestion_popover_disconnect (self);
+          g_clear_object (&self->model);
+        }
+
+      if (model != NULL)
+        {
+          self->model = g_object_ref (model);
+          egg_suggestion_popover_connect (self);
+        }
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MODEL]);
+    }
+}
+
+/**
+ * egg_suggestion_popover_get_model:
+ * @self: a #EggSuggestionPopover
+ *
+ * Gets the model being visualized.
+ *
+ * Returns: (nullable) (transfer none): A #GListModel or %NULL.
+ */
+GListModel *
+egg_suggestion_popover_get_model (EggSuggestionPopover *self)
+{
+  g_return_val_if_fail (EGG_IS_SUGGESTION_POPOVER (self), NULL);
+
+  return self->model;
+}
+
+static void
+find_index_of_row (GtkWidget *widget,
+                   gpointer   user_data)
+{
+  struct {
+    GtkWidget *row;
+    gint       index;
+    gint       counter;
+  } *row_lookup = user_data;
+
+  if (widget == row_lookup->row)
+    row_lookup->index = row_lookup->counter;
+
+  row_lookup->counter++;
+}
+
+void
+egg_suggestion_popover_move_by (EggSuggestionPopover *self,
+                                gint                  amount)
+{
+  GtkListBoxRow *row;
+  struct {
+    GtkWidget *row;
+    gint       index;
+    gint       counter;
+  } row_lookup;
+
+  g_return_if_fail (EGG_IS_SUGGESTION_POPOVER (self));
+
+  if (NULL == (row = gtk_list_box_get_row_at_index (self->list_box, 0)))
+    return;
+
+  if (NULL == gtk_list_box_get_selected_row (self->list_box))
+    {
+      gtk_list_box_select_row (self->list_box, row);
+      return;
+    }
+
+  /*
+   * It would be nice if we have a bit better API to have control over
+   * this from GtkListBox. move-cursor isn't really sufficient for our
+   * control over position without updating the focus.
+   *
+   * We could look at doing focus redirection to the popover first,
+   * but that isn't exactly a clean solution either and suggests that
+   * we subclass the listbox too.
+   *
+   * This is really inefficient, but in general we won't have that
+   * many results, becuase showin the user a ton of results is not
+   * exactly useful.
+   *
+   * If we do decide to reuse this class in something autocompletion
+   * in a text editor, we'll want to restrategize (including avoiding
+   * the creation of unnecessary rows and row reuse).
+   */
+  row = gtk_list_box_get_selected_row (self->list_box);
+
+  row_lookup.row = GTK_WIDGET (row);
+  row_lookup.counter = 0;
+  row_lookup.index = -1;
+
+  gtk_container_foreach (GTK_CONTAINER (self->list_box),
+                         find_index_of_row,
+                         &row_lookup);
+
+  row_lookup.index += amount;
+  row_lookup.index = CLAMP (row_lookup.index, -0, (gint)g_list_model_get_n_items (self->model) - 1);
+
+  row = gtk_list_box_get_row_at_index (self->list_box, row_lookup.index);
+  gtk_list_box_select_row (self->list_box, row);
+}
+
+static void
+find_suggestion_row (GtkWidget *widget,
+                     gpointer   user_data)
+{
+  EggSuggestionRow *row = EGG_SUGGESTION_ROW (widget);
+  EggSuggestion *suggestion = egg_suggestion_row_get_suggestion (row);
+  struct {
+    EggSuggestion  *suggestion;
+    GtkListBoxRow **row;
+  } *lookup = user_data;
+
+  if (suggestion == lookup->suggestion)
+    *lookup->row = GTK_LIST_BOX_ROW (row);
+}
+
+void
+egg_suggestion_popover_set_selected (EggSuggestionPopover *self,
+                                     EggSuggestion        *suggestion)
+{
+  GtkListBoxRow *row = NULL;
+  struct {
+    EggSuggestion  *suggestion;
+    GtkListBoxRow **row;
+  } lookup = { suggestion, &row };
+
+  g_return_if_fail (EGG_IS_SUGGESTION_POPOVER (self));
+  g_return_if_fail (!suggestion || EGG_IS_SUGGESTION (suggestion));
+
+  if (suggestion == NULL)
+    row = gtk_list_box_get_row_at_index (self->list_box, 0);
+  else
+    gtk_container_foreach (GTK_CONTAINER (self->list_box), find_suggestion_row, &lookup);
+
+  if (row != NULL)
+    gtk_list_box_select_row (self->list_box, row);
+}
+
+/**
+ * egg_suggestion_popover_get_selected:
+ * @self: a #EggSuggestionPopover
+ *
+ * Gets the currently selected suggestion.
+ *
+ * Returns: (transfer none) (nullable): An #EggSuggestion or %NULL.
+ */
+EggSuggestion *
+egg_suggestion_popover_get_selected (EggSuggestionPopover *self)
+{
+  EggSuggestionRow *row;
+
+  g_return_val_if_fail (EGG_IS_SUGGESTION_POPOVER (self), NULL);
+
+  row = EGG_SUGGESTION_ROW (gtk_list_box_get_selected_row (self->list_box));
+  if (row != NULL)
+    return egg_suggestion_row_get_suggestion (row);
+
+  return NULL;
+}
+
+void
+egg_suggestion_popover_activate_selected (EggSuggestionPopover *self)
+{
+  EggSuggestion *suggestion;
+
+  g_return_if_fail (EGG_IS_SUGGESTION_POPOVER (self));
+
+  if (NULL != (suggestion = egg_suggestion_popover_get_selected (self)))
+    g_signal_emit (self, signals [SUGGESTION_ACTIVATED], 0, suggestion);
+}
diff --git a/lib/egg/egg-suggestion-popover.h b/lib/egg/egg-suggestion-popover.h
new file mode 100644
index 0000000..bdfba5f
--- /dev/null
+++ b/lib/egg/egg-suggestion-popover.h
@@ -0,0 +1,49 @@
+/* egg-suggestion-popover.h
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU Lesser General Public License as published by the Free
+ * Software Foundation; either version 2.1 of the License, or (at your option)
+ * any later version.
+ *
+ * This file 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 Lesser 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 EGG_SUGGESTION_POPOVER_H
+#define EGG_SUGGESTION_POPOVER_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define EGG_TYPE_SUGGESTION_POPOVER (egg_suggestion_popover_get_type())
+
+G_DECLARE_FINAL_TYPE (EggSuggestionPopover, egg_suggestion_popover, EGG, SUGGESTION_POPOVER, GtkWindow)
+
+GtkWidget     *egg_suggestion_popover_new               (void);
+GtkWidget     *egg_suggestion_popover_get_relative_to   (EggSuggestionPopover *self);
+void           egg_suggestion_popover_set_relative_to   (EggSuggestionPopover *self,
+                                                         GtkWidget            *widget);
+void           egg_suggestion_popover_popup             (EggSuggestionPopover *self);
+void           egg_suggestion_popover_popdown           (EggSuggestionPopover *self);
+GListModel    *egg_suggestion_popover_get_model         (EggSuggestionPopover *self);
+void           egg_suggestion_popover_set_model         (EggSuggestionPopover *self,
+                                                         GListModel           *model);
+void           egg_suggestion_popover_move_by           (EggSuggestionPopover *self,
+                                                         gint                  amount);
+EggSuggestion *egg_suggestion_popover_get_selected      (EggSuggestionPopover *self);
+void           egg_suggestion_popover_set_selected      (EggSuggestionPopover *self,
+                                                         EggSuggestion        *suggestion);
+void           egg_suggestion_popover_activate_selected (EggSuggestionPopover *self);
+
+G_END_DECLS
+
+#endif /* EGG_SUGGESTION_POPOVER_H */
diff --git a/lib/egg/egg-suggestion-popover.ui b/lib/egg/egg-suggestion-popover.ui
new file mode 100644
index 0000000..265e326
--- /dev/null
+++ b/lib/egg/egg-suggestion-popover.ui
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="EggSuggestionPopover" parent="GtkWindow">
+    <child>
+      <object class="GtkRevealer" id="revealer">
+        <property name="reveal-child">false</property>
+        <property name="transition-duration">200</property>
+        <property name="transition-type">slide-down</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkBox">
+            <property name="orientation">vertical</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="EggElasticBin" id="elastic">
+                <property name="visible">true</property>
+                <child>
+                  <object class="GtkScrolledWindow" id="scrolled_window">
+                    <!-- XXX
+                         We should generate max-content-height based on window position
+                         or some other hueristic. (transient-for maybe?)
+                    -->
+                    <property name="max-content-height">500</property>
+                    <property name="hscrollbar-policy">never</property>
+                    <property name="propagate-natural-height">true</property>
+                    <property name="visible">true</property>
+                    <child>
+                      <object class="GtkListBox" id="list_box">
+                        <property name="selection-mode">browse</property>
+                        <property name="visible">true</property>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/lib/egg/egg-suggestion-row.c b/lib/egg/egg-suggestion-row.c
new file mode 100644
index 0000000..604678a
--- /dev/null
+++ b/lib/egg/egg-suggestion-row.c
@@ -0,0 +1,209 @@
+/* egg-suggestion-row.c
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU Lesser General Public License as published by the Free
+ * Software Foundation; either version 2.1 of the License, or (at your option)
+ * any later version.
+ *
+ * This file 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 Lesser 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/>.
+ */
+
+#define G_LOG_DOMAIN "egg-suggestion-row"
+
+#include "egg-suggestion-row.h"
+
+typedef struct
+{
+  EggSuggestion *suggestion;
+
+  GtkImage      *image;
+  GtkLabel      *title;
+  GtkLabel      *separator;
+  GtkLabel      *subtitle;
+} EggSuggestionRowPrivate;
+
+enum {
+  PROP_0,
+  PROP_SUGGESTION,
+  N_PROPS
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (EggSuggestionRow, egg_suggestion_row, GTK_TYPE_LIST_BOX_ROW)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+egg_suggestion_row_disconnect (EggSuggestionRow *self)
+{
+  EggSuggestionRowPrivate *priv = egg_suggestion_row_get_instance_private (self);
+
+  g_return_if_fail (EGG_IS_SUGGESTION_ROW (self));
+
+  if (priv->suggestion == NULL)
+    return;
+
+  g_object_set (priv->image, "icon-name", NULL, NULL);
+  gtk_label_set_label (priv->title, NULL);
+  gtk_label_set_label (priv->subtitle, NULL);
+}
+
+static void
+egg_suggestion_row_connect (EggSuggestionRow *self)
+{
+  EggSuggestionRowPrivate *priv = egg_suggestion_row_get_instance_private (self);
+  const gchar *icon_name;
+  const gchar *subtitle;
+
+  g_return_if_fail (EGG_IS_SUGGESTION_ROW (self));
+  g_return_if_fail (priv->suggestion != NULL);
+
+  icon_name = egg_suggestion_get_icon_name (priv->suggestion);
+  if (icon_name == NULL)
+    icon_name = "web-browser-symbolic";
+
+  g_object_set (priv->image, "icon-name", icon_name, NULL);
+  gtk_label_set_label (priv->title, egg_suggestion_get_title (priv->suggestion));
+
+  subtitle = egg_suggestion_get_subtitle (priv->suggestion);
+  gtk_label_set_label (priv->subtitle, subtitle);
+  gtk_widget_set_visible (GTK_WIDGET (priv->separator), !!subtitle);
+}
+
+static void
+egg_suggestion_row_finalize (GObject *object)
+{
+  EggSuggestionRow *self = (EggSuggestionRow *)object;
+  EggSuggestionRowPrivate *priv = egg_suggestion_row_get_instance_private (self);
+
+  g_clear_object (&priv->suggestion);
+
+  G_OBJECT_CLASS (egg_suggestion_row_parent_class)->finalize (object);
+}
+
+static void
+egg_suggestion_row_get_property (GObject    *object,
+                                 guint       prop_id,
+                                 GValue     *value,
+                                 GParamSpec *pspec)
+{
+  EggSuggestionRow *self = EGG_SUGGESTION_ROW (object);
+
+  switch (prop_id)
+    {
+    case PROP_SUGGESTION:
+      g_value_set_object (value, egg_suggestion_row_get_suggestion (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+egg_suggestion_row_set_property (GObject      *object,
+                                 guint         prop_id,
+                                 const GValue *value,
+                                 GParamSpec   *pspec)
+{
+  EggSuggestionRow *self = EGG_SUGGESTION_ROW (object);
+
+  switch (prop_id)
+    {
+    case PROP_SUGGESTION:
+      egg_suggestion_row_set_suggestion (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+egg_suggestion_row_class_init (EggSuggestionRowClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = egg_suggestion_row_finalize;
+  object_class->get_property = egg_suggestion_row_get_property;
+  object_class->set_property = egg_suggestion_row_set_property;
+
+  properties [PROP_SUGGESTION] =
+    g_param_spec_object ("suggestion",
+                         "Suggestion",
+                         "The suggestion to display",
+                         EGG_TYPE_SUGGESTION,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/libegg-private/egg-suggestion-row.ui");
+  gtk_widget_class_bind_template_child_private (widget_class, EggSuggestionRow, image);
+  gtk_widget_class_bind_template_child_private (widget_class, EggSuggestionRow, title);
+  gtk_widget_class_bind_template_child_private (widget_class, EggSuggestionRow, subtitle);
+  gtk_widget_class_bind_template_child_private (widget_class, EggSuggestionRow, separator);
+}
+
+static void
+egg_suggestion_row_init (EggSuggestionRow *self)
+{
+  GtkStyleContext *context;
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  context = gtk_widget_get_style_context (GTK_WIDGET (self));
+  gtk_style_context_add_class (context, "suggestion");
+}
+
+/**
+ * egg_suggestion_row_get_suggestion:
+ * @self: a #EggSuggestionRow
+ *
+ * Gets the suggestion to be displayed.
+ *
+ * Returns: (transfer none): An #EggSuggestion
+ */
+EggSuggestion *
+egg_suggestion_row_get_suggestion (EggSuggestionRow *self)
+{
+  EggSuggestionRowPrivate *priv = egg_suggestion_row_get_instance_private (self);
+
+  g_return_val_if_fail (EGG_IS_SUGGESTION_ROW (self), NULL);
+
+  return priv->suggestion;
+}
+
+void
+egg_suggestion_row_set_suggestion (EggSuggestionRow *self,
+                                   EggSuggestion    *suggestion)
+{
+  EggSuggestionRowPrivate *priv = egg_suggestion_row_get_instance_private (self);
+
+  g_return_if_fail (EGG_IS_SUGGESTION_ROW (self));
+  g_return_if_fail (!suggestion || EGG_IS_SUGGESTION (suggestion));
+
+  if (priv->suggestion != suggestion)
+    {
+      if (priv->suggestion != NULL)
+        {
+          egg_suggestion_row_disconnect (self);
+          g_clear_object (&priv->suggestion);
+        }
+
+      if (suggestion != NULL)
+        {
+          priv->suggestion = g_object_ref (suggestion);
+          egg_suggestion_row_connect (self);
+        }
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SUGGESTION]);
+    }
+}
diff --git a/lib/egg/egg-suggestion-row.h b/lib/egg/egg-suggestion-row.h
new file mode 100644
index 0000000..87a6925
--- /dev/null
+++ b/lib/egg/egg-suggestion-row.h
@@ -0,0 +1,49 @@
+/* egg-suggestion-row.h
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU Lesser General Public License as published by the Free
+ * Software Foundation; either version 2.1 of the License, or (at your option)
+ * any later version.
+ *
+ * This file 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 Lesser 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 EGG_SUGGESTION_ROW_H
+#define EGG_SUGGESTION_ROW_H
+
+#include <gtk/gtk.h>
+
+#include "egg-suggestion.h"
+
+G_BEGIN_DECLS
+
+#define EGG_TYPE_SUGGESTION_ROW (egg_suggestion_row_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (EggSuggestionRow, egg_suggestion_row, EGG, SUGGESTION_ROW, GtkListBoxRow)
+
+struct _EggSuggestionRowClass
+{
+  GtkListBoxRowClass parent_class;
+
+  gpointer _reserved1;
+  gpointer _reserved2;
+  gpointer _reserved3;
+  gpointer _reserved4;
+};
+
+GtkWidget     *egg_suggestion_row_new            (void);
+EggSuggestion *egg_suggestion_row_get_suggestion (EggSuggestionRow *self);
+void           egg_suggestion_row_set_suggestion (EggSuggestionRow *self,
+                                                  EggSuggestion    *suggestion);
+
+G_END_DECLS
+
+#endif /* EGG_SUGGESTION_ROW_H */
diff --git a/lib/egg/egg-suggestion-row.ui b/lib/egg/egg-suggestion-row.ui
new file mode 100644
index 0000000..bf79bc4
--- /dev/null
+++ b/lib/egg/egg-suggestion-row.ui
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="EggSuggestionRow" parent="GtkListBoxRow">
+    <child>
+      <object class="GtkBox">
+        <property name="orientation">horizontal</property>
+        <property name="spacing">12</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkImage" id="image">
+            <property name="hexpand">false</property>
+            <property name="visible">true</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkLabel" id="title">
+            <property name="hexpand">false</property>
+            <property name="visible">true</property>
+            <property name="xalign">0.0</property>
+            <property name="use-markup">true</property>
+            <property name="ellipsize">end</property>
+            <style>
+              <class name="title"/>
+            </style>
+          </object>
+        </child>
+        <child>
+          <object class="GtkLabel" id="separator">
+            <property name="hexpand">false</property>
+            <property name="label">—</property>
+            <property name="visible">true</property>
+            <style>
+              <class name="dim-label"/>
+            </style>
+          </object>
+        </child>
+        <child>
+          <object class="GtkLabel" id="subtitle">
+            <property name="hexpand">true</property>
+            <property name="visible">true</property>
+            <property name="xalign">0.0</property>
+            <property name="use-markup">true</property>
+            <property name="ellipsize">end</property>
+            <style>
+              <class name="dim-label"/>
+              <class name="subtitle"/>
+            </style>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/lib/egg/egg-suggestion.c b/lib/egg/egg-suggestion.c
new file mode 100644
index 0000000..d28d059
--- /dev/null
+++ b/lib/egg/egg-suggestion.c
@@ -0,0 +1,351 @@
+/* egg-suggestion.c
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU Lesser General Public License as published by the Free
+ * Software Foundation; either version 2.1 of the License, or (at your option)
+ * any later version.
+ *
+ * This file 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 Lesser 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/>.
+ */
+
+#define G_LOG_DOMAIN "egg-suggestion"
+
+#include "egg-suggestion.h"
+
+typedef struct
+{
+  gchar *title;
+  gchar *subtitle;
+  gchar *icon_name;
+  gchar *id;
+} EggSuggestionPrivate;
+
+enum {
+  PROP_0,
+  PROP_ICON_NAME,
+  PROP_ID,
+  PROP_SUBTITLE,
+  PROP_TITLE,
+  N_PROPS
+};
+
+enum {
+  REPLACE_TYPED_TEXT,
+  SUGGEST_SUFFIX,
+  N_SIGNALS
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (EggSuggestion, egg_suggestion, G_TYPE_OBJECT)
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+static void
+egg_suggestion_finalize (GObject *object)
+{
+  EggSuggestion *self = (EggSuggestion *)object;
+  EggSuggestionPrivate *priv = egg_suggestion_get_instance_private (self);
+
+  g_clear_pointer (&priv->title, g_free);
+  g_clear_pointer (&priv->subtitle, g_free);
+  g_clear_pointer (&priv->icon_name, g_free);
+  g_clear_pointer (&priv->id, g_free);
+
+  G_OBJECT_CLASS (egg_suggestion_parent_class)->finalize (object);
+}
+
+static void
+egg_suggestion_get_property (GObject    *object,
+                             guint       prop_id,
+                             GValue     *value,
+                             GParamSpec *pspec)
+{
+  EggSuggestion *self = EGG_SUGGESTION (object);
+
+  switch (prop_id)
+    {
+    case PROP_ID:
+      g_value_set_string (value, egg_suggestion_get_id (self));
+      break;
+
+    case PROP_ICON_NAME:
+      g_value_set_string (value, egg_suggestion_get_icon_name (self));
+      break;
+
+    case PROP_TITLE:
+      g_value_set_string (value, egg_suggestion_get_title (self));
+      break;
+
+    case PROP_SUBTITLE:
+      g_value_set_string (value, egg_suggestion_get_subtitle (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+egg_suggestion_set_property (GObject      *object,
+                             guint         prop_id,
+                             const GValue *value,
+                             GParamSpec   *pspec)
+{
+  EggSuggestion *self = EGG_SUGGESTION (object);
+
+  switch (prop_id)
+    {
+    case PROP_ICON_NAME:
+      egg_suggestion_set_icon_name (self, g_value_get_string (value));
+      break;
+
+    case PROP_ID:
+      egg_suggestion_set_id (self, g_value_get_string (value));
+      break;
+
+    case PROP_TITLE:
+      egg_suggestion_set_title (self, g_value_get_string (value));
+      break;
+
+    case PROP_SUBTITLE:
+      egg_suggestion_set_subtitle (self, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+egg_suggestion_class_init (EggSuggestionClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = egg_suggestion_finalize;
+  object_class->get_property = egg_suggestion_get_property;
+  object_class->set_property = egg_suggestion_set_property;
+
+  properties [PROP_ID] =
+    g_param_spec_string ("id",
+                         "Id",
+                         "The suggestion identifier",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_ICON_NAME] =
+    g_param_spec_string ("icon-name",
+                         "Icon Name",
+                         "The name of the icon to display",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_TITLE] =
+    g_param_spec_string ("title",
+                         "Title",
+                         "The title of the suggestion",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_SUBTITLE] =
+    g_param_spec_string ("subtitle",
+                         "Subtitle",
+                         "The subtitle of the suggestion",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  signals [REPLACE_TYPED_TEXT] =
+    g_signal_new ("replace-typed-text",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  G_STRUCT_OFFSET (EggSuggestionClass, replace_typed_text),
+                  g_signal_accumulator_first_wins, NULL, NULL,
+                  G_TYPE_STRING, 1, G_TYPE_STRING | G_SIGNAL_TYPE_STATIC_SCOPE);
+
+  signals [SUGGEST_SUFFIX] =
+    g_signal_new ("suggest-suffix",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  G_STRUCT_OFFSET (EggSuggestionClass, suggest_suffix),
+                  g_signal_accumulator_first_wins, NULL, NULL,
+                  G_TYPE_STRING, 1, G_TYPE_STRING | G_SIGNAL_TYPE_STATIC_SCOPE);
+}
+
+static void
+egg_suggestion_init (EggSuggestion *self)
+{
+}
+
+const gchar *
+egg_suggestion_get_id (EggSuggestion *self)
+{
+  EggSuggestionPrivate *priv = egg_suggestion_get_instance_private (self);
+
+  g_return_val_if_fail (EGG_IS_SUGGESTION (self), NULL);
+
+  return priv->id;
+}
+
+const gchar *
+egg_suggestion_get_icon_name (EggSuggestion *self)
+{
+  EggSuggestionPrivate *priv = egg_suggestion_get_instance_private (self);
+
+  g_return_val_if_fail (EGG_IS_SUGGESTION (self), NULL);
+
+  return priv->icon_name;
+}
+
+const gchar *
+egg_suggestion_get_title (EggSuggestion *self)
+{
+  EggSuggestionPrivate *priv = egg_suggestion_get_instance_private (self);
+
+  g_return_val_if_fail (EGG_IS_SUGGESTION (self), NULL);
+
+  return priv->title;
+}
+
+const gchar *
+egg_suggestion_get_subtitle (EggSuggestion *self)
+{
+  EggSuggestionPrivate *priv = egg_suggestion_get_instance_private (self);
+
+  g_return_val_if_fail (EGG_IS_SUGGESTION (self), NULL);
+
+  return priv->subtitle;
+}
+
+void
+egg_suggestion_set_icon_name (EggSuggestion *self,
+                              const gchar   *icon_name)
+{
+  EggSuggestionPrivate *priv = egg_suggestion_get_instance_private (self);
+
+  g_return_if_fail (EGG_IS_SUGGESTION (self));
+
+  if (g_strcmp0 (priv->icon_name, icon_name) != 0)
+    {
+      g_free (priv->icon_name);
+      priv->icon_name = g_strdup (icon_name);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ICON_NAME]);
+    }
+}
+
+void
+egg_suggestion_set_id (EggSuggestion *self,
+                       const gchar   *id)
+{
+  EggSuggestionPrivate *priv = egg_suggestion_get_instance_private (self);
+
+  g_return_if_fail (EGG_IS_SUGGESTION (self));
+
+  if (g_strcmp0 (priv->id, id) != 0)
+    {
+      g_free (priv->id);
+      priv->id = g_strdup (id);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ID]);
+    }
+}
+
+void
+egg_suggestion_set_title (EggSuggestion *self,
+                          const gchar   *title)
+{
+  EggSuggestionPrivate *priv = egg_suggestion_get_instance_private (self);
+
+  g_return_if_fail (EGG_IS_SUGGESTION (self));
+
+  if (g_strcmp0 (priv->title, title) != 0)
+    {
+      g_free (priv->title);
+      priv->title = g_strdup (title);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TITLE]);
+    }
+}
+
+void
+egg_suggestion_set_subtitle (EggSuggestion *self,
+                             const gchar   *subtitle)
+{
+  EggSuggestionPrivate *priv = egg_suggestion_get_instance_private (self);
+
+  g_return_if_fail (EGG_IS_SUGGESTION (self));
+
+  if (g_strcmp0 (priv->subtitle, subtitle) != 0)
+    {
+      g_free (priv->subtitle);
+      priv->subtitle = g_strdup (subtitle);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SUBTITLE]);
+    }
+}
+
+/**
+ * egg_suggestion_suggest_suffix:
+ * @self: a #EggSuggestion
+ * @typed_text: The user entered text
+ *
+ * This function requests potential text to append to @typed_text to make it
+ * more clear to the user what they will be activating by selecting this
+ * suggestion. For example, if they start typing "gno", a potential suggested
+ * suffix might be "me.org" to create "gnome.org".
+ *
+ * Returns: (transfer full) (nullable): Suffix to append to @typed_text
+ *   or %NULL to leave it unchanged.
+ */
+gchar *
+egg_suggestion_suggest_suffix (EggSuggestion *self,
+                               const gchar   *typed_text)
+{
+  gchar *ret = NULL;
+
+  g_return_val_if_fail (EGG_IS_SUGGESTION (self), NULL);
+  g_return_val_if_fail (typed_text != NULL, NULL);
+
+  g_signal_emit (self, signals [SUGGEST_SUFFIX], 0, typed_text, &ret);
+
+  return ret;
+}
+
+EggSuggestion *
+egg_suggestion_new (void)
+{
+  return g_object_new (EGG_TYPE_SUGGESTION, NULL);
+}
+
+/**
+ * egg_suggestion_replace_typed_text:
+ * @self: An #EggSuggestion
+ * @typed_text: the text that was typed into the entry
+ *
+ * This function is meant to be used to replace the text in the entry with text
+ * that represents the suggestion most accurately. This happens when the user
+ * presses tab while typing a suggestion. For example, if typing "gno" in the
+ * entry, you might have a suggest_suffix of "me.org" so that the user sees
+ * "gnome.org". But the replace_typed_text might include more data such as
+ * "https://gnome.org"; as it more closely represents the suggestion.
+ *
+ * Returns: (transfer full) (nullable): The replacement text to insert into
+ *   the entry when "tab" is pressed to complete the insertion.
+ */
+gchar *
+egg_suggestion_replace_typed_text (EggSuggestion *self,
+                                   const gchar   *typed_text)
+{
+  gchar *ret = NULL;
+
+  g_return_val_if_fail (EGG_IS_SUGGESTION (self), NULL);
+
+  g_signal_emit (self, signals [REPLACE_TYPED_TEXT], 0, typed_text, &ret);
+
+  return ret;
+}
diff --git a/lib/egg/egg-suggestion.h b/lib/egg/egg-suggestion.h
new file mode 100644
index 0000000..05a70ce
--- /dev/null
+++ b/lib/egg/egg-suggestion.h
@@ -0,0 +1,66 @@
+/* egg-suggestion.h
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU Lesser General Public License as published by the Free
+ * Software Foundation; either version 2.1 of the License, or (at your option)
+ * any later version.
+ *
+ * This file 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 Lesser 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 EGG_SUGGESTION_H
+#define EGG_SUGGESTION_H
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define EGG_TYPE_SUGGESTION (egg_suggestion_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (EggSuggestion, egg_suggestion, EGG, SUGGESTION, GObject)
+
+struct _EggSuggestionClass
+{
+  GObjectClass parent_class;
+
+  gchar *(*suggest_suffix)     (EggSuggestion *self,
+                                const gchar   *typed_text);
+  gchar *(*replace_typed_text) (EggSuggestion *self,
+                                const gchar   *typed_text);
+
+  gpointer _reserved1;
+  gpointer _reserved2;
+  gpointer _reserved3;
+  gpointer _reserved4;
+};
+
+EggSuggestion *egg_suggestion_new                (void);
+const gchar   *egg_suggestion_get_id             (EggSuggestion *self);
+void           egg_suggestion_set_id             (EggSuggestion *self,
+                                                  const gchar   *id);
+const gchar   *egg_suggestion_get_icon_name      (EggSuggestion *self);
+void           egg_suggestion_set_icon_name      (EggSuggestion *self,
+                                                  const gchar   *icon_name);
+const gchar   *egg_suggestion_get_title          (EggSuggestion *self);
+void           egg_suggestion_set_title          (EggSuggestion *self,
+                                                  const gchar   *title);
+const gchar   *egg_suggestion_get_subtitle       (EggSuggestion *self);
+void           egg_suggestion_set_subtitle       (EggSuggestion *self,
+                                                  const gchar   *subtitle);
+gchar         *egg_suggestion_suggest_suffix     (EggSuggestion *self,
+                                                  const gchar   *typed_text);
+gchar         *egg_suggestion_replace_typed_text (EggSuggestion *self,
+                                                  const gchar   *replace_typed_text);
+
+G_END_DECLS
+
+#endif /* EGG_SUGGESTION_H */
diff --git a/lib/egg/egg.gresources.xml b/lib/egg/egg.gresources.xml
new file mode 100644
index 0000000..a4030bd
--- /dev/null
+++ b/lib/egg/egg.gresources.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/org/gnome/libegg-private">
+    <file>egg-suggestion-entry.css</file>
+    <file>egg-suggestion-popover.ui</file>
+    <file>egg-suggestion-row.ui</file>
+  </gresource>
+</gresources>
diff --git a/lib/egg/eggtreemultidnd.c b/lib/egg/eggtreemultidnd.c
index a238abd..03e3dee 100644
--- a/lib/egg/eggtreemultidnd.c
+++ b/lib/egg/eggtreemultidnd.c
@@ -16,6 +16,8 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
+#define G_LOG_DOMAIN "Egg"
+
 #include <config.h>
 
 #include <string.h>
diff --git a/lib/widgets/ephy-location-entry.c b/lib/widgets/ephy-location-entry.c
index 72b7e12..467dd54 100644
--- a/lib/widgets/ephy-location-entry.c
+++ b/lib/widgets/ephy-location-entry.c
@@ -54,7 +54,7 @@
  */
 
 struct _EphyLocationEntry {
-  GtkEntry parent_instance;
+  EggSuggestionEntry parent_instance;
 
   GtkTreeModel *model;
 
@@ -115,7 +115,7 @@ static gint signals[LAST_SIGNAL] = { 0 };
 
 static void ephy_location_entry_title_widget_interface_init (EphyTitleWidgetInterface *iface);
 
-G_DEFINE_TYPE_WITH_CODE (EphyLocationEntry, ephy_location_entry, GTK_TYPE_ENTRY,
+G_DEFINE_TYPE_WITH_CODE (EphyLocationEntry, ephy_location_entry, EGG_TYPE_SUGGESTION_ENTRY,
                          G_IMPLEMENT_INTERFACE (EPHY_TYPE_TITLE_WIDGET,
                                                 ephy_location_entry_title_widget_interface_init))
 
diff --git a/lib/widgets/ephy-location-entry.h b/lib/widgets/ephy-location-entry.h
index 490524a..97e4e47 100644
--- a/lib/widgets/ephy-location-entry.h
+++ b/lib/widgets/ephy-location-entry.h
@@ -25,13 +25,15 @@
 
 #include <gtk/gtk.h>
 
+#include "egg/egg-suggestion-entry.h"
+
 #include "ephy-security-levels.h"
 
 G_BEGIN_DECLS
 
 #define EPHY_TYPE_LOCATION_ENTRY (ephy_location_entry_get_type())
 
-G_DECLARE_FINAL_TYPE (EphyLocationEntry, ephy_location_entry, EPHY, LOCATION_ENTRY, GtkEntry)
+G_DECLARE_FINAL_TYPE (EphyLocationEntry, ephy_location_entry, EPHY, LOCATION_ENTRY, EggSuggestionEntry)
 
 typedef enum {
   EPHY_LOCATION_ENTRY_BOOKMARK_ICON_HIDDEN,
diff --git a/src/Makefile.am b/src/Makefile.am
index aab514a..a2d8aec 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -62,6 +62,8 @@ libephymain_la_SOURCES = \
        ephy-session.h                          \
        ephy-shell.c                            \
        ephy-shell.h                            \
+       ephy-suggestion-model.c                 \
+       ephy-suggestion-model.h                 \
        ephy-window.c                           \
        ephy-window.h                           \
        languages.h                             \
diff --git a/src/ephy-location-controller.c b/src/ephy-location-controller.c
index 37fd6e7..89ad807 100644
--- a/src/ephy-location-controller.c
+++ b/src/ephy-location-controller.c
@@ -23,7 +23,6 @@
 #include "ephy-location-controller.h"
 
 
-#include "ephy-completion-model.h"
 #include "ephy-debug.h"
 #include "ephy-dnd.h"
 #include "ephy-embed-container.h"
@@ -31,6 +30,7 @@
 #include "ephy-link.h"
 #include "ephy-location-entry.h"
 #include "ephy-shell.h"
+#include "ephy-suggestion-model.h"
 #include "ephy-title-widget.h"
 #include "ephy-widgets-type-builtins.h"
 
@@ -75,18 +75,7 @@ enum {
 static GParamSpec *obj_properties[LAST_PROP];
 
 G_DEFINE_TYPE_WITH_CODE (EphyLocationController, ephy_location_controller, G_TYPE_OBJECT,
-                         G_IMPLEMENT_INTERFACE (EPHY_TYPE_LINK,
-                                                NULL))
-
-static gboolean
-match_func (GtkEntryCompletion *completion,
-            const char         *key,
-            GtkTreeIter        *iter,
-            gpointer            data)
-{
-  /* We want every row in the model to show up. */
-  return TRUE;
-}
+                         G_IMPLEMENT_INTERFACE (EPHY_TYPE_LINK, NULL))
 
 static void
 entry_drag_data_received_cb (GtkWidget *widget,
@@ -181,33 +170,18 @@ entry_activate_cb (GtkEntry               *entry,
 }
 
 static void
-update_done_cb (EphyHistoryService *service,
-                gboolean            success,
-                gpointer            result_data,
-                gpointer            user_data)
-{
-  /* FIXME: this hack is needed for the completion entry popup
-   * to resize smoothly. See:
-   * https://bugzilla.gnome.org/show_bug.cgi?id=671074 */
-  gtk_entry_completion_complete (GTK_ENTRY_COMPLETION (user_data));
-}
-
-static void
 user_changed_cb (GtkWidget *widget, EphyLocationController *controller)
 {
   const char *address;
-  GtkTreeModel *model;
-  GtkEntryCompletion *completion;
+  GListModel *model;
 
   address = ephy_title_widget_get_address (EPHY_TITLE_WIDGET (widget));
 
   LOG ("user_changed_cb, address %s", address);
 
-  completion = gtk_entry_get_completion (GTK_ENTRY (widget));
-  model = gtk_entry_completion_get_model (completion);
+  model = egg_suggestion_entry_get_model (EGG_SUGGESTION_ENTRY (widget));
 
-  ephy_completion_model_update_for_string (EPHY_COMPLETION_MODEL (model), address,
-                                           update_done_cb, completion);
+  ephy_suggestion_model_query_async (EPHY_SUGGESTION_MODEL (model), address, NULL, NULL, NULL);
 }
 
 static void
@@ -290,84 +264,12 @@ switch_page_cb (GtkNotebook            *notebook,
 }
 
 static void
-action_activated_cb (GtkEntryCompletion     *completion,
-                     int                     index,
-                     EphyLocationController *controller)
-{
-  GtkWidget *entry;
-  char *content;
-  char *url;
-  char **engine_names;
-
-  entry = gtk_entry_completion_get_entry (completion);
-  content = gtk_editable_get_chars (GTK_EDITABLE (entry), 0, -1);
-  if (content == NULL)
-    return;
-
-  engine_names = ephy_search_engine_manager_get_names (controller->search_engine_manager);
-  url = ephy_search_engine_manager_build_search_address (controller->search_engine_manager,
-                                                         engine_names[index],
-                                                         content);
-  g_strfreev (engine_names);
-
-  ephy_link_open (EPHY_LINK (controller), url, NULL,
-                  ephy_link_flags_from_current_event ());
-  g_free (content);
-  g_free (url);
-}
-
-static void
-fill_entry_completion_with_actions (GtkEntryCompletion     *completion,
-                                    EphyLocationController *controller)
-{
-  char **engine_names;
-
-  engine_names = ephy_search_engine_manager_get_names (controller->search_engine_manager);
-
-  controller->num_search_engines_actions = 0;
-
-  for (guint i = 0; engine_names[i] != NULL; i++) {
-    gtk_entry_completion_insert_action_text (completion, i, engine_names[i]);
-    controller->num_search_engines_actions++;
-  }
-
-  g_strfreev (engine_names);
-}
-
-static void
-add_completion_actions (EphyLocationController *controller,
-                        EphyLocationEntry      *lentry)
-{
-  GtkEntryCompletion *completion = gtk_entry_get_completion (GTK_ENTRY (lentry));
-
-  fill_entry_completion_with_actions (completion, controller);
-  g_signal_connect (completion, "action_activated",
-                    G_CALLBACK (action_activated_cb), controller);
-}
-
-static void
-search_engines_changed_cb (EphySearchEngineManager *manager,
-                           gpointer  data)
-{
-  EphyLocationController *controller;
-  GtkEntryCompletion *completion;
-
-  controller = EPHY_LOCATION_CONTROLLER (data);
-  completion = gtk_entry_get_completion (GTK_ENTRY (controller->title_widget));
-
-  for (guint i = 0; i < controller->num_search_engines_actions; i++)
-    gtk_entry_completion_delete_action (completion, 0);
-
-  fill_entry_completion_with_actions (completion, controller);
-}
-
-static void
 ephy_location_controller_constructed (GObject *object)
 {
   EphyLocationController *controller = EPHY_LOCATION_CONTROLLER (object);
   EphyHistoryService *history_service;
   EphyBookmarksManager *bookmarks_manager;
-  EphyCompletionModel *model;
+  EphySuggestionModel *model;
   GtkWidget *notebook, *widget;
 
   G_OBJECT_CLASS (ephy_location_controller_parent_class)->constructed (object);
@@ -387,28 +289,11 @@ ephy_location_controller_constructed (GObject *object)
 
   history_service = ephy_embed_shell_get_global_history_service (ephy_embed_shell_get_default ());
   bookmarks_manager = ephy_shell_get_bookmarks_manager (ephy_shell_get_default ());
-  model = ephy_completion_model_new (history_service, bookmarks_manager);
-  ephy_location_entry_set_completion (EPHY_LOCATION_ENTRY (controller->title_widget),
-                                      GTK_TREE_MODEL (model),
-                                      EPHY_COMPLETION_TEXT_COL,
-                                      EPHY_COMPLETION_ACTION_COL,
-                                      EPHY_COMPLETION_KEYWORDS_COL,
-                                      EPHY_COMPLETION_RELEVANCE_COL,
-                                      EPHY_COMPLETION_URL_COL,
-                                      EPHY_COMPLETION_EXTRA_COL,
-                                      EPHY_COMPLETION_FAVICON_COL);
+  model = ephy_suggestion_model_new (history_service, bookmarks_manager);
+  egg_suggestion_entry_set_model (EGG_SUGGESTION_ENTRY (controller->title_widget),
+                                  G_LIST_MODEL (model));
   g_object_unref (model);
 
-  ephy_location_entry_set_match_func (EPHY_LOCATION_ENTRY (controller->title_widget),
-                                      match_func,
-                                      controller->title_widget,
-                                      NULL);
-
-  add_completion_actions (controller, EPHY_LOCATION_ENTRY (controller->title_widget));
-
-  g_signal_connect (controller->search_engine_manager, "changed",
-                    G_CALLBACK (search_engines_changed_cb), controller);
-
   g_object_bind_property (controller, "editable",
                           controller->title_widget, "editable",
                           G_BINDING_SYNC_CREATE);
diff --git a/src/ephy-suggestion-model.c b/src/ephy-suggestion-model.c
new file mode 100644
index 0000000..63cfd06
--- /dev/null
+++ b/src/ephy-suggestion-model.c
@@ -0,0 +1,417 @@
+/* ephy-suggestion-model.c
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * 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 <glib/gi18n.h>
+
+#include "egg-suggestion.h"
+#include "ephy-suggestion-model.h"
+
+#define MAX_COMPLETION_HISTORY_URLS 8
+
+struct _EphySuggestionModel
+{
+  GObject               parent;
+  EphyHistoryService   *history_service;
+  EphyBookmarksManager *bookmarks_manager;
+  GSequence            *items;
+  GSList               *search_terms;
+};
+
+enum {
+  PROP_0,
+  PROP_BOOKMARKS_MANAGER,
+  PROP_HISTORY_SERVICE,
+  N_PROPS
+};
+
+static void list_model_iface_init (GListModelInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (EphySuggestionModel, ephy_suggestion_model, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ephy_suggestion_model_finalize (GObject *object)
+{
+  EphySuggestionModel *self = (EphySuggestionModel *)object;
+
+  g_clear_object (&self->bookmarks_manager);
+  g_clear_object (&self->history_service);
+  g_clear_pointer (&self->items, g_sequence_free);
+
+  g_slist_free_full (self->search_terms, (GDestroyNotify)g_regex_unref);
+
+  G_OBJECT_CLASS (ephy_suggestion_model_parent_class)->finalize (object);
+}
+
+static void
+ephy_suggestion_model_get_property (GObject    *object,
+                                    guint       prop_id,
+                                    GValue     *value,
+                                    GParamSpec *pspec)
+{
+  EphySuggestionModel *self = EPHY_SUGGESTION_MODEL (object);
+
+  switch (prop_id)
+    {
+    case PROP_HISTORY_SERVICE:
+      g_value_set_object (value, self->history_service);
+      break;
+
+    case PROP_BOOKMARKS_MANAGER:
+      g_value_set_object (value, self->bookmarks_manager);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ephy_suggestion_model_set_property (GObject      *object,
+                                    guint         prop_id,
+                                    const GValue *value,
+                                    GParamSpec   *pspec)
+{
+  EphySuggestionModel *self = EPHY_SUGGESTION_MODEL (object);
+
+  switch (prop_id)
+    {
+    case PROP_HISTORY_SERVICE:
+      self->history_service = g_value_dup_object (value);
+      break;
+
+    case PROP_BOOKMARKS_MANAGER:
+      self->bookmarks_manager = g_value_dup_object (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ephy_suggestion_model_class_init (EphySuggestionModelClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ephy_suggestion_model_finalize;
+  object_class->get_property = ephy_suggestion_model_get_property;
+  object_class->set_property = ephy_suggestion_model_set_property;
+
+  properties [PROP_BOOKMARKS_MANAGER] =
+    g_param_spec_object ("bookmarks-manager",
+                         "Bookmarks Manager",
+                         "The bookmarks manager for suggestions",
+                         EPHY_TYPE_BOOKMARKS_MANAGER,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_HISTORY_SERVICE] =
+    g_param_spec_object ("history-service",
+                         "History Service",
+                         "The history service for suggestions",
+                         EPHY_TYPE_HISTORY_SERVICE,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ephy_suggestion_model_init (EphySuggestionModel *self)
+{
+  self->items = g_sequence_new (g_object_unref);
+}
+
+static GType
+ephy_suggestion_model_get_item_type (GListModel *model)
+{
+  return EGG_TYPE_SUGGESTION;
+}
+
+static guint
+ephy_suggestion_model_get_n_items (GListModel *model)
+{
+  EphySuggestionModel *self = EPHY_SUGGESTION_MODEL (model);
+
+  return g_sequence_get_length (self->items);
+}
+
+static gpointer
+ephy_suggestion_model_get_item (GListModel *model,
+                                guint       position)
+{
+  EphySuggestionModel *self = EPHY_SUGGESTION_MODEL (model);
+  GSequenceIter *iter;
+  EggSuggestion *suggestion;
+
+  iter = g_sequence_get_iter_at_pos (self->items, position);
+  suggestion = g_sequence_get (iter);
+
+  return g_object_ref (suggestion);
+}
+
+static void
+list_model_iface_init (GListModelInterface *iface)
+{
+  iface->get_item_type = ephy_suggestion_model_get_item_type;
+  iface->get_item = ephy_suggestion_model_get_item;
+  iface->get_n_items = ephy_suggestion_model_get_n_items;
+}
+
+EphySuggestionModel *
+ephy_suggestion_model_new (EphyHistoryService   *history_service,
+                           EphyBookmarksManager *bookmarks_manager)
+{
+  g_return_val_if_fail (EPHY_IS_HISTORY_SERVICE (history_service), NULL);
+  g_return_val_if_fail (EPHY_IS_BOOKMARKS_MANAGER (bookmarks_manager), NULL);
+
+  return g_object_new (EPHY_TYPE_SUGGESTION_MODEL,
+                       "history-service", history_service,
+                       "bookmarks-manager", bookmarks_manager,
+                       NULL);
+}
+
+static void
+update_search_terms (EphySuggestionModel *self,
+                     const char          *text)
+{
+  const char *current;
+  const char *ptr;
+  char *tmp;
+  char *term;
+  GRegex *term_regex;
+  GRegex *quote_regex;
+  gint count;
+  gboolean inside_quotes = FALSE;
+
+  g_assert (EPHY_IS_SUGGESTION_MODEL (self));
+
+  if (self->search_terms) {
+    g_slist_free_full (self->search_terms, (GDestroyNotify)g_regex_unref);
+    self->search_terms = NULL;
+  }
+
+  quote_regex = g_regex_new ("\"", G_REGEX_OPTIMIZE,
+                             G_REGEX_MATCH_NOTEMPTY, NULL);
+
+  /*
+   * This code loops through the string using pointer arythmetics.
+   * Although the string we are handling may contain UTF-8 chars
+   * this works because only ASCII chars affect what is actually
+   * copied from the string as a search term.
+   */
+  for (count = 0, current = ptr = text; ptr[0] != '\0'; ptr++, count++) {
+    /*
+     * If we found a double quote character; we will
+     * consume bytes up until the next quote, or
+     * end of line;
+     */
+    if (ptr[0] == '"')
+      inside_quotes = !inside_quotes;
+
+    /*
+     * If we found a space, and we are not looking for a
+     * closing double quote, or if the next char is the
+     * end of the string, append what we have already as
+     * a search term.
+     */
+    if (((ptr[0] == ' ') && (!inside_quotes)) || ptr[1] == '\0') {
+      /*
+       * We special-case the end of the line because
+       * we would otherwise not copy the last character
+       * of the search string, since the for loop will
+       * stop before that.
+       */
+      if (ptr[1] == '\0')
+        count++;
+
+      /*
+       * remove quotes, and quote any regex-sensitive
+       * characters
+       */
+      tmp = g_regex_escape_string (current, count);
+      term = g_regex_replace (quote_regex, tmp, -1, 0,
+                              "", G_REGEX_MATCH_NOTEMPTY, NULL);
+      g_strstrip (term);
+      g_free (tmp);
+
+      /* we don't want empty search terms */
+      if (term[0] != '\0') {
+        term_regex = g_regex_new (term,
+                                  G_REGEX_CASELESS | G_REGEX_OPTIMIZE,
+                                  G_REGEX_MATCH_NOTEMPTY, NULL);
+        self->search_terms = g_slist_append (self->search_terms, term_regex);
+      }
+      g_free (term);
+
+      /* count will be incremented by the for loop */
+      count = -1;
+      current = ptr + 1;
+    }
+  }
+
+  g_regex_unref (quote_regex);
+}
+
+static gboolean
+should_add_bookmark_to_model (EphySuggestionModel *self,
+                              const char          *search_string,
+                              const char          *title,
+                              const char          *location)
+{
+  gboolean ret = TRUE;
+
+  if (self->search_terms) {
+    GSList *iter;
+    GRegex *current = NULL;
+
+    for (iter = self->search_terms; iter != NULL; iter = iter->next) {
+      current = (GRegex *)iter->data;
+      if ((!g_regex_match (current, title ? title : "", G_REGEX_MATCH_NOTEMPTY, NULL)) &&
+          (!g_regex_match (current, location ? location : "", G_REGEX_MATCH_NOTEMPTY, NULL))) {
+        ret = FALSE;
+        break;
+      }
+    }
+  }
+
+  return ret;
+}
+
+static void
+query_completed_cb (EphyHistoryService *service,
+                    gboolean            success,
+                    gpointer            result_data,
+                    gpointer            user_data)
+{
+  g_autoptr(GTask) task = user_data;
+  EphySuggestionModel *self;
+  const gchar *query;
+  GSequence *bookmarks;
+  GList *urls = NULL;
+  guint removed;
+  guint added = 0;
+
+  self = g_task_get_source_object (task);
+  query = g_task_get_task_data (task);
+
+  removed = g_sequence_get_length (self->items);
+
+  g_clear_pointer (&self->items, g_sequence_free);
+  self->items = g_sequence_new (g_object_unref);
+
+  /* Add bookmarks */
+  bookmarks = ephy_bookmarks_manager_get_bookmarks (self->bookmarks_manager);
+  for (GSequenceIter *iter = g_sequence_get_begin_iter (bookmarks);
+       !g_sequence_iter_is_end (iter);
+       iter = g_sequence_iter_next (iter))
+    {
+      EphyBookmark *bookmark;
+      const char *url, *title;
+
+      bookmark = g_sequence_get (iter);
+
+      url = ephy_bookmark_get_url (bookmark);
+      title = ephy_bookmark_get_title (bookmark);
+
+      if (should_add_bookmark_to_model (self, query, title, url))
+        {
+          g_autoptr(EggSuggestion) suggestion = NULL;
+
+          suggestion = g_object_new (EGG_TYPE_SUGGESTION,
+                                     "title", url,
+                                     "subtitle", title,
+                                     "id", url,
+                                     NULL);
+          g_sequence_append (self->items, g_steal_pointer (&suggestion));
+          added++;
+        }
+    }
+
+  /* History */
+  urls = (GList *)result_data;
+
+  for (const GList *p = g_list_last (urls); p != NULL; p = p->prev)
+    {
+      EphyHistoryURL *url = (EphyHistoryURL *)p->data;
+      g_autoptr(EggSuggestion) suggestion = NULL;
+      g_autofree gchar *title = g_markup_escape_text (url->url, -1);
+      g_autofree gchar *subtitle = g_markup_escape_text (url->title, -1);
+
+      suggestion = g_object_new (EGG_TYPE_SUGGESTION,
+                                 "icon-name", "web-browser-symbolic",
+                                 "id", url->url,
+                                 "title", title,
+                                 "subtitle", subtitle,
+                                 NULL);
+      g_sequence_prepend (self->items, g_steal_pointer (&suggestion));
+      added++;
+    }
+
+  g_list_model_items_changed (G_LIST_MODEL (self), 0, removed, added);
+
+  g_task_return_boolean (task, TRUE);
+}
+
+void
+ephy_suggestion_model_query_async (EphySuggestionModel *self,
+                                   const gchar         *query,
+                                   GCancellable        *cancellable,
+                                   GAsyncReadyCallback  callback,
+                                   gpointer             user_data)
+{
+  g_autoptr(GTask) task = NULL;
+  g_autofree gchar **strings = NULL;
+  GList *qlist = NULL;
+
+  g_return_if_fail (EPHY_IS_SUGGESTION_MODEL (self));
+  g_return_if_fail (query != NULL);
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, ephy_suggestion_model_query_async);
+  g_task_set_task_data (task, g_strdup (query), g_free);
+
+  /* Split the search string. */
+  strings = g_strsplit (query, " ", -1);
+  for (guint i = 0; strings[i]; i++)
+    qlist = g_list_append (qlist, g_steal_pointer (&strings[i]));
+
+  update_search_terms (self, query);
+
+  ephy_history_service_find_urls (self->history_service,
+                                  0, 0,
+                                  MAX_COMPLETION_HISTORY_URLS, 0,
+                                  qlist,
+                                  EPHY_HISTORY_SORT_MOST_VISITED,
+                                  cancellable,
+                                  (EphyHistoryJobCallback)query_completed_cb,
+                                  g_steal_pointer (&task));
+}
+
+gboolean
+ephy_suggestion_model_query_finish (EphySuggestionModel  *self,
+                                    GAsyncResult         *result,
+                                    GError              **error)
+{
+  g_return_val_if_fail (EPHY_IS_SUGGESTION_MODEL (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
diff --git a/src/ephy-suggestion-model.h b/src/ephy-suggestion-model.h
new file mode 100644
index 0000000..614c90a
--- /dev/null
+++ b/src/ephy-suggestion-model.h
@@ -0,0 +1,46 @@
+/* ephy-suggestion-model.h
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * 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 EPHY_SUGGESTION_MODEL_H
+#define EPHY_SUGGESTION_MODEL_H
+
+#include <gio/gio.h>
+
+#include "ephy-history-service.h"
+#include "ephy-bookmarks-manager.h"
+
+G_BEGIN_DECLS
+
+#define EPHY_TYPE_SUGGESTION_MODEL (ephy_suggestion_model_get_type())
+
+G_DECLARE_FINAL_TYPE (EphySuggestionModel, ephy_suggestion_model, EPHY, SUGGESTION_MODEL, GObject)
+
+EphySuggestionModel *ephy_suggestion_model_new          (EphyHistoryService    *history_service,
+                                                         EphyBookmarksManager  *bookmarks_manager);
+void                 ephy_suggestion_model_query_async  (EphySuggestionModel   *self,
+                                                         const gchar           *query,
+                                                         GCancellable          *cancellable,
+                                                         GAsyncReadyCallback    callback,
+                                                         gpointer               user_data);
+gboolean             ephy_suggestion_model_query_finish (EphySuggestionModel   *self,
+                                                         GAsyncResult          *result,
+                                                         GError               **error);
+
+G_END_DECLS
+
+#endif /* EPHY_SUGGESTION_MODEL_H */



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