diff options
Diffstat (limited to 'src/gui/gtd-widget.c')
| -rw-r--r-- | src/gui/gtd-widget.c | 1668 |
1 files changed, 1668 insertions, 0 deletions
diff --git a/src/gui/gtd-widget.c b/src/gui/gtd-widget.c new file mode 100644 index 0000000..e5bece4 --- /dev/null +++ b/src/gui/gtd-widget.c @@ -0,0 +1,1668 @@ +/* gtd-widget.c + * + * Copyright 2018-2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.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 + */ + +#include "gtd-animatable.h" +#include "gtd-bin-layout.h" +#include "gtd-debug.h" +#include "gtd-animation-enums.h" +#include "gtd-interval.h" +#include "gtd-timeline.h" +#include "gtd-property-transition.h" + +#include <graphene-gobject.h> +#include <gobject/gvaluecollector.h> + +enum +{ + X, + Y, + Z, +}; + +typedef struct +{ + guint easing_duration; + guint easing_delay; + GtdEaseMode easing_mode; +} AnimationState; + +typedef struct +{ + struct { + GHashTable *transitions; + GArray *states; + AnimationState *current_state; + } animation; + + graphene_point3d_t pivot_point; + gfloat rotation[3]; + gfloat scale[3]; + graphene_point3d_t translation; + + GskTransform *cached_transform; +} GtdWidgetPrivate; + +static void set_animatable_property (GtdWidget *self, + guint prop_id, + const GValue *value, + GParamSpec *pspec); + +static void gtd_animatable_iface_init (GtdAnimatableInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (GtdWidget, gtd_widget, GTK_TYPE_WIDGET, + G_ADD_PRIVATE (GtdWidget) + G_IMPLEMENT_INTERFACE (GTD_TYPE_ANIMATABLE, gtd_animatable_iface_init)) + +enum +{ + PROP_0, + PROP_PIVOT_POINT, + PROP_ROTATION_X, + PROP_ROTATION_Y, + PROP_ROTATION_Z, + PROP_SCALE_X, + PROP_SCALE_Y, + PROP_SCALE_Z, + PROP_TRANSLATION_X, + PROP_TRANSLATION_Y, + PROP_TRANSLATION_Z, + N_PROPS +}; + +enum +{ + TRANSITION_STOPPED, + TRANSITIONS_COMPLETED, + NUM_SIGNALS +}; + + +static guint signals[NUM_SIGNALS] = { 0, }; + +static GParamSpec *properties [N_PROPS] = { NULL, }; + + +/* + * Auxiliary methods + */ + +typedef struct +{ + GtdWidget *widget; + GtdTransition *transition; + gchar *name; + gulong completed_id; +} TransitionClosure; + +static void +transition_closure_free (gpointer data) +{ + if (G_LIKELY (data != NULL)) + { + TransitionClosure *closure = data; + GtdTimeline *timeline; + + timeline = GTD_TIMELINE (closure->transition); + + /* we disconnect the signal handler before stopping the timeline, + * so that we don't end up inside on_transition_stopped() from + * a call to g_hash_table_remove(). + */ + g_clear_signal_handler (&closure->completed_id, closure->transition); + + if (gtd_timeline_is_playing (timeline)) + gtd_timeline_stop (timeline); + + g_object_unref (closure->transition); + + g_free (closure->name); + + g_slice_free (TransitionClosure, closure); + } +} + +static void +on_transition_stopped_cb (GtdTransition *transition, + gboolean is_finished, + TransitionClosure *closure) +{ + GtdWidget *self = closure->widget; + GtdWidgetPrivate *priv = gtd_widget_get_instance_private (self); + GQuark t_quark; + gchar *t_name; + + if (closure->name == NULL) + return; + + /* we need copies because we emit the signal after the + * TransitionClosure data structure has been freed + */ + t_quark = g_quark_from_string (closure->name); + t_name = g_strdup (closure->name); + + if (gtd_transition_get_remove_on_complete (transition)) + { + /* this is safe, because the timeline has now stopped, + * so we won't recurse; the reference on the Animatable + * will be dropped by the ::stopped signal closure in + * GtdTransition, which is RUN_LAST, and thus will + * be called after this handler + */ + g_hash_table_remove (priv->animation.transitions, closure->name); + } + + /* we emit the ::transition-stopped after removing the + * transition, so that we can chain up new transitions + * without interfering with the one that just finished + */ + g_signal_emit (self, signals[TRANSITION_STOPPED], t_quark, t_name, is_finished); + + g_free (t_name); + + /* if it's the last transition then we clean up */ + if (g_hash_table_size (priv->animation.transitions) == 0) + { + g_hash_table_unref (priv->animation.transitions); + priv->animation.transitions = NULL; + + GTD_TRACE_MSG ("[animation] Transitions for '%p' completed", self); + + g_signal_emit (self, signals[TRANSITIONS_COMPLETED], 0); + } +} + +static void +add_transition_to_widget (GtdWidget *self, + const gchar *name, + GtdTransition *transition) +{ + GtdWidgetPrivate *priv = gtd_widget_get_instance_private (self); + TransitionClosure *closure; + GtdTimeline *timeline; + + GTD_ENTRY; + + if (!priv->animation.transitions) + { + priv->animation.transitions = g_hash_table_new_full (g_str_hash, + g_str_equal, + NULL, + transition_closure_free); + } + + if (g_hash_table_lookup (priv->animation.transitions, name) != NULL) + { + g_warning ("A transition with name '%s' already exists for the widget '%p'", + name, + self); + GTD_RETURN (); + } + + gtd_transition_set_animatable (transition, GTD_ANIMATABLE (self)); + + timeline = GTD_TIMELINE (transition); + + closure = g_slice_new (TransitionClosure); + closure->widget = self; + closure->transition = g_object_ref (transition); + closure->name = g_strdup (name); + closure->completed_id = g_signal_connect (timeline, + "stopped", + G_CALLBACK (on_transition_stopped_cb), + closure); + + GTD_TRACE_MSG ("[animation] Adding transition '%s' [%p] to widget %p", + closure->name, + closure->transition, + self); + + g_hash_table_insert (priv->animation.transitions, closure->name, closure); + gtd_timeline_start (timeline); + + GTD_EXIT; +} + +static gboolean +should_skip_implicit_transition (GtdWidget *self, + GParamSpec *pspec) +{ + GtdWidgetPrivate *priv = gtd_widget_get_instance_private (self); + + /* if the easing state has a non-zero duration we always want an + * implicit transition to occur + */ + if (priv->animation.current_state->easing_duration == 0) + return TRUE; + + /* if the widget is not mapped and is not part of a branch of the scene + * graph that is being cloned, then we always skip implicit transitions + * on the account of the fact that the widget is not going to be visible + * when those transitions happen + */ + if (!gtk_widget_get_mapped (GTK_WIDGET (self))) + return TRUE; + + return FALSE; +} + +static GtdTransition* +create_transition (GtdWidget *self, + GParamSpec *pspec, + ...) +{ + GtdWidgetPrivate *priv = gtd_widget_get_instance_private (self); + g_autofree gchar *error = NULL; + g_auto (GValue) initial = G_VALUE_INIT; + g_auto (GValue) final = G_VALUE_INIT; + TransitionClosure *closure; + GtdTimeline *timeline; + GtdInterval *interval; + GtdTransition *res = NULL; + va_list var_args; + GType ptype; + + g_assert (pspec != NULL); + + if (!priv->animation.transitions) + { + priv->animation.transitions = g_hash_table_new_full (g_str_hash, + g_str_equal, + NULL, + transition_closure_free); + } + + va_start (var_args, pspec); + + ptype = G_PARAM_SPEC_VALUE_TYPE (pspec); + + G_VALUE_COLLECT_INIT (&initial, ptype, var_args, 0, &error); + if (error != NULL) + { + g_critical ("%s: %s", G_STRLOC, error); + goto out; + } + + G_VALUE_COLLECT_INIT (&final, ptype, var_args, 0, &error); + if (error != NULL) + { + g_critical ("%s: %s", G_STRLOC, error); + goto out; + } + + if (should_skip_implicit_transition (self, pspec)) + { + GTD_TRACE_MSG ("[animation] Skipping implicit transition for '%p::%s'", + self, + pspec->name); + + /* remove a transition, if one exists */ + gtd_widget_remove_transition (self, pspec->name); + + /* we don't go through the Animatable interface because we + * already know we got here through an animatable property. + */ + set_animatable_property (self, pspec->param_id, &final, pspec); + + goto out; + } + + closure = g_hash_table_lookup (priv->animation.transitions, pspec->name); + if (closure == NULL) + { + res = gtd_property_transition_new (pspec->name); + + gtd_transition_set_remove_on_complete (res, TRUE); + + interval = gtd_interval_new_with_values (ptype, &initial, &final); + gtd_transition_set_interval (res, interval); + + timeline = GTD_TIMELINE (res); + gtd_timeline_set_delay (timeline, priv->animation.current_state->easing_delay); + gtd_timeline_set_duration (timeline, priv->animation.current_state->easing_duration); + gtd_timeline_set_progress_mode (timeline, priv->animation.current_state->easing_mode); + + /* this will start the transition as well */ + add_transition_to_widget (self, pspec->name, res); + + /* the widget now owns the transition */ + g_object_unref (res); + } + else + { + GtdEaseMode cur_mode; + guint cur_duration; + + GTD_TRACE_MSG ("[animation] Existing transition for %p:%s", + self, + pspec->name); + + timeline = GTD_TIMELINE (closure->transition); + + cur_duration = gtd_timeline_get_duration (timeline); + if (cur_duration != priv->animation.current_state->easing_duration) + gtd_timeline_set_duration (timeline, priv->animation.current_state->easing_duration); + + cur_mode = gtd_timeline_get_progress_mode (timeline); + if (cur_mode != priv->animation.current_state->easing_mode) + gtd_timeline_set_progress_mode (timeline, priv->animation.current_state->easing_mode); + + gtd_timeline_rewind (timeline); + + interval = gtd_transition_get_interval (closure->transition); + gtd_interval_set_initial_value (interval, &initial); + gtd_interval_set_final_value (interval, &final); + + res = closure->transition; + } + +out: + va_end (var_args); + + return res; +} + +static void +invalidate_cached_transform (GtdWidget *self) +{ + GtdWidgetPrivate *priv = gtd_widget_get_instance_private (self); + + g_clear_pointer (&priv->cached_transform, gsk_transform_unref); +} + +static void +calculate_transform (GtdWidget *self) +{ + GtdWidgetPrivate *priv = gtd_widget_get_instance_private (self); + graphene_point3d_t pivot; + GskTransform *transform; + gboolean pivot_is_zero; + gint height; + gint width; + + transform = NULL; + width = gtk_widget_get_width (GTK_WIDGET (self)); + height = gtk_widget_get_height (GTK_WIDGET (self)); + + /* Pivot point */ + pivot_is_zero = graphene_point3d_equal (&priv->pivot_point, graphene_point3d_zero ()); + pivot = GRAPHENE_POINT3D_INIT (width * priv->pivot_point.x, + height * priv->pivot_point.y, + priv->pivot_point.z); + if (!pivot_is_zero) + transform = gsk_transform_translate_3d (transform, &pivot); + + /* Perspective */ + transform = gsk_transform_perspective (transform, 2 * MAX (width, height)); + + /* Translation */ + if (G_APPROX_VALUE (priv->translation.z, 0.f, FLT_EPSILON)) + { + transform = gsk_transform_translate (transform, + &GRAPHENE_POINT_INIT (priv->translation.x, + priv->translation.y)); + } + else + { + transform = gsk_transform_translate_3d (transform, &priv->translation); + } + + /* Scale */ + if (G_APPROX_VALUE (priv->scale[Z], 1.f, FLT_EPSILON)) + transform = gsk_transform_scale (transform, priv->scale[X], priv->scale[Y]); + else + transform = gsk_transform_scale_3d (transform, priv->scale[X], priv->scale[Y], priv->scale[Z]); + + /* Rotation */ + transform = gsk_transform_rotate_3d (transform, priv->rotation[X], graphene_vec3_x_axis ()); + transform = gsk_transform_rotate_3d (transform, priv->rotation[Y], graphene_vec3_y_axis ()); + transform = gsk_transform_rotate_3d (transform, priv->rotation[Z], graphene_vec3_z_axis ()); + + /* Rollback pivot point */ + if (!pivot_is_zero) + transform = gsk_transform_translate_3d (transform, + &GRAPHENE_POINT3D_INIT (-pivot.x, + -pivot.y, + -pivot.z)); + + priv->cached_transform = transform; +} + +static void +set_rotation_internal (GtdWidget *self, + gfloat rotation_x, + gfloat rotation_y, + gfloat rotation_z) +{ + GtdWidgetPrivate *priv; + gboolean changed[3]; + + GTD_ENTRY; + + g_return_if_fail (GTD_IS_WIDGET (self)); + + priv = gtd_widget_get_instance_private (self); + + changed[X] = !G_APPROX_VALUE (priv->rotation[X], rotation_x, FLT_EPSILON); + changed[Y] = !G_APPROX_VALUE (priv->rotation[Y], rotation_y, FLT_EPSILON); + changed[Z] = !G_APPROX_VALUE (priv->rotation[Z], rotation_z, FLT_EPSILON); + + if (!changed[X] && !changed[Y] && !changed[Z]) + GTD_RETURN (); + + invalidate_cached_transform (self); + + priv->rotation[X] = rotation_x; + priv->rotation[Y] = rotation_y; + priv->rotation[Z] = rotation_z; + + if (changed[X]) + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ROTATION_X]); + + if (changed[Y]) + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ROTATION_Y]); + + if (changed[Z]) + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ROTATION_Z]); + + gtk_widget_queue_resize (GTK_WIDGET (self)); + + GTD_EXIT; +} + +static void +set_scale_internal (GtdWidget *self, + gfloat scale_x, + gfloat scale_y, + gfloat scale_z) +{ + GtdWidgetPrivate *priv; + gboolean changed[3]; + + GTD_ENTRY; + + g_return_if_fail (GTD_IS_WIDGET (self)); + + priv = gtd_widget_get_instance_private (self); + + changed[X] = !G_APPROX_VALUE (priv->scale[X], scale_x, FLT_EPSILON); + changed[Y] = !G_APPROX_VALUE (priv->scale[Y], scale_y, FLT_EPSILON); + changed[Z] = !G_APPROX_VALUE (priv->scale[Z], scale_z, FLT_EPSILON); + + if (!changed[X] && !changed[Y] && !changed[Z]) + GTD_RETURN (); + + invalidate_cached_transform (self); + + priv->scale[X] = scale_x; + priv->scale[Y] = scale_y; + priv->scale[Z] = scale_z; + + if (changed[X]) + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SCALE_X]); + + if (changed[Y]) + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SCALE_Y]); + + if (changed[Z]) + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SCALE_Z]); + + gtk_widget_queue_resize (GTK_WIDGET (self)); + + GTD_EXIT; +} + +static void +set_translation_internal (GtdWidget *self, + gfloat translation_x, + gfloat translation_y, + gfloat translation_z) +{ + graphene_point3d_t old_translation, translation; + GtdWidgetPrivate *priv; + + GTD_ENTRY; + + g_return_if_fail (GTD_IS_WIDGET (self)); + + priv = gtd_widget_get_instance_private (self); + translation = GRAPHENE_POINT3D_INIT (translation_x, translation_y, translation_z); + + if (graphene_point3d_equal (&priv->translation, &translation)) + GTD_RETURN (); + + old_translation = priv->translation; + + invalidate_cached_transform (self); + priv->translation = translation; + + if (!G_APPROX_VALUE (old_translation.x, translation.x, FLT_EPSILON)) + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_TRANSLATION_X]); + + if (!G_APPROX_VALUE (old_translation.y, translation.y, FLT_EPSILON)) + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_TRANSLATION_Y]); + + if (!G_APPROX_VALUE (old_translation.y, translation.y, FLT_EPSILON)) + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_TRANSLATION_Z]); + + gtk_widget_queue_resize (GTK_WIDGET (self)); + + GTD_EXIT; +} + +static void +set_animatable_property (GtdWidget *self, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GtdWidgetPrivate *priv = gtd_widget_get_instance_private (self); + GObject *object = G_OBJECT (self); + + g_object_freeze_notify (object); + + switch (prop_id) + { + case PROP_ROTATION_X: + set_rotation_internal (self, g_value_get_float (value), priv->rotation[Y], priv->rotation[Z]); + break; + + case PROP_ROTATION_Y: + set_rotation_internal (self, priv->rotation[X], g_value_get_float (value), priv->rotation[Z]); + break; + + case PROP_ROTATION_Z: + set_rotation_internal (self, priv->rotation[X], priv->rotation[Y], g_value_get_float (value)); + break; + + case PROP_SCALE_X: + set_scale_internal (self, g_value_get_float (value), priv->scale[Y], priv->scale[Z]); + break; + + case PROP_SCALE_Y: + set_scale_internal (self, priv->scale[X], g_value_get_float (value), priv->scale[Z]); + break; + + case PROP_SCALE_Z: + set_scale_internal (self, priv->scale[X], priv->scale[Y], g_value_get_float (value)); + break; + + case PROP_TRANSLATION_X: + set_translation_internal (self, g_value_get_float (value), priv->translation.y, priv->translation.z); + break; + + case PROP_TRANSLATION_Y: + set_translation_internal (self, priv->translation.x, g_value_get_float (value), priv->translation.z); + break; + + case PROP_TRANSLATION_Z: + set_translation_internal (self, priv->translation.x, priv->translation.y, g_value_get_float (value)); + break; + + default: + g_object_set_property (object, pspec->name, value); + break; + } + + g_object_thaw_notify (object); +} + +/* + * GtdAnimatable interface + */ + +static GParamSpec * +gtd_widget_find_property (GtdAnimatable *animatable, + const gchar *property_name) +{ + return g_object_class_find_property (G_OBJECT_GET_CLASS (animatable), property_name); +} + +static void +gtd_widget_get_initial_state (GtdAnimatable *animatable, + const gchar *property_name, + GValue *initial) +{ + g_object_get_property (G_OBJECT (animatable), property_name, initial); +} + +static void +gtd_widget_set_final_state (GtdAnimatable *animatable, + const gchar *property_name, + const GValue *final) +{ + GObjectClass *obj_class = G_OBJECT_GET_CLASS (animatable); + GParamSpec *pspec; + + pspec = g_object_class_find_property (obj_class, property_name); + + if (pspec) + set_animatable_property (GTD_WIDGET (animatable), pspec->param_id, final, pspec); +} + +static GtdWidget* +gtd_widget_get_widget (GtdAnimatable *animatable) +{ + return GTD_WIDGET (animatable); +} + +static void +gtd_animatable_iface_init (GtdAnimatableInterface *iface) +{ + iface->find_property = gtd_widget_find_property; + iface->get_initial_state = gtd_widget_get_initial_state; + iface->set_final_state = gtd_widget_set_final_state; + iface->get_widget = gtd_widget_get_widget; +} + + +/* + * GObject overrides + */ + +static void +gtd_widget_dispose (GObject *object) +{ + GtdWidget *self = GTD_WIDGET (object); + GtkWidget *child = gtk_widget_get_first_child (GTK_WIDGET (object)); + + while (child) + { + GtkWidget *next = gtk_widget_get_next_sibling (child); + + gtk_widget_unparent (child); + child = next; + } + + invalidate_cached_transform (self); + gtd_widget_remove_all_transitions (self); + + G_OBJECT_CLASS (gtd_widget_parent_class)->dispose (object); +} + +static void +gtd_widget_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdWidget *self = GTD_WIDGET (object); + GtdWidgetPrivate *priv = gtd_widget_get_instance_private (self); + + switch (prop_id) + { + case PROP_PIVOT_POINT: + g_value_set_boxed (value, &priv->pivot_point); + break; + + case PROP_ROTATION_X: + g_value_set_float (value, priv->rotation[X]); + break; + + case PROP_ROTATION_Y: + g_value_set_float (value, priv->rotation[Y]); + break; + + case PROP_ROTATION_Z: + g_value_set_float (value, priv->rotation[Z]); + break; + + case PROP_SCALE_X: + g_value_set_float (value, priv->scale[X]); + break; + + case PROP_SCALE_Y: + g_value_set_float (value, priv->scale[Y]); + break; + + case PROP_SCALE_Z: + g_value_set_float (value, priv->scale[Z]); + break; + + case PROP_TRANSLATION_X: + g_value_set_float (value, priv->translation.x); + break; + + case PROP_TRANSLATION_Y: + g_value_set_float (value, priv->translation.y); + break; + + case PROP_TRANSLATION_Z: + g_value_set_float (value, priv->translation.z); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_widget_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GtdWidget *self = GTD_WIDGET (object); + GtdWidgetPrivate *priv = gtd_widget_get_instance_private (self); + + switch (prop_id) + { + case PROP_PIVOT_POINT: + gtd_widget_set_pivot_point (self, g_value_get_boxed (value)); + break; + + case PROP_ROTATION_X: + gtd_widget_set_rotation (self, g_value_get_float (value), priv->rotation[Y], priv->rotation[Z]); + break; + + case PROP_ROTATION_Y: + gtd_widget_set_rotation (self, priv->rotation[X], g_value_get_float (value), priv->rotation[Z]); + break; + + case PROP_ROTATION_Z: + gtd_widget_set_rotation (self, priv->rotation[X], priv->rotation[Y], g_value_get_float (value)); + break; + + case PROP_SCALE_X: + gtd_widget_set_scale (self, g_value_get_float (value), priv->scale[Y], priv->scale[Z]); + break; + + case PROP_SCALE_Y: + gtd_widget_set_scale (self, priv->scale[X], g_value_get_float (value), priv->scale[Z]); + break; + + case PROP_SCALE_Z: + gtd_widget_set_scale (self, priv->scale[X], priv->scale[Y], g_value_get_float (value)); + break; + + case PROP_TRANSLATION_X: + gtd_widget_set_translation (self, g_value_get_float (value), priv->translation.y, priv->translation.z); + break; + + case PROP_TRANSLATION_Y: + gtd_widget_set_translation (self, priv->translation.x, g_value_get_float (value), priv->translation.z); + break; + + case PROP_TRANSLATION_Z: + gtd_widget_set_translation (self, priv->translation.x, priv->translation.y, g_value_get_float (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_widget_class_init (GtdWidgetClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = gtd_widget_dispose; + object_class->get_property = gtd_widget_get_property; + object_class->set_property = gtd_widget_set_property; + + /** + * GtdWidget: + */ + properties[PROP_PIVOT_POINT] = g_param_spec_boxed ("pivot-point", + "Pivot point", + "Pivot point", + GRAPHENE_TYPE_POINT3D, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * GtdWidget:rotation-x + */ + properties[PROP_ROTATION_X] = g_param_spec_float ("rotation-x", + "Rotation in the X axis", + "Rotation in the X axis", + -G_MAXFLOAT, + G_MAXFLOAT, + 0.f, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * GtdWidget:rotation-y + */ + properties[PROP_ROTATION_Y] = g_param_spec_float ("rotation-y", + "Rotation in the Y axis", + "Rotation in the Y axis", + -G_MAXFLOAT, + G_MAXFLOAT, + 0.f, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * GtdWidget:rotation-z + */ + properties[PROP_ROTATION_Z] = g_param_spec_float ("rotation-z", + "Rotation in the Z axis", + "Rotation in the Z axis", + -G_MAXFLOAT, + G_MAXFLOAT, + 0.f, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * GtdWidget:scale-x + */ + properties[PROP_SCALE_X] = g_param_spec_float ("scale-x", + "Scale in the X axis", + "Scale in the X axis", + -G_MAXFLOAT, + G_MAXFLOAT, + 1.f, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * GtdWidget:scale-y + */ + properties[PROP_SCALE_Y] = g_param_spec_float ("scale-y", + "Scale in the Y axis", + "Scale in the Y axis", + -G_MAXFLOAT, + G_MAXFLOAT, + 1.f, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * GtdWidget:scale-z + */ + properties[PROP_SCALE_Z] = g_param_spec_float ("scale-z", + "Scale in the Z axis", + "Scale in the Z axis", + -G_MAXFLOAT, + G_MAXFLOAT, + 1.f, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * GtdWidget:translation-x + */ + properties[PROP_TRANSLATION_X] = g_param_spec_float ("translation-x", + "Translation in the X axis", + "Translation in the X axis", + -G_MAXFLOAT, + G_MAXFLOAT, + 0.f, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * GtdWidget:translation-y + */ + properties[PROP_TRANSLATION_Y] = g_param_spec_float ("translation-y", + "Translation in the Y axis", + "Translation in the Y axis", + -G_MAXFLOAT, + G_MAXFLOAT, + 0.f, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * GtdWidget:translation-z + */ + properties[PROP_TRANSLATION_Z] = g_param_spec_float ("translation-z", + "Translation in the Z axis", + "Translation in the Z axis", + -G_MAXFLOAT, + G_MAXFLOAT, + 0.f, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + /** + * GtdWidget::transitions-completed: + * @actor: a #GtdWidget + * + * The ::transitions-completed signal is emitted once all transitions + * involving @actor are complete. + * + * Since: 1.10 + */ + signals[TRANSITIONS_COMPLETED] = + g_signal_new ("transitions-completed", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 0); + + /** + * GtdWidget::transition-stopped: + * @actor: a #GtdWidget + * @name: the name of the transition + * @is_finished: whether the transition was finished, or stopped + * + * The ::transition-stopped signal is emitted once a transition + * is stopped; a transition is stopped once it reached its total + * duration (including eventual repeats), it has been stopped + * using gtd_timeline_stop(), or it has been removed from the + * transitions applied on @actor, using gtd_actor_remove_transition(). + * + * Since: 1.12 + */ + signals[TRANSITION_STOPPED] = + g_signal_new ("transition-stopped", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | + G_SIGNAL_NO_HOOKS | G_SIGNAL_DETAILED, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 2, + G_TYPE_STRING, + G_TYPE_BOOLEAN); + + gtk_widget_class_set_layout_manager_type (widget_class, GTD_TYPE_BIN_LAYOUT); +} + +static void +gtd_widget_init (GtdWidget *self) +{ + GtdWidgetPrivate *priv = gtd_widget_get_instance_private (self); + + priv->scale[X] = 1.f; + priv->scale[Y] = 1.f; + priv->scale[Z] = 1.f; + + priv->pivot_point = GRAPHENE_POINT3D_INIT (0.5, 0.5, 0.f); + + gtd_widget_save_easing_state (self); + gtd_widget_set_easing_duration (self, 0); +} + +GtkWidget* +gtd_widget_new (void) +{ + return g_object_new (GTD_TYPE_WIDGET, NULL); +} + +/** + */ +void +gtd_widget_get_pivot_point (GtdWidget *self, + graphene_point3d_t *out_pivot_point) +{ + GtdWidgetPrivate *priv; + + g_return_if_fail (GTD_IS_WIDGET (self)); + g_return_if_fail (out_pivot_point != NULL); + + priv = gtd_widget_get_instance_private (self); + *out_pivot_point = priv->pivot_point; +} + +/** + */ +void +gtd_widget_set_pivot_point (GtdWidget *self, + const graphene_point3d_t *pivot_point) +{ + GtdWidgetPrivate *priv; + + GTD_ENTRY; + + g_return_if_fail (GTD_IS_WIDGET (self)); + g_return_if_fail (pivot_point != NULL); + g_return_if_fail (pivot_point->x >= 0.f && pivot_point->x <= 1.0); + g_return_if_fail (pivot_point->y >= 0.f && pivot_point->y <= 1.0); + + priv = gtd_widget_get_instance_private (self); + + if (graphene_point3d_equal (&priv->pivot_point, pivot_point)) + GTD_RETURN (); + + invalidate_cached_transform (self); + priv->pivot_point = *pivot_point; + + gtk_widget_queue_resize (GTK_WIDGET (self)); + + GTD_EXIT; +} + +/** + */ +void +gtd_widget_get_rotation (GtdWidget *self, + gfloat *rotation_x, + gfloat *rotation_y, + gfloat *rotation_z) +{ + GtdWidgetPrivate *priv; + + g_return_if_fail (GTD_IS_WIDGET (self)); + + priv = gtd_widget_get_instance_private (self); + + if (rotation_x) + *rotation_x = priv->rotation[X]; + + if (rotation_y) + *rotation_y = priv->rotation[Y]; + + if (rotation_z) + *rotation_z = priv->rotation[Z]; +} + +/** + */ +void +gtd_widget_set_rotation (GtdWidget *self, + gfloat rotation_x, + gfloat rotation_y, + gfloat rotation_z) +{ + GtdWidgetPrivate *priv; + gboolean changed[3]; + + GTD_ENTRY; + + g_return_if_fail (GTD_IS_WIDGET (self)); + + priv = gtd_widget_get_instance_private (self); + + changed[X] = !G_APPROX_VALUE (priv->rotation[X], rotation_x, FLT_EPSILON); + changed[Y] = !G_APPROX_VALUE (priv->rotation[Y], rotation_y, FLT_EPSILON); + changed[Z] = !G_APPROX_VALUE (priv->rotation[Z], rotation_z, FLT_EPSILON); + + if (!changed[X] && !changed[Y] && !changed[Z]) + GTD_RETURN (); + + if (changed[X]) + create_transition (self, properties[PROP_ROTATION_X], priv->rotation[X], rotation_x); + + if (changed[Y]) + create_transition (self, properties[PROP_ROTATION_Y], priv->rotation[Y], rotation_y); + + if (changed[Z]) + create_transition (self, properties[PROP_ROTATION_Z], priv->rotation[Z], rotation_z); + + GTD_EXIT; +} + +/** + */ +void +gtd_widget_get_scale (GtdWidget *self, + gfloat *scale_x, + gfloat *scale_y, + gfloat *scale_z) +{ + GtdWidgetPrivate *priv; + + g_return_if_fail (GTD_IS_WIDGET (self)); + + priv = gtd_widget_get_instance_private (self); + + if (scale_x) + *scale_x = priv->scale[0]; + + if (scale_y) + *scale_y = priv->scale[1]; + + if (scale_z) + *scale_z = priv->scale[2]; +} + +/** + */ +void +gtd_widget_set_scale (GtdWidget *self, + gfloat scale_x, + gfloat scale_y, + gfloat scale_z) +{ + GtdWidgetPrivate *priv; + gboolean changed[3]; + + GTD_ENTRY; + + g_return_if_fail (GTD_IS_WIDGET (self)); + + priv = gtd_widget_get_instance_private (self); + + changed[X] = !G_APPROX_VALUE (priv->scale[X], scale_x, FLT_EPSILON); + changed[Y] = !G_APPROX_VALUE (priv->scale[Y], scale_y, FLT_EPSILON); + changed[Z] = !G_APPROX_VALUE (priv->scale[Z], scale_z, FLT_EPSILON); + + if (!changed[X] && !changed[Y] && !changed[Z]) + GTD_RETURN (); + + if (changed[X]) + create_transition (self, properties[PROP_SCALE_X], priv->scale[X], scale_x); + + if (changed[Y]) + create_transition (self, properties[PROP_SCALE_Y], priv->scale[Y], scale_y); + + if (changed[Z]) + create_transition (self, properties[PROP_SCALE_Z], priv->scale[Z], scale_z); + + GTD_EXIT; +} + +/** + */ +void +gtd_widget_get_translation (GtdWidget *self, + gfloat *translation_x, + gfloat *translation_y, + gfloat *translation_z) +{ + GtdWidgetPrivate *priv; + + g_return_if_fail (GTD_IS_WIDGET (self)); + + priv = gtd_widget_get_instance_private (self); + + if (translation_x) + *translation_x = priv->translation.x; + + if (translation_y) + *translation_y = priv->translation.y; + + if (translation_z) + *translation_z = priv->translation.z; +} + +/** + */ +void +gtd_widget_set_translation (GtdWidget *self, + gfloat translation_x, + gfloat translation_y, + gfloat translation_z) +{ + graphene_point3d_t translation; + GtdWidgetPrivate *priv; + + GTD_ENTRY; + + g_return_if_fail (GTD_IS_WIDGET (self)); + + priv = gtd_widget_get_instance_private (self); + translation = GRAPHENE_POINT3D_INIT (translation_x, translation_y, translation_z); + + if (graphene_point3d_equal (&priv->translation, &translation)) + GTD_RETURN (); + + if (!G_APPROX_VALUE (priv->translation.x, translation.x, FLT_EPSILON)) + create_transition (self, properties[PROP_TRANSLATION_X], priv->translation.x, translation_x); + + if (!G_APPROX_VALUE (priv->translation.y, translation.y, FLT_EPSILON)) + create_transition (self, properties[PROP_TRANSLATION_Y], priv->translation.y, translation_y); + + if (!G_APPROX_VALUE (priv->translation.y, translation.y, FLT_EPSILON)) + create_transition (self, properties[PROP_TRANSLATION_Z], priv->translation.z, translation_z); + + GTD_EXIT; +} + +/** + */ +GskTransform* +gtd_widget_apply_transform (GtdWidget *self, + GskTransform *transform) +{ + GtdWidgetPrivate *priv; + + g_return_val_if_fail (GTD_IS_WIDGET (self), NULL); + + priv = gtd_widget_get_instance_private (self); + + if (!priv->cached_transform) + calculate_transform (self); + + if (!transform) + return gsk_transform_ref (priv->cached_transform); + + return gsk_transform_transform (transform, priv->cached_transform); +} + +/** + * gtd_widget_add_transition: + * @self: a #GtdWidget + * @name: the name of the transition to add + * @transition: the #GtdTransition to add + * + * Adds a @transition to the #GtdWidget's list of animations. + * + * The @name string is a per-widget unique identifier of the @transition: only + * one #GtdTransition can be associated to the specified @name. + * + * The @transition will be started once added. + * + * This function will take a reference on the @transition. + * + * This function is usually called implicitly when modifying an animatable + * property. + * + * Since: 1.10 + */ +void +gtd_widget_add_transition (GtdWidget *self, + const gchar *name, + GtdTransition *transition) +{ + g_return_if_fail (GTD_IS_WIDGET (self)); + g_return_if_fail (name != NULL); + g_return_if_fail (GTD_IS_TRANSITION (transition)); + + add_transition_to_widget (self, name, transition); +} + +/** + * gtd_widget_remove_transition: + * @self: a #GtdWidget + * @name: the name of the transition to remove + * + * Removes the transition stored inside a #GtdWidget using @name + * identifier. + * + * If the transition is currently in progress, it will be stopped. + * + * This function releases the reference acquired when the transition + * was added to the #GtdWidget. + * + * Since: 1.10 + */ +void +gtd_widget_remove_transition (GtdWidget *self, + const gchar *name) +{ + GtdWidgetPrivate *priv; + TransitionClosure *closure; + gboolean was_playing; + GQuark t_quark; + gchar *t_name; + + g_return_if_fail (GTD_IS_WIDGET (self)); + g_return_if_fail (name != NULL); + + priv = gtd_widget_get_instance_private (self); + + if (priv->animation.transitions == NULL) + return; + + closure = g_hash_table_lookup (priv->animation.transitions, name); + if (closure == NULL) + return; + + was_playing = + gtd_timeline_is_playing (GTD_TIMELINE (closure->transition)); + t_quark = g_quark_from_string (closure->name); + t_name = g_strdup (closure->name); + + g_hash_table_remove (priv->animation.transitions, name); + + /* we want to maintain the invariant that ::transition-stopped is + * emitted after the transition has been removed, to allow replacing + * or chaining; removing the transition from the hash table will + * stop it, but transition_closure_free() will disconnect the signal + * handler we install in add_transition_internal(), to avoid loops + * or segfaults. + * + * since we know already that a transition will stop once it's removed + * from an widget, we can simply emit the ::transition-stopped here + * ourselves, if the timeline was playing (if it wasn't, then the + * signal was already emitted at least once). + */ + if (was_playing) + g_signal_emit (self, signals[TRANSITION_STOPPED], t_quark, t_name, FALSE); + + g_free (t_name); +} + +/** + * gtd_widget_remove_all_transitions: + * @self: a #GtdWidget + * + * Removes all transitions associated to @self. + * + * Since: 1.10 + */ +void +gtd_widget_remove_all_transitions (GtdWidget *self) +{ + GtdWidgetPrivate *priv; + + g_return_if_fail (GTD_IS_WIDGET (self)); + + priv = gtd_widget_get_instance_private (self); + if (priv->animation.transitions == NULL) + return; + + g_hash_table_remove_all (priv->animation.transitions); +} + +/** + * gtd_widget_set_easing_duration: + * @self: a #GtdWidget + * @msecs: the duration of the easing, or %NULL + * + * Sets the duration of the tweening for animatable properties + * of @self for the current easing state. + * + * Since: 1.10 + */ +void +gtd_widget_set_easing_duration (GtdWidget *self, + guint msecs) +{ + GtdWidgetPrivate *priv; + + g_return_if_fail (GTD_IS_WIDGET (self)); + + priv = gtd_widget_get_instance_private (self); + + if (priv->animation.current_state == NULL) + { + g_warning ("You must call gtd_widget_save_easing_state() prior " + "to calling gtd_widget_set_easing_duration()."); + return; + } + + if (priv->animation.current_state->easing_duration != msecs) + priv->animation.current_state->easing_duration = msecs; +} + +/** + * gtd_widget_get_easing_duration: + * @self: a #GtdWidget + * + * Retrieves the duration of the tweening for animatable + * properties of @self for the current easing state. + * + * Return value: the duration of the tweening, in milliseconds + * + * Since: 1.10 + */ +guint +gtd_widget_get_easing_duration (GtdWidget *self) +{ + GtdWidgetPrivate *priv; + + g_return_val_if_fail (GTD_IS_WIDGET (self), 0); + + priv = gtd_widget_get_instance_private (self); + if (priv->animation.current_state != NULL) + return priv->animation.current_state->easing_duration; + + return 0; +} + +/** + * gtd_widget_set_easing_mode: + * @self: a #GtdWidget + * @mode: an easing mode, excluding %GTD_CUSTOM_MODE + * + * Sets the easing mode for the tweening of animatable properties + * of @self. + * + * Since: 1.10 + */ +void +gtd_widget_set_easing_mode (GtdWidget *self, + GtdEaseMode mode) +{ + GtdWidgetPrivate *priv; + + g_return_if_fail (GTD_IS_WIDGET (self)); + g_return_if_fail (mode != GTD_CUSTOM_MODE); + g_return_if_fail (mode < GTD_EASE_LAST); + + priv = gtd_widget_get_instance_private (self); + if (priv->animation.current_state == NULL) + { + g_warning ("You must call gtd_widget_save_easing_state() prior " + "to calling gtd_widget_set_easing_mode()."); + return; + } + + if (priv->animation.current_state->easing_mode != mode) + priv->animation.current_state->easing_mode = mode; +} + +/** + * gtd_widget_get_easing_mode: + * @self: a #GtdWidget + * + * Retrieves the easing mode for the tweening of animatable properties + * of @self for the current easing state. + * + * Return value: an easing mode + * + * Since: 1.10 + */ +GtdEaseMode +gtd_widget_get_easing_mode (GtdWidget *self) +{ + GtdWidgetPrivate *priv; + + g_return_val_if_fail (GTD_IS_WIDGET (self), GTD_EASE_OUT_CUBIC); + + priv = gtd_widget_get_instance_private (self); + + if (priv->animation.current_state != NULL) + return priv->animation.current_state->easing_mode; + + return GTD_EASE_OUT_CUBIC; +} + +/** + * gtd_widget_set_easing_delay: + * @self: a #GtdWidget + * @msecs: the delay before the start of the tweening, in milliseconds + * + * Sets the delay that should be applied before tweening animatable + * properties. + * + * Since: 1.10 + */ +void +gtd_widget_set_easing_delay (GtdWidget *self, + guint msecs) +{ + GtdWidgetPrivate *priv; + + g_return_if_fail (GTD_IS_WIDGET (self)); + + priv = gtd_widget_get_instance_private (self); + + if (priv->animation.current_state == NULL) + { + g_warning ("You must call gtd_widget_save_easing_state() prior " + "to calling gtd_widget_set_easing_delay()."); + return; + } + + if (priv->animation.current_state->easing_delay != msecs) + priv->animation.current_state->easing_delay = msecs; +} + +/** + * gtd_widget_get_easing_delay: + * @self: a #GtdWidget + * + * Retrieves the delay that should be applied when tweening animatable + * properties. + * + * Return value: a delay, in milliseconds + * + * Since: 1.10 + */ +guint +gtd_widget_get_easing_delay (GtdWidget *self) +{ + GtdWidgetPrivate *priv; + + g_return_val_if_fail (GTD_IS_WIDGET (self), 0); + + priv = gtd_widget_get_instance_private (self); + + if (priv->animation.current_state != NULL) + return priv->animation.current_state->easing_delay; + + return 0; +} + +/** + * gtd_widget_get_transition: + * @self: a #GtdWidget + * @name: the name of the transition + * + * Retrieves the #GtdTransition of a #GtdWidget by using the + * transition @name. + * + * Transitions created for animatable properties use the name of the + * property itself, for instance the code below: + * + * |[<!-- language="C" --> + * gtd_widget_set_easing_duration (widget, 1000); + * gtd_widget_set_rotation_angle (widget, GTD_Y_AXIS, 360.0); + * + * transition = gtd_widget_get_transition (widget, "rotation-angle-y"); + * g_signal_connect (transition, "stopped", + * G_CALLBACK (on_transition_stopped), + * widget); + * ]| + * + * will call the `on_transition_stopped` callback when the transition + * is finished. + * + * If you just want to get notifications of the completion of a transition, + * you should use the #GtdWidget::transition-stopped signal, using the + * transition name as the signal detail. + * + * Return value: (transfer none): a #GtdTransition, or %NULL is none + * was found to match the passed name; the returned instance is owned + * by Gtd and it should not be freed + */ +GtdTransition * +gtd_widget_get_transition (GtdWidget *self, + const gchar *name) +{ + TransitionClosure *closure; + GtdWidgetPrivate *priv; + + g_return_val_if_fail (GTD_IS_WIDGET (self), NULL); + g_return_val_if_fail (name != NULL, NULL); + + priv = gtd_widget_get_instance_private (self); + if (priv->animation.transitions == NULL) + return NULL; + + closure = g_hash_table_lookup (priv->animation.transitions, name); + if (closure == NULL) + return NULL; + + return closure->transition; +} + +/** + * gtd_widget_has_transitions: (skip) + */ +gboolean +gtd_widget_has_transitions (GtdWidget *self) +{ + GtdWidgetPrivate *priv; + + g_return_val_if_fail (GTD_IS_WIDGET (self), FALSE); + + priv = gtd_widget_get_instance_private (self); + if (priv->animation.transitions == NULL) + return FALSE; + + return g_hash_table_size (priv->animation.transitions) > 0; +} + +/** + * gtd_widget_save_easing_state: + * @self: a #GtdWidget + * + * Saves the current easing state for animatable properties, and creates + * a new state with the default values for easing mode and duration. + * + * New transitions created after calling this function will inherit the + * duration, easing mode, and delay of the new easing state; this also + * applies to transitions modified in flight. + */ +void +gtd_widget_save_easing_state (GtdWidget *self) +{ + GtdWidgetPrivate *priv; + AnimationState new_state; + + g_return_if_fail (GTD_IS_WIDGET (self)); + + priv = gtd_widget_get_instance_private (self); + + if (priv->animation.states == NULL) + priv->animation.states = g_array_new (FALSE, FALSE, sizeof (AnimationState)); + + new_state.easing_mode = GTD_EASE_OUT_CUBIC; + new_state.easing_duration = 250; + new_state.easing_delay = 0; + + g_array_append_val (priv->animation.states, new_state); + + priv->animation.current_state = &g_array_index (priv->animation.states, + AnimationState, + priv->animation.states->len - 1); +} + +/** + * gtd_widget_restore_easing_state: + * @self: a #GtdWidget + * + * Restores the easing state as it was prior to a call to + * gtd_widget_save_easing_state(). + * + * Since: 1.10 + */ +void +gtd_widget_restore_easing_state (GtdWidget *self) +{ + GtdWidgetPrivate *priv; + + g_return_if_fail (GTD_IS_WIDGET (self)); + + priv = gtd_widget_get_instance_private (self); + + if (priv->animation.states == NULL) + { + g_critical ("The function gtd_widget_restore_easing_state() has " + "been called without a previous call to " + "gtd_widget_save_easing_state()."); + return; + } + + g_array_remove_index (priv->animation.states, priv->animation.states->len - 1); + + if (priv->animation.states->len > 0) + priv->animation.current_state = &g_array_index (priv->animation.states, AnimationState, priv->animation.states->len - 1); + else + { + g_array_unref (priv->animation.states); + priv->animation.states = NULL; + priv->animation.current_state = NULL; + } +} |
