[gnome-builder/wip/gtk4-port: 82/343] libide/gtk: add libide-gtk as landing spot for widgetry




commit 6082278718434c7d2b1113497e8eb706504cc1a8
Author: Christian Hergert <chergert redhat com>
Date:   Sun Mar 27 11:14:34 2022 -0700

    libide/gtk: add libide-gtk as landing spot for widgetry
    
    We have a bunch of widgetry that might land in Builder again instead of
    from libdazzle and this will be our landing spot for it along with things
    like animations (until we can use those from Adwaita), custom buttons,
    popovers, menu managers, etc.
    
    The goal here is to get a lot more out of libide-gui and into libide-gtk
    where we do not need to depend on much more than libide-core, io,
    threading, etc.

 src/libide/gtk/ide-animation.c            | 1184 +++++++++++++++++++++++++++++
 src/libide/gtk/ide-animation.h            |   86 +++
 src/libide/gtk/ide-frame-source-private.h |   34 +
 src/libide/gtk/ide-frame-source.c         |  173 +++++
 src/libide/gtk/libide-gtk.h               |   26 +
 src/libide/gtk/meson.build                |   82 ++
 src/libide/meson.build                    |    1 +
 7 files changed, 1586 insertions(+)
---
diff --git a/src/libide/gtk/ide-animation.c b/src/libide/gtk/ide-animation.c
new file mode 100644
index 000000000..9763f0b17
--- /dev/null
+++ b/src/libide/gtk/ide-animation.c
@@ -0,0 +1,1184 @@
+/* ide-animation.c
+ *
+ * Copyright (C) 2010-2022 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/>.
+ */
+
+#define G_LOG_DOMAIN "ide-animation"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <gobject/gvaluecollector.h>
+
+#include <gdk/gdk.h>
+#include <gtk/gtk.h>
+
+#include <stdlib.h>
+#include <string.h>
+
+#include "ide-animation.h"
+#include "ide-frame-source-private.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 _IdeAnimation
+{
+  GInitiallyUnowned  parent_instance;
+
+  gpointer           target;              /* Target object to animate */
+  gint64             begin_time;          /* Time in which animation started */
+  gint64             end_time;            /* Deadline for the animation */
+  guint              duration_msec;       /* Duration in milliseconds */
+  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 */
+  guint              stop_called : 1;
+};
+
+G_DEFINE_TYPE (IdeAnimation, ide_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[IDE_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);
+
+
+/**
+ * ide_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.
+ * @IDE_ANIMATION_CUBIC means the valu ewill be transformed into
+ * cubic acceleration (x * x * x).
+ */
+static gdouble
+ide_animation_alpha_ease_in_cubic (gdouble offset)
+{
+  return offset * offset * offset;
+}
+
+
+static gdouble
+ide_animation_alpha_ease_out_cubic (gdouble offset)
+{
+  gdouble p = offset - 1.0;
+
+  return p * p * p + 1.0;
+}
+
+static gdouble
+ide_animation_alpha_ease_in_out_cubic (gdouble offset)
+{
+  gdouble p = offset * 2.0;
+
+  if (p < 1.0)
+    return 0.5 * p * p * p;
+  p -= 2.0;
+  return 0.5 * (p * p * p + 2.0);
+}
+
+
+/**
+ * ide_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.
+ * @IDE_ANIMATION_LINEAR means no tranformation will be made.
+ *
+ * Returns: @offset.
+ * Side effects: None.
+ */
+static gdouble
+ide_animation_alpha_linear (gdouble offset)
+{
+  return offset;
+}
+
+
+/**
+ * ide_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.
+ * @IDE_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
+ide_animation_alpha_ease_in_quad (gdouble offset)
+{
+  return offset * offset;
+}
+
+
+/**
+ * ide_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.
+ * @IDE_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
+ide_animation_alpha_ease_out_quad (gdouble offset)
+{
+  return -1.0 * offset * (offset - 2.0);
+}
+
+
+/**
+ * ide_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.
+ * @IDE_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
+ide_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);
+}
+
+
+/**
+ * ide_animation_load_begin_values:
+ * @animation: (in): A #IdeAnimation.
+ *
+ * Load the begin values for all the properties we are about to
+ * animate.
+ *
+ * Side effects: None.
+ */
+static void
+ide_animation_load_begin_values (IdeAnimation *animation)
+{
+  g_assert (IDE_IS_ANIMATION (animation));
+
+  for (guint i = 0; i < animation->tweens->len; i++)
+    {
+      Tween *tween = &g_array_index (animation->tweens, Tween, i);
+
+      g_value_reset (&tween->begin);
+      g_object_get_property (animation->target,
+                             tween->pspec->name,
+                             &tween->begin);
+    }
+}
+
+
+/**
+ * ide_animation_unload_begin_values:
+ * @animation: (in): A #IdeAnimation.
+ *
+ * Unloads the begin values for the animation. This might be particularly
+ * useful once we support pointer types.
+ *
+ * Side effects: None.
+ */
+static void
+ide_animation_unload_begin_values (IdeAnimation *animation)
+{
+  Tween *tween;
+  guint i;
+
+  g_assert (IDE_IS_ANIMATION (animation));
+
+  for (i = 0; i < animation->tweens->len; i++)
+    {
+      tween = &g_array_index (animation->tweens, Tween, i);
+      g_value_reset (&tween->begin);
+    }
+}
+
+
+/**
+ * ide_animation_get_offset:
+ * @animation: A #IdeAnimation.
+ * @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
+ide_animation_get_offset (IdeAnimation *animation,
+                          gint64        frame_time)
+{
+  g_assert (IDE_IS_ANIMATION (animation));
+
+  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_time = CLAMP (frame_time, animation->begin_time, animation->end_time);
+
+  /* Check end_time first in case end_time == begin_time */
+  if (frame_time == animation->end_time)
+    return 1.0;
+  else if (frame_time == animation->begin_time)
+    return 0.0;
+
+  return (frame_time - animation->begin_time) / (gdouble)(animation->duration_msec * 1000L);
+}
+
+
+/**
+ * ide_animation_update_property:
+ * @animation: (in): A #IdeAnimation.
+ * @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
+ide_animation_update_property (IdeAnimation  *animation,
+                              gpointer      target,
+                              Tween        *tween,
+                              const GValue *value)
+{
+  g_assert (IDE_IS_ANIMATION (animation));
+  g_assert (G_IS_OBJECT (target));
+  g_assert (tween);
+  g_assert (value);
+
+  g_object_set_property (target, tween->pspec->name, value);
+}
+
+
+/**
+ * ide_animation_get_value_at_offset:
+ * @animation: (in): A #IdeAnimation.
+ * @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
+ide_animation_get_value_at_offset (IdeAnimation *animation,
+                                   gdouble       offset,
+                                   Tween        *tween,
+                                   GValue       *value)
+{
+  g_assert (IDE_IS_ANIMATION (animation));
+  g_assert (tween != NULL);
+  g_assert (value != NULL);
+  g_assert (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
+ide_animation_set_frame_clock (IdeAnimation  *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
+ide_animation_set_target (IdeAnimation *animation,
+                          gpointer      target)
+{
+  g_assert (!animation->target);
+
+  animation->target = g_object_ref (target);
+
+  if (GTK_IS_WIDGET (animation->target))
+    ide_animation_set_frame_clock (animation,
+                                  gtk_widget_get_frame_clock (animation->target));
+}
+
+
+/**
+ * ide_animation_tick:
+ * @animation: (in): A #IdeAnimation.
+ *
+ * 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
+ide_animation_tick (IdeAnimation *animation,
+                    gdouble       offset)
+{
+  gdouble alpha;
+  GValue value = { 0 };
+  Tween *tween;
+  guint i;
+
+  g_assert (IDE_IS_ANIMATION (animation));
+
+  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);
+      ide_animation_get_value_at_offset (animation, alpha, tween, &value);
+      ide_animation_update_property (animation,
+                                     animation->target,
+                                     tween,
+                                     &value);
+      g_value_unset (&value);
+    }
+
+  /*
+   * Notify anyone interested in the tick signal.
+   */
+  g_signal_emit (animation, signals[TICK], 0);
+
+  animation->last_offset = offset;
+
+  return offset < 1.0;
+}
+
+
+/**
+ * ide_animation_timeout_cb:
+ * @user_data: (in): A #IdeAnimation.
+ *
+ * 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
+ide_animation_timeout_cb (gpointer user_data)
+{
+  IdeAnimation *animation = user_data;
+  gboolean ret;
+  gdouble offset;
+
+  offset = ide_animation_get_offset (animation, 0);
+
+  if (!(ret = ide_animation_tick (animation, offset)))
+    ide_animation_stop (animation);
+
+  return ret;
+}
+
+
+static gboolean
+ide_animation_widget_tick_cb (GdkFrameClock *frame_clock,
+                              IdeAnimation  *animation)
+{
+  gboolean ret = G_SOURCE_REMOVE;
+
+  g_assert (GDK_IS_FRAME_CLOCK (frame_clock));
+  g_assert (IDE_IS_ANIMATION (animation));
+
+  if (animation->tween_handler)
+    {
+      gdouble offset;
+
+      offset = ide_animation_get_offset (animation, 0);
+
+      if (!(ret = ide_animation_tick (animation, offset)))
+        ide_animation_stop (animation);
+    }
+
+  return ret;
+}
+
+
+static void
+ide_animation_widget_after_paint_cb (GdkFrameClock *frame_clock,
+                                     IdeAnimation  *animation)
+{
+  gint64 base_time;
+  gint64 interval;
+  gint64 next_frame_time;
+  gdouble offset;
+
+  g_assert (GDK_IS_FRAME_CLOCK (frame_clock));
+  g_assert (IDE_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 = ide_animation_get_offset (animation, next_frame_time);
+
+  ide_animation_tick (animation, offset);
+}
+
+
+/**
+ * ide_animation_start:
+ * @animation: (in): A #IdeAnimation.
+ *
+ * Start the animation. When the animation stops, the internal reference will
+ * be dropped and the animation may be finalized.
+ *
+ * Side effects: None.
+ */
+void
+ide_animation_start (IdeAnimation *animation)
+{
+  g_return_if_fail (IDE_IS_ANIMATION (animation));
+  g_return_if_fail (!animation->tween_handler);
+
+  g_object_ref_sink (animation);
+  ide_animation_load_begin_values (animation);
+
+  /*
+   * We want the real current time instead of the GdkFrameClocks current time
+   * because if the clock was asleep, it could be innaccurate.
+   */
+
+  if (animation->frame_clock)
+    {
+      animation->begin_time = gdk_frame_clock_get_frame_time (animation->frame_clock);
+      animation->end_time = animation->begin_time + (animation->duration_msec * 1000L);
+      animation->tween_handler =
+        g_signal_connect_object (animation->frame_clock,
+                                 "update",
+                                 G_CALLBACK (ide_animation_widget_tick_cb),
+                                 animation,
+                                 0);
+      animation->after_paint_handler =
+        g_signal_connect_object (animation->frame_clock,
+                                 "after-paint",
+                                 G_CALLBACK (ide_animation_widget_after_paint_cb),
+                                 animation,
+                                 0);
+      gdk_frame_clock_begin_updating (animation->frame_clock);
+    }
+  else
+    {
+      animation->begin_time = g_get_monotonic_time ();
+      animation->end_time = animation->begin_time + (animation->duration_msec * 1000L);
+      animation->tween_handler = ide_frame_source_add (FALLBACK_FRAME_RATE,
+                                                       ide_animation_timeout_cb,
+                                                       animation);
+    }
+}
+
+
+static void
+ide_animation_notify (IdeAnimation *self)
+{
+  g_assert (IDE_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);
+    }
+}
+
+
+/**
+ * ide_animation_stop:
+ * @animation: (nullable): A #IdeAnimation.
+ *
+ * Stops a running animation. The internal reference to the animation is
+ * dropped and therefore may cause the object to finalize.
+ *
+ * As a convenience, this function accepts %NULL for @animation but
+ * does nothing if that should occur.
+ */
+void
+ide_animation_stop (IdeAnimation *animation)
+{
+  if (animation == NULL)
+    return;
+
+  g_return_if_fail (IDE_IS_ANIMATION (animation));
+
+  if (animation->stop_called)
+    return;
+
+  animation->stop_called = TRUE;
+
+  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;
+        }
+      ide_animation_unload_begin_values (animation);
+      ide_animation_notify (animation);
+      g_object_unref (animation);
+    }
+}
+
+
+/**
+ * ide_animation_add_property:
+ * @animation: (in): A #IdeAnimation.
+ * @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
+ide_animation_add_property (IdeAnimation *animation,
+                            GParamSpec   *pspec,
+                            const GValue *value)
+{
+  Tween tween = { 0 };
+  GType type;
+
+  g_return_if_fail (IDE_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);
+}
+
+
+/**
+ * ide_animation_dispose:
+ * @object: (in): A #IdeAnimation.
+ *
+ * Releases any object references the animation contains.
+ *
+ * Side effects: None.
+ */
+static void
+ide_animation_dispose (GObject *object)
+{
+  IdeAnimation *self = IDE_ANIMATION (object);
+
+  g_clear_object (&self->target);
+  g_clear_object (&self->frame_clock);
+
+  G_OBJECT_CLASS (ide_animation_parent_class)->dispose (object);
+}
+
+
+/**
+ * ide_animation_finalize:
+ * @object: (in): A #IdeAnimation.
+ *
+ * Finalizes the object and releases any resources allocated.
+ *
+ * Side effects: None.
+ */
+static void
+ide_animation_finalize (GObject *object)
+{
+  IdeAnimation *self = IDE_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 (ide_animation_parent_class)->finalize (object);
+}
+
+
+/**
+ * ide_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
+ide_animation_set_property (GObject      *object,
+                            guint         prop_id,
+                            const GValue *value,
+                            GParamSpec   *pspec)
+{
+  IdeAnimation *animation = IDE_ANIMATION (object);
+
+  switch (prop_id)
+    {
+    case PROP_DURATION:
+      animation->duration_msec = g_value_get_uint (value) * slow_down_factor;
+      break;
+
+    case PROP_FRAME_CLOCK:
+      ide_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:
+      ide_animation_set_target (animation, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+
+/**
+ * ide_animation_class_init:
+ * @klass: (in): A #IdeAnimationClass.
+ *
+ * Initializes the GObjectClass.
+ *
+ * Side effects: Properties, signals, and vtables are initialized.
+ */
+static void
+ide_animation_class_init (IdeAnimationClass *klass)
+{
+  GObjectClass *object_class;
+  const gchar *slow_down_factor_env;
+
+  debug = !!g_getenv ("IDE_ANIMATION_DEBUG");
+  slow_down_factor_env = g_getenv ("IDE_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 = ide_animation_dispose;
+  object_class->finalize = ide_animation_finalize;
+  object_class->set_property = ide_animation_set_property;
+
+  /**
+   * IdeAnimation: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));
+
+  /**
+   * IdeAnimation: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",
+                       IDE_TYPE_ANIMATION_MODE,
+                       IDE_ANIMATION_LINEAR,
+                       (G_PARAM_WRITABLE |
+                        G_PARAM_CONSTRUCT_ONLY |
+                        G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeAnimation: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);
+
+  /**
+   * IdeAnimation::tick:
+   *
+   * The "tick" signal is emitted on each frame in the animation.
+   */
+  signals[TICK] = g_signal_new ("tick",
+                                 IDE_TYPE_ANIMATION,
+                                 G_SIGNAL_RUN_FIRST,
+                                 0,
+                                 NULL, NULL, NULL,
+                                 G_TYPE_NONE,
+                                 0);
+
+#define SET_ALPHA(_T, _t) \
+  alpha_funcs[IDE_ANIMATION_ ## _T] = ide_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);
+}
+
+
+/**
+ * ide_animation_init:
+ * @animation: (in): A #IdeAnimation.
+ *
+ * Initializes the #IdeAnimation instance.
+ *
+ * Side effects: Everything.
+ */
+static void
+ide_animation_init (IdeAnimation *animation)
+{
+  animation->duration_msec = 250;
+  animation->mode = IDE_ANIMATION_EASE_IN_OUT_QUAD;
+  animation->tweens = g_array_new (FALSE, FALSE, sizeof (Tween));
+  animation->last_offset = -G_MINDOUBLE;
+}
+
+
+/**
+ * ide_animation_mode_get_type:
+ *
+ * Retrieves the GType for #IdeAnimationMode.
+ *
+ * Returns: A GType.
+ * Side effects: GType registered on first call.
+ */
+GType
+ide_animation_mode_get_type (void)
+{
+  static GType type_id = 0;
+  static const GEnumValue values[] = {
+    { IDE_ANIMATION_LINEAR, "IDE_ANIMATION_LINEAR", "linear" },
+    { IDE_ANIMATION_EASE_IN_QUAD, "IDE_ANIMATION_EASE_IN_QUAD", "ease-in-quad" },
+    { IDE_ANIMATION_EASE_IN_OUT_QUAD, "IDE_ANIMATION_EASE_IN_OUT_QUAD", "ease-in-out-quad" },
+    { IDE_ANIMATION_EASE_OUT_QUAD, "IDE_ANIMATION_EASE_OUT_QUAD", "ease-out-quad" },
+    { IDE_ANIMATION_EASE_IN_CUBIC, "IDE_ANIMATION_EASE_IN_CUBIC", "ease-in-cubic" },
+    { IDE_ANIMATION_EASE_OUT_CUBIC, "IDE_ANIMATION_EASE_OUT_CUBIC", "ease-out-cubic" },
+    { IDE_ANIMATION_EASE_IN_OUT_CUBIC, "IDE_ANIMATION_EASE_IN_OUT_CUBIC", "ease-in-out-cubic" },
+    { 0 }
+  };
+
+  if (G_UNLIKELY (!type_id))
+    type_id = g_enum_register_static ("IdeAnimationMode", values);
+  return type_id;
+}
+
+/**
+ * ide_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 #IdeAnimation.
+ */
+IdeAnimation *
+ide_object_animatev (gpointer          object,
+                     IdeAnimationMode  mode,
+                     guint             duration_msec,
+                     GdkFrameClock    *frame_clock,
+                     const gchar      *first_property,
+                     va_list           args)
+{
+  IdeAnimation *animation;
+  GObjectClass *klass;
+  const gchar *name;
+  GParamSpec *pspec;
+  GValue value = { 0 };
+  gchar *error = NULL;
+  gboolean enable_animations;
+
+  g_return_val_if_fail (first_property != NULL, NULL);
+  g_return_val_if_fail (mode < IDE_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;
+  klass = G_OBJECT_GET_CLASS (object);
+  animation = g_object_new (IDE_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)))
+        {
+          g_critical (_("Failed to find property %s in %s"),
+                      name, G_OBJECT_CLASS_NAME (klass));
+          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;
+        }
+
+      ide_animation_add_property (animation, pspec, &value);
+      g_value_unset (&value);
+    }
+  while ((name = va_arg (args, const gchar *)));
+
+  ide_animation_start (animation);
+
+  return animation;
+
+failure:
+  g_object_ref_sink (animation);
+  g_object_unref (animation);
+  return NULL;
+}
+
+/**
+ * ide_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 #IdeAnimation.
+ * Side effects: None.
+ */
+IdeAnimation*
+ide_object_animate (gpointer        object,
+                    IdeAnimationMode mode,
+                    guint           duration_msec,
+                    GdkFrameClock  *frame_clock,
+                    const gchar    *first_property,
+                    ...)
+{
+  IdeAnimation *animation;
+  va_list args;
+
+  va_start (args, first_property);
+  animation = ide_object_animatev (object,
+                                   mode,
+                                   duration_msec,
+                                   frame_clock,
+                                   first_property,
+                                   args);
+  va_end (args);
+
+  return animation;
+}
+
+/**
+ * ide_object_animate_full:
+ *
+ * Return value: (transfer none): A #IdeAnimation.
+ */
+IdeAnimation*
+ide_object_animate_full (gpointer        object,
+                         IdeAnimationMode mode,
+                         guint           duration_msec,
+                         GdkFrameClock  *frame_clock,
+                         GDestroyNotify  notify,
+                         gpointer        notify_data,
+                         const gchar    *first_property,
+                         ...)
+{
+  IdeAnimation *animation;
+  va_list args;
+
+  va_start (args, first_property);
+  animation = ide_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
+ide_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       (150.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/src/libide/gtk/ide-animation.h b/src/libide/gtk/ide-animation.h
new file mode 100644
index 000000000..ed403d510
--- /dev/null
+++ b/src/libide/gtk/ide-animation.h
@@ -0,0 +1,86 @@
+/* ide-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/>.
+ */
+
+#pragma once
+
+#include <gdk/gdk.h>
+
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_ANIMATION      (ide_animation_get_type())
+#define IDE_TYPE_ANIMATION_MODE (ide_animation_mode_get_type())
+
+IDE_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (IdeAnimation, ide_animation, IDE, ANIMATION, GInitiallyUnowned)
+
+enum _IdeAnimationMode
+{
+  IDE_ANIMATION_LINEAR,
+  IDE_ANIMATION_EASE_IN_QUAD,
+  IDE_ANIMATION_EASE_OUT_QUAD,
+  IDE_ANIMATION_EASE_IN_OUT_QUAD,
+  IDE_ANIMATION_EASE_IN_CUBIC,
+  IDE_ANIMATION_EASE_OUT_CUBIC,
+  IDE_ANIMATION_EASE_IN_OUT_CUBIC,
+
+  IDE_ANIMATION_LAST
+};
+
+typedef enum   _IdeAnimationMode    IdeAnimationMode;
+
+IDE_AVAILABLE_IN_ALL
+GType         ide_animation_mode_get_type      (void);
+IDE_AVAILABLE_IN_ALL
+void          ide_animation_start              (IdeAnimation     *animation);
+IDE_AVAILABLE_IN_ALL
+void          ide_animation_stop               (IdeAnimation     *animation);
+IDE_AVAILABLE_IN_ALL
+void          ide_animation_add_property       (IdeAnimation     *animation,
+                                                GParamSpec       *pspec,
+                                                const GValue     *value);
+IDE_AVAILABLE_IN_ALL
+IdeAnimation *ide_object_animatev              (gpointer          object,
+                                                IdeAnimationMode  mode,
+                                                guint             duration_msec,
+                                                GdkFrameClock    *frame_clock,
+                                                const gchar      *first_property,
+                                                va_list           args);
+IDE_AVAILABLE_IN_ALL
+IdeAnimation* ide_object_animate               (gpointer          object,
+                                                IdeAnimationMode  mode,
+                                                guint             duration_msec,
+                                                GdkFrameClock    *frame_clock,
+                                                const gchar      *first_property,
+                                                ...) G_GNUC_NULL_TERMINATED;
+IDE_AVAILABLE_IN_ALL
+IdeAnimation* ide_object_animate_full          (gpointer          object,
+                                                IdeAnimationMode  mode,
+                                                guint             duration_msec,
+                                                GdkFrameClock    *frame_clock,
+                                                GDestroyNotify    notify,
+                                                gpointer          notify_data,
+                                                const gchar      *first_property,
+                                                ...) G_GNUC_NULL_TERMINATED;
+IDE_AVAILABLE_IN_ALL
+guint         ide_animation_calculate_duration (GdkMonitor       *monitor,
+                                                gdouble           from_value,
+                                                gdouble           to_value);
+
+G_END_DECLS
diff --git a/src/libide/gtk/ide-frame-source-private.h b/src/libide/gtk/ide-frame-source-private.h
new file mode 100644
index 000000000..ea7b930b3
--- /dev/null
+++ b/src/libide/gtk/ide-frame-source-private.h
@@ -0,0 +1,34 @@
+/* ide-frame-source.h
+ *
+ * Copyright (C) 2010-2022 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/>.
+ */
+
+#pragma once
+
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+guint ide_frame_source_add      (guint       frames_per_sec,
+                                 GSourceFunc callback,
+                                 gpointer    user_data);
+guint ide_frame_source_add_full (gint           priority,
+                                 guint          frames_per_sec,
+                                 GSourceFunc    callback,
+                                 gpointer       user_data,
+                                 GDestroyNotify notify);
+
+G_END_DECLS
diff --git a/src/libide/gtk/ide-frame-source.c b/src/libide/gtk/ide-frame-source.c
new file mode 100644
index 000000000..e5ffe8a98
--- /dev/null
+++ b/src/libide/gtk/ide-frame-source.c
@@ -0,0 +1,173 @@
+/* ide-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/>.
+ */
+
+#define G_LOG_DOMAIN "ide-frame-source"
+
+#include "config.h"
+
+#include "ide-frame-source-private.h"
+
+/**
+ * SECTION:ide-frame-source
+ * @title: IdeFrameSource
+ * @short_description: a frame source for objects without frame clocks
+ *
+ * If you are working with something that is not a #GtkWidget, getting
+ * access to a frame-clock is sometimes not possible. This can be used
+ * as a suitable fallback that approximates a frame-rate.
+ *
+ * If you have access to a #GdkFrameClock, in most cases you'll want that
+ * instead of using this.
+ */
+
+typedef struct
+{
+   GSource parent;
+   guint   fps;
+   guint   frame_count;
+   gint64  start_time;
+} IdeFrameSource;
+
+static gboolean
+ide_frame_source_prepare (GSource *source,
+                          gint    *timeout_)
+{
+   IdeFrameSource *fsource = (IdeFrameSource *)(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
+ide_frame_source_check (GSource *source)
+{
+   gint timeout_;
+   return ide_frame_source_prepare(source, &timeout_);
+}
+
+static gboolean
+ide_frame_source_dispatch (GSource     *source,
+                           GSourceFunc  source_func,
+                           gpointer     user_data)
+{
+   IdeFrameSource *fsource = (IdeFrameSource *)(gpointer)source;
+   gboolean ret;
+
+   if ((ret = source_func(user_data)))
+      fsource->frame_count++;
+   return ret;
+}
+
+static GSourceFuncs source_funcs = {
+   ide_frame_source_prepare,
+   ide_frame_source_check,
+   ide_frame_source_dispatch,
+};
+
+/**
+ * ide_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
+ide_frame_source_add (guint       frames_per_sec,
+                      GSourceFunc callback,
+                      gpointer    user_data)
+{
+   IdeFrameSource *fsource;
+   GSource *source;
+   guint ret;
+
+   g_return_val_if_fail (frames_per_sec > 0, 0);
+
+   source = g_source_new (&source_funcs, sizeof (IdeFrameSource));
+   fsource = (IdeFrameSource *)(gpointer)source;
+   fsource->fps = frames_per_sec;
+   fsource->frame_count = 0;
+   fsource->start_time = g_get_monotonic_time () / 1000L;
+   g_source_set_callback (source, callback, user_data, NULL);
+   g_source_set_name (source, "IdeFrameSource");
+
+   ret = g_source_attach (source, NULL);
+   g_source_unref (source);
+
+   return ret;
+}
+
+guint
+ide_frame_source_add_full (gint           priority,
+                           guint          frames_per_sec,
+                           GSourceFunc    callback,
+                           gpointer       user_data,
+                           GDestroyNotify notify)
+{
+   IdeFrameSource *fsource;
+   GSource *source;
+   guint ret;
+
+   g_return_val_if_fail (frames_per_sec > 0, 0);
+
+   source = g_source_new (&source_funcs, sizeof (IdeFrameSource));
+   fsource = (IdeFrameSource *)(gpointer)source;
+   fsource->fps = frames_per_sec;
+   fsource->frame_count = 0;
+   fsource->start_time = g_get_monotonic_time () / 1000L;
+   g_source_set_callback (source, callback, user_data, notify);
+   g_source_set_name (source, "IdeFrameSource");
+
+   ret = g_source_attach (source, NULL);
+   g_source_unref (source);
+
+   return ret;
+}
diff --git a/src/libide/gtk/libide-gtk.h b/src/libide/gtk/libide-gtk.h
new file mode 100644
index 000000000..d5aa77a6a
--- /dev/null
+++ b/src/libide/gtk/libide-gtk.h
@@ -0,0 +1,26 @@
+/* libide-gtk.h
+ *
+ * Copyright 2022 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/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#define IDE_GTK_INSIDE
+# include "ide-animation.h"
+# include "ide-gtk-enums.h"
+#undef IDE_GTK_INSIDE
diff --git a/src/libide/gtk/meson.build b/src/libide/gtk/meson.build
new file mode 100644
index 000000000..f057f7a9d
--- /dev/null
+++ b/src/libide/gtk/meson.build
@@ -0,0 +1,82 @@
+libide_gtk_header_dir = join_paths(libide_header_dir, 'gtk')
+libide_gtk_header_subdir = join_paths(libide_header_subdir, 'gtk')
+libide_include_directories += include_directories('.')
+
+#
+# Public API Headers
+#
+
+libide_gtk_public_headers = [
+  'ide-animation.h',
+  'libide-gtk.h',
+]
+
+libide_gtk_enum_headers = [
+  'ide-animation.h',
+]
+
+install_headers(libide_gtk_public_headers, subdir: libide_gtk_header_subdir)
+
+#
+# Sources
+#
+
+libide_gtk_public_sources = [
+  'ide-animation.c',
+]
+
+libide_gtk_private_sources = [
+  'ide-frame-source.c',
+]
+
+#
+# Enum generation
+#
+
+libide_gtk_enums = gnome.mkenums_simple('ide-gtk-enums',
+     body_prefix: '#include "config.h"',
+   header_prefix: '#include <libide-core.h>',
+       decorator: '_IDE_EXTERN',
+         sources: libide_gtk_enum_headers,
+  install_header: true,
+     install_dir: libide_gtk_header_dir,
+)
+libide_gtk_generated_sources = [libide_gtk_enums[0]]
+libide_gtk_generated_headers = [libide_gtk_enums[1]]
+
+#
+# Dependencies
+#
+
+libide_gtk_deps = [
+  libgio_dep,
+  libgtk_dep,
+
+  libide_core_dep,
+  libide_io_dep,
+  libide_threading_dep,
+]
+
+#
+# Library Definitions
+#
+
+libide_gtk = static_library('ide-gtk-' + libide_api_version,
+   libide_gtk_public_sources + libide_gtk_private_sources + libide_gtk_generated_sources + 
libide_gtk_generated_headers,
+   dependencies: libide_gtk_deps,
+         c_args: libide_args + release_args + ['-DIDE_GTK_COMPILATION'],
+)
+
+libide_gtk_dep = declare_dependency(
+         dependencies: libide_gtk_deps,
+            link_with: libide_gtk,
+  include_directories: include_directories('.'),
+             sources: libide_gtk_generated_headers,
+)
+
+gnome_builder_public_sources += files(libide_gtk_public_sources)
+gnome_builder_public_headers += files(libide_gtk_public_headers)
+gnome_builder_generated_headers += libide_gtk_generated_headers
+gnome_builder_generated_sources += libide_gtk_generated_sources
+gnome_builder_include_subdirs += libide_gtk_header_subdir
+gnome_builder_gir_extra_args += ['--c-include=libide-gtk.h', '-DIDE_GTK_COMPILATION']
diff --git a/src/libide/meson.build b/src/libide/meson.build
index d522cec7c..4ee4643d7 100644
--- a/src/libide/meson.build
+++ b/src/libide/meson.build
@@ -13,6 +13,7 @@ subdir('search')
 subdir('projects')
 subdir('foundry')
 subdir('debugger')
+subdir('gtk')
 subdir('themes')
 subdir('gui')
 subdir('terminal')


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