summaryrefslogtreecommitdiff
path: root/src/animation/gtd-timeline.c
diff options
context:
space:
mode:
authorMatthew Fennell <matthew@fennell.dev>2025-12-27 12:40:20 +0000
committerMatthew Fennell <matthew@fennell.dev>2025-12-27 12:40:20 +0000
commit5d8e439bc597159e3c9f0a8b65c0ae869dead3a8 (patch)
treeed28aefed8add0da1c55c08fdf80b23c4346e0dc /src/animation/gtd-timeline.c
Import Upstream version 43.0upstream/latest
Diffstat (limited to 'src/animation/gtd-timeline.c')
-rw-r--r--src/animation/gtd-timeline.c1547
1 files changed, 1547 insertions, 0 deletions
diff --git a/src/animation/gtd-timeline.c b/src/animation/gtd-timeline.c
new file mode 100644
index 0000000..da16fd9
--- /dev/null
+++ b/src/animation/gtd-timeline.c
@@ -0,0 +1,1547 @@
+/*
+ * Gtd.
+ *
+ * An OpenGL based 'interactive canvas' library.
+ *
+ * Authored By Matthew Allum <mallum@openedhand.com>
+ *
+ * Copyright (C) 2006 OpenedHand
+ *
+ * This library 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 of the License, or (at your option) any later version.
+ *
+ * This library 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 Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * SECTION:gtd-timeline
+ * @short_description: A class for time-based events
+ *
+ * #GtdTimeline is a base class for managing time-based event that cause
+ * GTK to redraw, such as animations.
+ *
+ * Each #GtdTimeline instance has a duration: once a timeline has been
+ * started, using gtd_timeline_start(), it will emit a signal that can
+ * be used to update the state of the widgets.
+ *
+ * It is important to note that #GtdTimeline is not a generic API for
+ * calling closures after an interval; each Timeline is tied into the master
+ * clock used to drive the frame cycle. If you need to schedule a closure
+ * after an interval, see gtd_threads_add_timeout() instead.
+ *
+ * Users of #GtdTimeline should connect to the #GtdTimeline::new-frame
+ * signal, which is emitted each time a timeline is advanced during the maste
+ * clock iteration. The #GtdTimeline::new-frame signal provides the time
+ * elapsed since the beginning of the timeline, in milliseconds. A normalized
+ * progress value can be obtained by calling gtd_timeline_get_progress().
+ * By using gtd_timeline_get_delta() it is possible to obtain the wallclock
+ * time elapsed since the last emission of the #GtdTimeline::new-frame
+ * signal.
+ *
+ * Initial state can be set up by using the #GtdTimeline::started signal,
+ * while final state can be set up by using the #GtdTimeline::stopped
+ * signal. The #GtdTimeline guarantees the emission of at least a single
+ * #GtdTimeline::new-frame signal, as well as the emission of the
+ * #GtdTimeline::completed signal every time the #GtdTimeline reaches
+ * its #GtdTimeline:duration.
+ *
+ * It is possible to connect to specific points in the timeline progress by
+ * adding markers using gtd_timeline_add_marker_at_time() and connecting
+ * to the #GtdTimeline::marker-reached signal.
+ *
+ * Timelines can be made to loop once they reach the end of their duration, by
+ * using gtd_timeline_set_repeat_count(); a looping timeline will still
+ * emit the #GtdTimeline::completed signal once it reaches the end of its
+ * duration at each repeat. If you want to be notified of the end of the last
+ * repeat, use the #GtdTimeline::stopped signal.
+ *
+ * Timelines have a #GtdTimeline:direction: the default direction is
+ * %GTD_TIMELINE_FORWARD, and goes from 0 to the duration; it is possible
+ * to change the direction to %GTD_TIMELINE_BACKWARD, and have the timeline
+ * go from the duration to 0. The direction can be automatically reversed
+ * when reaching completion by using the #GtdTimeline:auto-reverse property.
+ *
+ * Timelines are used in the Gtd animation framework by classes like
+ * #GtdTransition.
+ */
+
+#define G_LOG_DOMAIN "GtdTimeline"
+
+#include "gtd-timeline.h"
+
+#include "gtd-debug.h"
+#include "endeavour.h"
+
+typedef struct
+{
+ GtdTimelineDirection direction;
+
+ GtdWidget *widget;
+ guint frame_tick_id;
+
+ gint64 duration_us;
+ gint64 delay_us;
+
+ /* The current amount of elapsed time */
+ gint64 elapsed_time_us;
+
+ /* The elapsed time since the last frame was fired */
+ gint64 delta_us;
+
+ /* Time we last advanced the elapsed time and showed a frame */
+ gint64 last_frame_time_us;
+
+ gint64 start_us;
+
+ /* How many times the timeline should repeat */
+ gint repeat_count;
+
+ /* The number of times the timeline has repeated */
+ gint current_repeat;
+
+ GtdTimelineProgressFunc progress_func;
+ gpointer progress_data;
+ GDestroyNotify progress_notify;
+ GtdEaseMode progress_mode;
+
+ guint is_playing : 1;
+
+ /* If we've just started playing and haven't yet gotten
+ * a tick from the master clock
+ */
+ guint auto_reverse : 1;
+} GtdTimelinePrivate;
+
+enum
+{
+ PROP_0,
+
+ PROP_AUTO_REVERSE,
+ PROP_DELAY,
+ PROP_DURATION,
+ PROP_DIRECTION,
+ PROP_REPEAT_COUNT,
+ PROP_PROGRESS_MODE,
+ PROP_WIDGET,
+
+ PROP_LAST
+};
+
+static GParamSpec *obj_props[PROP_LAST] = { NULL, };
+
+enum
+{
+ NEW_FRAME,
+ STARTED,
+ PAUSED,
+ COMPLETED,
+ STOPPED,
+
+ LAST_SIGNAL
+};
+
+static guint timeline_signals[LAST_SIGNAL] = { 0, };
+
+static void set_is_playing (GtdTimeline *self,
+ gboolean is_playing);
+
+G_DEFINE_TYPE_WITH_PRIVATE (GtdTimeline, gtd_timeline, G_TYPE_OBJECT)
+
+static inline gint64
+us_to_ms (gint64 ms)
+{
+ return ms / 1000;
+}
+
+static inline gint64
+ms_to_us (gint64 us)
+{
+ return us * 1000;
+}
+
+static inline gboolean
+is_waiting_for_delay (GtdTimeline *self,
+ gint64 frame_time_us)
+{
+ GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+ return priv->start_us + priv->delay_us > frame_time_us;
+}
+
+static void
+emit_frame_signal (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+ gint64 elapsed_time_ms = us_to_ms (priv->elapsed_time_us);
+
+ GTD_TRACE_MSG ("Emitting ::new-frame signal on timeline[%p]", self);
+
+ g_signal_emit (self, timeline_signals[NEW_FRAME], 0, elapsed_time_ms);
+}
+
+static gboolean
+is_complete (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+
+ return (priv->direction == GTD_TIMELINE_FORWARD
+ ? priv->elapsed_time_us >= priv->duration_us
+ : priv->elapsed_time_us <= 0);
+}
+
+static gboolean
+maybe_loop_timeline (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+ GtdTimelineDirection saved_direction = priv->direction;
+ gint64 overflow_us = priv->elapsed_time_us;
+ gint64 end_us;
+
+ /* Update the current elapsed time in case the signal handlers
+ * want to take a peek. If we clamp elapsed time, then we need
+ * to correpondingly reduce elapsed_time_delta to reflect the correct
+ * range of times */
+ if (priv->direction == GTD_TIMELINE_FORWARD)
+ priv->elapsed_time_us = priv->duration_us;
+ else if (priv->direction == GTD_TIMELINE_BACKWARD)
+ priv->elapsed_time_us = 0;
+
+ end_us = priv->elapsed_time_us;
+
+ /* Emit the signal */
+ emit_frame_signal (self);
+
+ /* Did the signal handler modify the elapsed time? */
+ if (priv->elapsed_time_us != end_us)
+ return TRUE;
+
+ /* Note: If the new-frame signal handler paused the timeline
+ * on the last frame we will still go ahead and send the
+ * completed signal */
+ GTD_TRACE_MSG ("Timeline [%p] completed (cur: %ldµs, tot: %ldµs)",
+ self,
+ priv->elapsed_time_us,
+ priv->delta_us);
+
+ if (priv->is_playing &&
+ (priv->repeat_count == 0 ||
+ priv->repeat_count == priv->current_repeat))
+ {
+ /* We stop the timeline now, so that the completed signal handler
+ * may choose to re-start the timeline
+ */
+ set_is_playing (self, FALSE);
+
+ g_signal_emit (self, timeline_signals[COMPLETED], 0);
+ g_signal_emit (self, timeline_signals[STOPPED], 0, TRUE);
+ }
+ else
+ {
+ g_signal_emit (self, timeline_signals[COMPLETED], 0);
+ }
+
+ priv->current_repeat += 1;
+
+ if (priv->auto_reverse)
+ {
+ /* :auto-reverse changes the direction of the timeline */
+ if (priv->direction == GTD_TIMELINE_FORWARD)
+ priv->direction = GTD_TIMELINE_BACKWARD;
+ else
+ priv->direction = GTD_TIMELINE_FORWARD;
+
+ g_object_notify_by_pspec (G_OBJECT (self),
+ obj_props[PROP_DIRECTION]);
+ }
+
+ /*
+ * Again check to see if the user has manually played with
+ * the elapsed time, before we finally stop or loop the timeline,
+ * except changing time from 0 -> duration (or vice-versa)
+ * since these are considered equivalent
+ */
+ if (priv->elapsed_time_us != end_us &&
+ !((priv->elapsed_time_us == 0 && end_us == priv->duration_us) ||
+ (priv->elapsed_time_us == priv->duration_us && end_us == 0)))
+ {
+ return TRUE;
+ }
+
+ if (priv->repeat_count == 0)
+ {
+ gtd_timeline_rewind (self);
+ return FALSE;
+ }
+
+ /* Try and interpolate smoothly around a loop */
+ if (saved_direction == GTD_TIMELINE_FORWARD)
+ priv->elapsed_time_us = overflow_us - priv->duration_us;
+ else
+ priv->elapsed_time_us = priv->duration_us + overflow_us;
+
+ /* Or if the direction changed, we try and bounce */
+ if (priv->direction != saved_direction)
+ priv->elapsed_time_us = priv->duration_us - priv->elapsed_time_us;
+
+ return TRUE;
+}
+
+static gboolean
+tick_timeline (GtdTimeline *self,
+ gint64 tick_time_us)
+{
+ GtdTimelinePrivate *priv;
+ gboolean should_continue;
+ gboolean complete;
+ gint64 elapsed_us;
+
+ priv = gtd_timeline_get_instance_private (self);
+
+ GTD_TRACE_MSG ("Timeline [%p] ticked (elapsed_time: %ldµs, delta_us: %ldµs, "
+ "last_frame_time: %ldµs, tick_time: %ldµs)",
+ self,
+ priv->elapsed_time_us,
+ priv->delta_us,
+ priv->last_frame_time_us,
+ tick_time_us);
+
+ /* Check the is_playing variable before performing the timeline tick.
+ * This is necessary, as if a timeline is stopped in response to a
+ * frame clock generated signal of a different timeline, this code can
+ * still be reached.
+ */
+ if (!priv->is_playing)
+ return FALSE;
+
+ elapsed_us = tick_time_us - priv->last_frame_time_us;
+ priv->last_frame_time_us = tick_time_us;
+
+ if (is_waiting_for_delay (self, tick_time_us))
+ {
+ GTD_TRACE_MSG ("- waiting for delay");
+ return G_SOURCE_CONTINUE;
+ }
+
+ /* if the clock rolled back between ticks we need to
+ * account for it; the best course of action, since the
+ * clock roll back can happen by any arbitrary amount
+ * of milliseconds, is to drop a frame here
+ */
+ if (elapsed_us <= 0)
+ return TRUE;
+
+ priv->delta_us = elapsed_us;
+
+ GTD_TRACE_MSG ("Timeline [%p] activated (elapsed time: %ldµs, "
+ "duration: %ldµs, delta_us: %ldµs)",
+ self,
+ priv->elapsed_time_us,
+ priv->duration_us,
+ priv->delta_us);
+
+ g_object_ref (self);
+
+ /* Advance time */
+ if (priv->direction == GTD_TIMELINE_FORWARD)
+ priv->elapsed_time_us += priv->delta_us;
+ else
+ priv->elapsed_time_us -= priv->delta_us;
+
+ complete = is_complete (self);
+ should_continue = !complete ? priv->is_playing : maybe_loop_timeline (self);
+
+ /* If we have not reached the end of the timeline */
+ if (!complete)
+ emit_frame_signal (self);
+
+ g_object_unref (self);
+
+ return should_continue;
+}
+
+static gboolean
+frame_tick_cb (GtkWidget *widget,
+ GdkFrameClock *frame_clock,
+ gpointer user_data)
+{
+ GtdTimeline *self = GTD_TIMELINE (user_data);
+ GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+ gint64 frame_time_us;
+
+ frame_time_us = gdk_frame_clock_get_frame_time (frame_clock);
+
+ if (tick_timeline (self, frame_time_us))
+ return G_SOURCE_CONTINUE;
+
+ priv->frame_tick_id = 0;
+ return G_SOURCE_REMOVE;
+}
+
+static void
+add_tick_callback (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+
+ g_assert (!(priv->frame_tick_id > 0 && !priv->widget));
+
+ if (priv->frame_tick_id == 0)
+ {
+ priv->frame_tick_id = gtk_widget_add_tick_callback (GTK_WIDGET (priv->widget),
+ frame_tick_cb,
+ self,
+ NULL);
+ }
+}
+
+static void
+remove_tick_callback (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+
+ g_assert (!(priv->frame_tick_id > 0 && !priv->widget));
+
+ if (priv->frame_tick_id > 0)
+ {
+ gtk_widget_remove_tick_callback (GTK_WIDGET (priv->widget), priv->frame_tick_id);
+ priv->frame_tick_id = 0;
+ }
+}
+
+static void
+set_is_playing (GtdTimeline *self,
+ gboolean is_playing)
+{
+ GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+
+ is_playing = !!is_playing;
+
+ if (is_playing == priv->is_playing)
+ return;
+
+ priv->is_playing = is_playing;
+
+ if (priv->is_playing)
+ {
+ priv->start_us = g_get_monotonic_time ();
+ priv->last_frame_time_us = priv->start_us;
+ priv->current_repeat = 0;
+
+ add_tick_callback (self);
+ }
+ else
+ {
+ remove_tick_callback (self);
+ }
+}
+
+static gdouble
+timeline_progress_func (GtdTimeline *self,
+ gdouble elapsed,
+ gdouble duration,
+ gpointer user_data)
+{
+ GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+
+ return gtd_easing_for_mode (priv->progress_mode, elapsed, duration);
+}
+
+
+/*
+ * GObject overrides
+ */
+
+static void
+gtd_timeline_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GtdTimeline *self = GTD_TIMELINE (object);
+
+ switch (prop_id)
+ {
+ case PROP_DELAY:
+ gtd_timeline_set_delay (self, g_value_get_uint (value));
+ break;
+
+ case PROP_DURATION:
+ gtd_timeline_set_duration (self, g_value_get_uint (value));
+ break;
+
+ case PROP_DIRECTION:
+ gtd_timeline_set_direction (self, g_value_get_enum (value));
+ break;
+
+ case PROP_AUTO_REVERSE:
+ gtd_timeline_set_auto_reverse (self, g_value_get_boolean (value));
+ break;
+
+ case PROP_REPEAT_COUNT:
+ gtd_timeline_set_repeat_count (self, g_value_get_int (value));
+ break;
+
+ case PROP_PROGRESS_MODE:
+ gtd_timeline_set_progress_mode (self, g_value_get_enum (value));
+ break;
+
+ case PROP_WIDGET:
+ gtd_timeline_set_widget (self, g_value_get_object (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gtd_timeline_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GtdTimeline *self = GTD_TIMELINE (object);
+ GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+
+ switch (prop_id)
+ {
+ case PROP_DELAY:
+ g_value_set_uint (value, us_to_ms (priv->delay_us));
+ break;
+
+ case PROP_DURATION:
+ g_value_set_uint (value, gtd_timeline_get_duration (self));
+ break;
+
+ case PROP_DIRECTION:
+ g_value_set_enum (value, priv->direction);
+ break;
+
+ case PROP_AUTO_REVERSE:
+ g_value_set_boolean (value, priv->auto_reverse);
+ break;
+
+ case PROP_REPEAT_COUNT:
+ g_value_set_int (value, priv->repeat_count);
+ break;
+
+ case PROP_PROGRESS_MODE:
+ g_value_set_enum (value, priv->progress_mode);
+ break;
+
+ case PROP_WIDGET:
+ g_value_set_object (value, priv->widget);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gtd_timeline_dispose (GObject *object)
+{
+ GtdTimeline *self = GTD_TIMELINE (object);
+ GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+
+ if (priv->progress_notify != NULL)
+ {
+ priv->progress_notify (priv->progress_data);
+ priv->progress_func = NULL;
+ priv->progress_data = NULL;
+ priv->progress_notify = NULL;
+ }
+
+ G_OBJECT_CLASS (gtd_timeline_parent_class)->dispose (object);
+}
+
+static void
+gtd_timeline_class_init (GtdTimelineClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ /**
+ * GtdTimeline::widget:
+ *
+ * The widget the timeline is associated with. This will determine what frame
+ * clock will drive it.
+ */
+ obj_props[PROP_WIDGET] =
+ g_param_spec_object ("widget",
+ "Widget",
+ "Associated GtdWidget",
+ GTD_TYPE_WIDGET,
+ G_PARAM_CONSTRUCT | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+ /**
+ * GtdTimeline:delay:
+ *
+ * A delay, in milliseconds, that should be observed by the
+ * timeline before actually starting.
+ */
+ obj_props[PROP_DELAY] =
+ g_param_spec_uint ("delay",
+ "Delay",
+ "Delay before start",
+ 0, G_MAXUINT,
+ 0,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * GtdTimeline:duration:
+ *
+ * Duration of the timeline in milliseconds.
+ */
+ obj_props[PROP_DURATION] =
+ g_param_spec_uint ("duration",
+ "Duration",
+ "Duration of the timeline in milliseconds",
+ 0, G_MAXUINT,
+ 1000,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * GtdTimeline:direction:GIT
+ *
+ * The direction of the timeline, either %GTD_TIMELINE_FORWARD or
+ * %GTD_TIMELINE_BACKWARD.
+ */
+ obj_props[PROP_DIRECTION] =
+ g_param_spec_enum ("direction",
+ "Direction",
+ "Direction of the timeline",
+ GTD_TYPE_TIMELINE_DIRECTION,
+ GTD_TIMELINE_FORWARD,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * GtdTimeline:auto-reverse:
+ *
+ * If the direction of the timeline should be automatically reversed
+ * when reaching the end.
+ */
+ obj_props[PROP_AUTO_REVERSE] =
+ g_param_spec_boolean ("auto-reverse",
+ "Auto Reverse",
+ "Whether the direction should be reversed when reaching the end",
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * GtdTimeline:repeat-count:
+ *
+ * Defines how many times the timeline should repeat.
+ *
+ * If the repeat count is 0, the timeline does not repeat.
+ *
+ * If the repeat count is set to -1, the timeline will repeat until it is
+ * stopped.
+ */
+ obj_props[PROP_REPEAT_COUNT] =
+ g_param_spec_int ("repeat-count",
+ "Repeat Count",
+ "How many times the timeline should repeat",
+ -1, G_MAXINT,
+ 0,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * GtdTimeline:progress-mode:
+ *
+ * Controls the way a #GtdTimeline computes the normalized progress.
+ */
+ obj_props[PROP_PROGRESS_MODE] =
+ g_param_spec_enum ("progress-mode",
+ "Progress Mode",
+ "How the timeline should compute the progress",
+ GTD_TYPE_EASE_MODE,
+ GTD_EASE_LINEAR,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ object_class->dispose = gtd_timeline_dispose;
+ object_class->set_property = gtd_timeline_set_property;
+ object_class->get_property = gtd_timeline_get_property;
+ g_object_class_install_properties (object_class, PROP_LAST, obj_props);
+
+ /**
+ * GtdTimeline::new-frame:
+ * @timeline: the timeline which received the signal
+ * @msecs: the elapsed time between 0 and duration
+ *
+ * The ::new-frame signal is emitted for each timeline running
+ * timeline before a new frame is drawn to give animations a chance
+ * to update the scene.
+ */
+ timeline_signals[NEW_FRAME] =
+ g_signal_new ("new-frame",
+ G_TYPE_FROM_CLASS (object_class),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GtdTimelineClass, new_frame),
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 1, G_TYPE_INT);
+ /**
+ * GtdTimeline::completed:
+ * @timeline: the #GtdTimeline which received the signal
+ *
+ * The #GtdTimeline::completed signal is emitted when the timeline's
+ * elapsed time reaches the value of the #GtdTimeline:duration
+ * property.
+ *
+ * This signal will be emitted even if the #GtdTimeline is set to be
+ * repeating.
+ *
+ * If you want to get notification on whether the #GtdTimeline has
+ * been stopped or has finished its run, including its eventual repeats,
+ * you should use the #GtdTimeline::stopped signal instead.
+ */
+ timeline_signals[COMPLETED] =
+ g_signal_new ("completed",
+ G_TYPE_FROM_CLASS (object_class),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GtdTimelineClass, completed),
+ NULL, NULL, NULL,
+ G_TYPE_NONE, 0);
+ /**
+ * GtdTimeline::started:
+ * @timeline: the #GtdTimeline which received the signal
+ *
+ * The ::started signal is emitted when the timeline starts its run.
+ * This might be as soon as gtd_timeline_start() is invoked or
+ * after the delay set in the GtdTimeline:delay property has
+ * expired.
+ */
+ timeline_signals[STARTED] =
+ g_signal_new ("started",
+ G_TYPE_FROM_CLASS (object_class),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GtdTimelineClass, started),
+ NULL, NULL, NULL,
+ G_TYPE_NONE, 0);
+ /**
+ * GtdTimeline::paused:
+ * @timeline: the #GtdTimeline which received the signal
+ *
+ * The ::paused signal is emitted when gtd_timeline_pause() is invoked.
+ */
+ timeline_signals[PAUSED] =
+ g_signal_new ("paused",
+ G_TYPE_FROM_CLASS (object_class),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GtdTimelineClass, paused),
+ NULL, NULL, NULL,
+ G_TYPE_NONE, 0);
+
+ /**
+ * GtdTimeline::stopped:
+ * @timeline: the #GtdTimeline that emitted the signal
+ * @is_finished: %TRUE if the signal was emitted at the end of the
+ * timeline.
+ *
+ * The #GtdTimeline::stopped signal is emitted when the timeline
+ * has been stopped, either because gtd_timeline_stop() has been
+ * called, or because it has been exhausted.
+ *
+ * This is different from the #GtdTimeline::completed signal,
+ * which gets emitted after every repeat finishes.
+ *
+ * If the #GtdTimeline has is marked as infinitely repeating,
+ * this signal will never be emitted.
+ */
+ timeline_signals[STOPPED] =
+ g_signal_new ("stopped",
+ G_TYPE_FROM_CLASS (object_class),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GtdTimelineClass, stopped),
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 1,
+ G_TYPE_BOOLEAN);
+}
+
+static void
+gtd_timeline_init (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+
+ priv->progress_mode = GTD_EASE_LINEAR;
+ priv->progress_func = timeline_progress_func;
+}
+
+/**
+ * gtd_timeline_new:
+ * @duration_ms: Duration of the timeline in milliseconds
+ *
+ * Creates a new #GtdTimeline with a duration of @duration_ms milli seconds.
+ *
+ * Return value: the newly created #GtdTimeline instance. Use
+ * g_object_unref() when done using it
+ */
+GtdTimeline *
+gtd_timeline_new (guint duration_ms)
+{
+ return g_object_new (GTD_TYPE_TIMELINE,
+ "duration", duration_ms,
+ NULL);
+}
+
+/**
+ * gtd_timeline_new_for_widget:
+ * @widget: The #GtdWidget the timeline is associated with
+ * @duration_ms: Duration of the timeline in milliseconds
+ *
+ * Creates a new #GtdTimeline with a duration of @duration milli seconds.
+ *
+ * Return value: the newly created #GtdTimeline instance. Use
+ * g_object_unref() when done using it
+ */
+GtdTimeline *
+gtd_timeline_new_for_widget (GtdWidget *widget,
+ guint duration_ms)
+{
+ return g_object_new (GTD_TYPE_TIMELINE,
+ "duration", duration_ms,
+ "widget", widget,
+ NULL);
+}
+
+/**
+ * gtd_timeline_set_widget:
+ * @timeline: a #GtdTimeline
+ * @widget: a #GtdWidget
+ *
+ * Set the widget the timeline is associated with.
+ */
+void
+gtd_timeline_set_widget (GtdTimeline *self,
+ GtdWidget *widget)
+{
+ GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+
+ g_return_if_fail (GTD_IS_TIMELINE (self));
+
+ if (priv->widget)
+ {
+ remove_tick_callback (self);
+ priv->widget = NULL;
+ }
+
+ priv->widget = widget;
+
+ if (priv->is_playing)
+ add_tick_callback (self);
+}
+
+/**
+ * gtd_timeline_start:
+ * @timeline: A #GtdTimeline
+ *
+ * Starts the #GtdTimeline playing.
+ **/
+void
+gtd_timeline_start (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv;
+
+ g_return_if_fail (GTD_IS_TIMELINE (self));
+
+ priv = gtd_timeline_get_instance_private (self);
+
+ if (priv->is_playing)
+ return;
+
+ if (priv->duration_us == 0)
+ return;
+
+ priv->delta_us = 0;
+ set_is_playing (self, TRUE);
+
+ g_signal_emit (self, timeline_signals[STARTED], 0);
+}
+
+/**
+ * gtd_timeline_pause:
+ * @timeline: A #GtdTimeline
+ *
+ * Pauses the #GtdTimeline on current frame
+ **/
+void
+gtd_timeline_pause (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv;
+
+ g_return_if_fail (GTD_IS_TIMELINE (self));
+
+ priv = gtd_timeline_get_instance_private (self);
+
+ if (!priv->is_playing)
+ return;
+
+ priv->delta_us = 0;
+ set_is_playing (self, FALSE);
+
+ g_signal_emit (self, timeline_signals[PAUSED], 0);
+}
+
+/**
+ * gtd_timeline_stop:
+ * @timeline: A #GtdTimeline
+ *
+ * Stops the #GtdTimeline and moves to frame 0
+ **/
+void
+gtd_timeline_stop (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv;
+ gboolean was_playing;
+
+ g_return_if_fail (GTD_IS_TIMELINE (self));
+
+ priv = gtd_timeline_get_instance_private (self);
+
+ /* we check the is_playing here because pause() will return immediately
+ * if the timeline wasn't playing, so we don't know if it was actually
+ * stopped, and yet we still don't want to emit a ::stopped signal if
+ * the timeline was not playing in the first place.
+ */
+ was_playing = priv->is_playing;
+
+ gtd_timeline_pause (self);
+ gtd_timeline_rewind (self);
+
+ if (was_playing)
+ g_signal_emit (self, timeline_signals[STOPPED], 0, FALSE);
+}
+
+/**
+ * gtd_timeline_rewind:
+ * @timeline: A #GtdTimeline
+ *
+ * Rewinds #GtdTimeline to the first frame if its direction is
+ * %GTD_TIMELINE_FORWARD and the last frame if it is
+ * %GTD_TIMELINE_BACKWARD.
+ */
+void
+gtd_timeline_rewind (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv;
+
+ g_return_if_fail (GTD_IS_TIMELINE (self));
+
+ priv = gtd_timeline_get_instance_private (self);
+
+ if (priv->direction == GTD_TIMELINE_FORWARD)
+ gtd_timeline_advance (self, 0);
+ else if (priv->direction == GTD_TIMELINE_BACKWARD)
+ gtd_timeline_advance (self, us_to_ms (priv->duration_us));
+}
+
+/**
+ * gtd_timeline_skip:
+ * @timeline: A #GtdTimeline
+ * @msecs: Amount of time to skip
+ *
+ * Advance timeline by the requested time in milliseconds
+ */
+void
+gtd_timeline_skip (GtdTimeline *self,
+ guint msecs)
+{
+ GtdTimelinePrivate *priv;
+ gint64 us;
+
+ g_return_if_fail (GTD_IS_TIMELINE (self));
+
+ priv = gtd_timeline_get_instance_private (self);
+ us = ms_to_us (msecs);
+
+ if (priv->direction == GTD_TIMELINE_FORWARD)
+ {
+ priv->elapsed_time_us += us;
+
+ if (priv->elapsed_time_us > priv->duration_us)
+ priv->elapsed_time_us = 1;
+ }
+ else if (priv->direction == GTD_TIMELINE_BACKWARD)
+ {
+ priv->elapsed_time_us -= us;
+
+ if (priv->elapsed_time_us < 1)
+ priv->elapsed_time_us = priv->duration_us - 1;
+ }
+
+ priv->delta_us = 0;
+}
+
+/**
+ * gtd_timeline_advance:
+ * @timeline: A #GtdTimeline
+ * @msecs: Time to advance to
+ *
+ * Advance timeline to the requested point. The point is given as a
+ * time in milliseconds since the timeline started.
+ *
+ * The @timeline will not emit the #GtdTimeline::new-frame
+ * signal for the given time. The first ::new-frame signal after the call to
+ * gtd_timeline_advance() will be emit the skipped markers.
+ */
+void
+gtd_timeline_advance (GtdTimeline *self,
+ guint msecs)
+{
+ GtdTimelinePrivate *priv;
+ gint64 us;
+
+ g_return_if_fail (GTD_IS_TIMELINE (self));
+
+ priv = gtd_timeline_get_instance_private (self);
+ us = ms_to_us (msecs);
+
+ priv->elapsed_time_us = MIN (us, priv->duration_us);
+}
+
+/**
+ * gtd_timeline_get_elapsed_time:
+ * @timeline: A #GtdTimeline
+ *
+ * Request the current time position of the timeline.
+ *
+ * Return value: current elapsed time in milliseconds.
+ */
+guint
+gtd_timeline_get_elapsed_time (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv;
+
+ g_return_val_if_fail (GTD_IS_TIMELINE (self), 0);
+
+ priv = gtd_timeline_get_instance_private (self);
+ return us_to_ms (priv->elapsed_time_us);
+}
+
+/**
+ * gtd_timeline_is_playing:
+ * @timeline: A #GtdTimeline
+ *
+ * Queries state of a #GtdTimeline.
+ *
+ * Return value: %TRUE if timeline is currently playing
+ */
+gboolean
+gtd_timeline_is_playing (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv;
+
+ g_return_val_if_fail (GTD_IS_TIMELINE (self), FALSE);
+
+ priv = gtd_timeline_get_instance_private (self);
+ return priv->is_playing;
+}
+
+/**
+ * gtd_timeline_get_delay:
+ * @timeline: a #GtdTimeline
+ *
+ * Retrieves the delay set using gtd_timeline_set_delay().
+ *
+ * Return value: the delay in milliseconds.
+ */
+guint
+gtd_timeline_get_delay (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv;
+
+ g_return_val_if_fail (GTD_IS_TIMELINE (self), 0);
+
+ priv = gtd_timeline_get_instance_private (self);
+ return us_to_ms (priv->delay_us);
+}
+
+/**
+ * gtd_timeline_set_delay:
+ * @timeline: a #GtdTimeline
+ * @msecs: delay in milliseconds
+ *
+ * Sets the delay, in milliseconds, before @timeline should start.
+ */
+void
+gtd_timeline_set_delay (GtdTimeline *self,
+ guint msecs)
+{
+ GtdTimelinePrivate *priv;
+ gint64 us;
+
+ g_return_if_fail (GTD_IS_TIMELINE (self));
+
+ priv = gtd_timeline_get_instance_private (self);
+ us = ms_to_us (msecs);
+
+ if (priv->delay_us != us)
+ {
+ priv->delay_us = us;
+ g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_DELAY]);
+ }
+}
+
+/**
+ * gtd_timeline_get_duration:
+ * @timeline: a #GtdTimeline
+ *
+ * Retrieves the duration of a #GtdTimeline in milliseconds.
+ * See gtd_timeline_set_duration().
+ *
+ * Return value: the duration of the timeline, in milliseconds.
+ */
+guint
+gtd_timeline_get_duration (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv;
+
+ g_return_val_if_fail (GTD_IS_TIMELINE (self), 0);
+
+ priv = gtd_timeline_get_instance_private (self);
+
+ return us_to_ms (priv->duration_us);
+}
+
+/**
+ * gtd_timeline_set_duration:
+ * @timeline: a #GtdTimeline
+ * @msecs: duration of the timeline in milliseconds
+ *
+ * Sets the duration of the timeline, in milliseconds. The speed
+ * of the timeline depends on the GtdTimeline:fps setting.
+ */
+void
+gtd_timeline_set_duration (GtdTimeline *self,
+ guint msecs)
+{
+ GtdTimelinePrivate *priv;
+ gint64 us;
+
+ g_return_if_fail (GTD_IS_TIMELINE (self));
+ g_return_if_fail (msecs > 0);
+
+ priv = gtd_timeline_get_instance_private (self);
+ us = ms_to_us (msecs);
+
+ if (priv->duration_us != us)
+ {
+ priv->duration_us = us;
+
+ g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_DURATION]);
+ }
+}
+
+/**
+ * gtd_timeline_get_progress:
+ * @timeline: a #GtdTimeline
+ *
+ * The position of the timeline in a normalized [-1, 2] interval.
+ *
+ * The return value of this function is determined by the progress
+ * mode set using gtd_timeline_set_progress_mode(), or by the
+ * progress function set using gtd_timeline_set_progress_func().
+ *
+ * Return value: the normalized current position in the timeline.
+ */
+gdouble
+gtd_timeline_get_progress (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv;
+
+ g_return_val_if_fail (GTD_IS_TIMELINE (self), 0.0);
+
+ priv = gtd_timeline_get_instance_private (self);
+
+ return priv->progress_func (self,
+ (gdouble) priv->elapsed_time_us,
+ (gdouble) priv->duration_us,
+ priv->progress_data);
+}
+
+/**
+ * gtd_timeline_get_direction:
+ * @timeline: a #GtdTimeline
+ *
+ * Retrieves the direction of the timeline set with
+ * gtd_timeline_set_direction().
+ *
+ * Return value: the direction of the timeline
+ */
+GtdTimelineDirection
+gtd_timeline_get_direction (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv;
+
+ g_return_val_if_fail (GTD_IS_TIMELINE (self), GTD_TIMELINE_FORWARD);
+
+ priv = gtd_timeline_get_instance_private (self);
+ return priv->direction;
+}
+
+/**
+ * gtd_timeline_set_direction:
+ * @timeline: a #GtdTimeline
+ * @direction: the direction of the timeline
+ *
+ * Sets the direction of @timeline, either %GTD_TIMELINE_FORWARD or
+ * %GTD_TIMELINE_BACKWARD.
+ */
+void
+gtd_timeline_set_direction (GtdTimeline *self,
+ GtdTimelineDirection direction)
+{
+ GtdTimelinePrivate *priv;
+
+ g_return_if_fail (GTD_IS_TIMELINE (self));
+
+ priv = gtd_timeline_get_instance_private (self);
+
+ if (priv->direction != direction)
+ {
+ priv->direction = direction;
+
+ if (priv->elapsed_time_us == 0)
+ priv->elapsed_time_us = priv->duration_us;
+
+ g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_DIRECTION]);
+ }
+}
+
+/**
+ * gtd_timeline_get_delta:
+ * @timeline: a #GtdTimeline
+ *
+ * Retrieves the amount of time elapsed since the last
+ * GtdTimeline::new-frame signal.
+ *
+ * This function is only useful inside handlers for the ::new-frame
+ * signal, and its behaviour is undefined if the timeline is not
+ * playing.
+ *
+ * Return value: the amount of time in milliseconds elapsed since the
+ * last frame
+ */
+guint
+gtd_timeline_get_delta (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv;
+
+ g_return_val_if_fail (GTD_IS_TIMELINE (self), 0);
+
+ if (!gtd_timeline_is_playing (self))
+ return 0;
+
+ priv = gtd_timeline_get_instance_private (self);
+ return us_to_ms (priv->delta_us);
+}
+
+/**
+ * gtd_timeline_set_auto_reverse:
+ * @timeline: a #GtdTimeline
+ * @reverse: %TRUE if the @timeline should reverse the direction
+ *
+ * Sets whether @timeline should reverse the direction after the
+ * emission of the #GtdTimeline::completed signal.
+ *
+ * Setting the #GtdTimeline:auto-reverse property to %TRUE is the
+ * equivalent of connecting a callback to the #GtdTimeline::completed
+ * signal and changing the direction of the timeline from that callback;
+ * for instance, this code:
+ *
+ * |[
+ * static void
+ * reverse_timeline (GtdTimeline *self)
+ * {
+ * GtdTimelineDirection dir = gtd_timeline_get_direction (self);
+ *
+ * if (dir == GTD_TIMELINE_FORWARD)
+ * dir = GTD_TIMELINE_BACKWARD;
+ * else
+ * dir = GTD_TIMELINE_FORWARD;
+ *
+ * gtd_timeline_set_direction (self, dir);
+ * }
+ * ...
+ * timeline = gtd_timeline_new (1000);
+ * gtd_timeline_set_repeat_count (self, -1);
+ * g_signal_connect (self, "completed",
+ * G_CALLBACK (reverse_timeline),
+ * NULL);
+ * ]|
+ *
+ * can be effectively replaced by:
+ *
+ * |[
+ * timeline = gtd_timeline_new (1000);
+ * gtd_timeline_set_repeat_count (self, -1);
+ * gtd_timeline_set_auto_reverse (self);
+ * ]|
+ */
+void
+gtd_timeline_set_auto_reverse (GtdTimeline *self,
+ gboolean reverse)
+{
+ GtdTimelinePrivate *priv;
+
+ g_return_if_fail (GTD_IS_TIMELINE (self));
+
+ reverse = !!reverse;
+
+ priv = gtd_timeline_get_instance_private (self);
+
+ if (priv->auto_reverse != reverse)
+ {
+ priv->auto_reverse = reverse;
+ g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_AUTO_REVERSE]);
+ }
+}
+
+/**
+ * gtd_timeline_get_auto_reverse:
+ * @timeline: a #GtdTimeline
+ *
+ * Retrieves the value set by gtd_timeline_set_auto_reverse().
+ *
+ * Return value: %TRUE if the timeline should automatically reverse, and
+ * %FALSE otherwise
+ */
+gboolean
+gtd_timeline_get_auto_reverse (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv;
+
+ g_return_val_if_fail (GTD_IS_TIMELINE (self), FALSE);
+
+ priv = gtd_timeline_get_instance_private (self);
+ return priv->auto_reverse;
+}
+
+/**
+ * gtd_timeline_set_repeat_count:
+ * @timeline: a #GtdTimeline
+ * @count: the number of times the timeline should repeat
+ *
+ * Sets the number of times the @timeline should repeat.
+ *
+ * If @count is 0, the timeline never repeats.
+ *
+ * If @count is -1, the timeline will always repeat until
+ * it's stopped.
+ */
+void
+gtd_timeline_set_repeat_count (GtdTimeline *self,
+ gint count)
+{
+ GtdTimelinePrivate *priv;
+
+ g_return_if_fail (GTD_IS_TIMELINE (self));
+ g_return_if_fail (count >= -1);
+
+ priv = gtd_timeline_get_instance_private (self);
+
+ if (priv->repeat_count != count)
+ {
+ priv->repeat_count = count;
+ g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_REPEAT_COUNT]);
+ }
+}
+
+/**
+ * gtd_timeline_get_repeat_count:
+ * @timeline: a #GtdTimeline
+ *
+ * Retrieves the number set using gtd_timeline_set_repeat_count().
+ *
+ * Return value: the number of repeats
+ */
+gint
+gtd_timeline_get_repeat_count (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv;
+
+ g_return_val_if_fail (GTD_IS_TIMELINE (self), 0);
+
+ priv = gtd_timeline_get_instance_private (self);
+ return priv->repeat_count;
+}
+
+/**
+ * gtd_timeline_set_progress_func:
+ * @timeline: a #GtdTimeline
+ * @func: (scope notified) (allow-none): a progress function, or %NULL
+ * @data: (closure): data to pass to @func
+ * @notify: a function to be called when the progress function is removed
+ * or the timeline is disposed
+ *
+ * Sets a custom progress function for @timeline. The progress function will
+ * be called by gtd_timeline_get_progress() and will be used to compute
+ * the progress value based on the elapsed time and the total duration of the
+ * timeline.
+ *
+ * If @func is not %NULL, the #GtdTimeline:progress-mode property will
+ * be set to %GTD_CUSTOM_MODE.
+ *
+ * If @func is %NULL, any previously set progress function will be unset, and
+ * the #GtdTimeline:progress-mode property will be set to %GTD_EASE_LINEAR.
+ */
+void
+gtd_timeline_set_progress_func (GtdTimeline *self,
+ GtdTimelineProgressFunc func,
+ gpointer data,
+ GDestroyNotify notify)
+{
+ GtdTimelinePrivate *priv;
+
+ g_return_if_fail (GTD_IS_TIMELINE (self));
+
+ priv = gtd_timeline_get_instance_private (self);
+
+ if (priv->progress_notify != NULL)
+ priv->progress_notify (priv->progress_data);
+
+ priv->progress_func = func;
+ priv->progress_data = data;
+ priv->progress_notify = notify;
+
+ if (priv->progress_func != NULL)
+ priv->progress_mode = GTD_CUSTOM_MODE;
+ else
+ priv->progress_mode = GTD_EASE_LINEAR;
+
+ g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_PROGRESS_MODE]);
+}
+
+/**
+ * gtd_timeline_set_progress_mode:
+ * @timeline: a #GtdTimeline
+ * @mode: the progress mode, as a #GtdEaseMode
+ *
+ * Sets the progress function using a value from the #GtdEaseMode
+ * enumeration. The @mode cannot be %GTD_CUSTOM_MODE or bigger than
+ * %GTD_ANIMATION_LAST.
+ */
+void
+gtd_timeline_set_progress_mode (GtdTimeline *self,
+ GtdEaseMode mode)
+{
+ GtdTimelinePrivate *priv;
+
+ g_return_if_fail (GTD_IS_TIMELINE (self));
+ g_return_if_fail (mode < GTD_EASE_LAST);
+ g_return_if_fail (mode != GTD_CUSTOM_MODE);
+
+ priv = gtd_timeline_get_instance_private (self);
+
+ if (priv->progress_mode == mode)
+ return;
+
+ if (priv->progress_notify != NULL)
+ priv->progress_notify (priv->progress_data);
+
+ priv->progress_mode = mode;
+ priv->progress_func = timeline_progress_func;
+ priv->progress_data = NULL;
+ priv->progress_notify = NULL;
+
+ g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_PROGRESS_MODE]);
+}
+
+/**
+ * gtd_timeline_get_progress_mode:
+ * @timeline: a #GtdTimeline
+ *
+ * Retrieves the progress mode set using gtd_timeline_set_progress_mode()
+ * or gtd_timeline_set_progress_func().
+ *
+ * Return value: a #GtdEaseMode
+ */
+GtdEaseMode
+gtd_timeline_get_progress_mode (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv;
+
+ g_return_val_if_fail (GTD_IS_TIMELINE (self), GTD_EASE_LINEAR);
+
+ priv = gtd_timeline_get_instance_private (self);
+ return priv->progress_mode;
+}
+
+/**
+ * gtd_timeline_get_duration_hint:
+ * @timeline: a #GtdTimeline
+ *
+ * Retrieves the full duration of the @timeline, taking into account the
+ * current value of the #GtdTimeline:repeat-count property.
+ *
+ * If the #GtdTimeline:repeat-count property is set to -1, this function
+ * will return %G_MAXINT64.
+ *
+ * The returned value is to be considered a hint, and it's only valid
+ * as long as the @timeline hasn't been changed.
+ *
+ * Return value: the full duration of the #GtdTimeline
+ */
+gint64
+gtd_timeline_get_duration_hint (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv;
+ gint64 duration_ms;
+
+ g_return_val_if_fail (GTD_IS_TIMELINE (self), 0);
+
+ priv = gtd_timeline_get_instance_private (self);
+ duration_ms = us_to_ms (priv->duration_us);
+
+ if (priv->repeat_count == 0)
+ return duration_ms;
+ else if (priv->repeat_count < 0)
+ return G_MAXINT64;
+ else
+ return priv->repeat_count * duration_ms;
+}
+
+/**
+ * gtd_timeline_get_current_repeat:
+ * @timeline: a #GtdTimeline
+ *
+ * Retrieves the current repeat for a timeline.
+ *
+ * Repeats start at 0.
+ *
+ * Return value: the current repeat
+ */
+gint
+gtd_timeline_get_current_repeat (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv;
+
+ g_return_val_if_fail (GTD_IS_TIMELINE (self), 0);
+
+ priv = gtd_timeline_get_instance_private (self);
+ return priv->current_repeat;
+}
+
+/**
+ * gtd_timeline_get_widget:
+ * @timeline: a #GtdTimeline
+ *
+ * Get the widget the timeline is associated with.
+ *
+ * Returns: (transfer none): the associated #GtdWidget
+ */
+GtdWidget *
+gtd_timeline_get_widget (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+
+ return priv->widget;
+}