1 /* dzl-animation.c
2  *
3  * Copyright (C) 2010-2016 Christian Hergert <christian@hergert.me>
4  *
5  * This file is free software; you can redistribute it and/or modify it under
6  * the terms of the GNU Lesser General Public License as published by the Free
7  * Software Foundation; either version 2.1 of the License, or (at your option)
8  * any later version.
9  *
10  * This file is distributed in the hope that it will be useful, but WITHOUT
11  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
13  * License for more details.
14  *
15  * You should have received a copy of the GNU General Public License along
16  * with this program.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 
19 #define G_LOG_DOMAIN "dzl-animation"
20 
21 #include "config.h"
22 
23 #include <glib/gi18n.h>
24 #include <gobject/gvaluecollector.h>
25 #include <gdk/gdk.h>
26 #include <gtk/gtk.h>
27 #include <stdlib.h>
28 #include <string.h>
29 
30 #include "dzl-animation.h"
31 #include "dzl-frame-source.h"
32 
33 #define FALLBACK_FRAME_RATE 60
34 
35 typedef gdouble (*AlphaFunc) (gdouble       offset);
36 typedef void    (*TweenFunc) (const GValue *begin,
37                               const GValue *end,
38                               GValue       *value,
39                               gdouble       offset);
40 
41 typedef struct
42 {
43   gboolean    is_child;  /* Does GParamSpec belong to parent widget */
44   GParamSpec *pspec;     /* GParamSpec of target property */
45   GValue      begin;     /* Begin value in animation */
46   GValue      end;       /* End value in animation */
47 } Tween;
48 
49 
50 struct _DzlAnimation
51 {
52   GInitiallyUnowned  parent_instance;
53 
54   gpointer           target;              /* Target object to animate */
55   gint64             begin_time;          /* Time in which animation started */
56   gint64             end_time;            /* Deadline for the animation */
57   guint              duration_msec;       /* Duration in milliseconds */
58   guint              mode;                /* Tween mode */
59   gulong             tween_handler;       /* GSource or signal handler */
60   gulong             after_paint_handler; /* signal handler */
61   gdouble            last_offset;         /* Track our last offset */
62   GArray            *tweens;              /* Array of tweens to perform */
63   GdkFrameClock     *frame_clock;         /* An optional frame-clock for sync. */
64   GDestroyNotify     notify;              /* Notify callback */
65   gpointer           notify_data;         /* Data for notify */
66   guint              stop_called : 1;
67 };
68 
69 G_DEFINE_TYPE (DzlAnimation, dzl_animation, G_TYPE_INITIALLY_UNOWNED)
70 
71 enum {
72   PROP_0,
73   PROP_DURATION,
74   PROP_FRAME_CLOCK,
75   PROP_MODE,
76   PROP_TARGET,
77   LAST_PROP
78 };
79 
80 
81 enum {
82   TICK,
83   LAST_SIGNAL
84 };
85 
86 
87 /*
88  * Helper macros.
89  */
90 #define LAST_FUNDAMENTAL 64
91 #define TWEEN(type)                                       \
92   static void                                             \
93   tween_ ## type (const GValue * begin,                   \
94                   const GValue * end,                     \
95                   GValue * value,                         \
96                   gdouble offset)                         \
97   {                                                       \
98     g ## type x = g_value_get_ ## type (begin);           \
99     g ## type y = g_value_get_ ## type (end);             \
100     g_value_set_ ## type (value, x + ((y - x) * offset)); \
101   }
102 
103 
104 /*
105  * Globals.
106  */
107 static AlphaFunc   alpha_funcs[DZL_ANIMATION_LAST];
108 static gboolean    debug;
109 static GParamSpec *properties[LAST_PROP];
110 static guint       signals[LAST_SIGNAL];
111 static TweenFunc   tween_funcs[LAST_FUNDAMENTAL];
112 static guint       slow_down_factor = 1;
113 
114 
115 /*
116  * Tweeners for basic types.
117  */
118 TWEEN (int);
119 TWEEN (uint);
120 TWEEN (long);
121 TWEEN (ulong);
122 TWEEN (float);
123 TWEEN (double);
124 
125 
126 /**
127  * dzl_animation_alpha_ease_in_cubic:
128  * @offset: (in): The position within the animation; 0.0 to 1.0.
129  *
130  * An alpha function to transform the offset within the animation.
131  * @DZL_ANIMATION_CUBIC means the valu ewill be transformed into
132  * cubic acceleration (x * x * x).
133  */
134 static gdouble
dzl_animation_alpha_ease_in_cubic(gdouble offset)135 dzl_animation_alpha_ease_in_cubic (gdouble offset)
136 {
137   return offset * offset * offset;
138 }
139 
140 
141 static gdouble
dzl_animation_alpha_ease_out_cubic(gdouble offset)142 dzl_animation_alpha_ease_out_cubic (gdouble offset)
143 {
144   gdouble p = offset - 1.0;
145 
146   return p * p * p + 1.0;
147 }
148 
149 static gdouble
dzl_animation_alpha_ease_in_out_cubic(gdouble offset)150 dzl_animation_alpha_ease_in_out_cubic (gdouble offset)
151 {
152   gdouble p = offset * 2.0;
153 
154   if (p < 1.0)
155     return 0.5 * p * p * p;
156   p -= 2.0;
157   return 0.5 * (p * p * p + 2.0);
158 }
159 
160 
161 /**
162  * dzl_animation_alpha_linear:
163  * @offset: (in): The position within the animation; 0.0 to 1.0.
164  *
165  * An alpha function to transform the offset within the animation.
166  * @DZL_ANIMATION_LINEAR means no tranformation will be made.
167  *
168  * Returns: @offset.
169  * Side effects: None.
170  */
171 static gdouble
dzl_animation_alpha_linear(gdouble offset)172 dzl_animation_alpha_linear (gdouble offset)
173 {
174   return offset;
175 }
176 
177 
178 /**
179  * dzl_animation_alpha_ease_in_quad:
180  * @offset: (in): The position within the animation; 0.0 to 1.0.
181  *
182  * An alpha function to transform the offset within the animation.
183  * @DZL_ANIMATION_EASE_IN_QUAD means that the value will be transformed
184  * into a quadratic acceleration.
185  *
186  * Returns: A tranformation of @offset.
187  * Side effects: None.
188  */
189 static gdouble
dzl_animation_alpha_ease_in_quad(gdouble offset)190 dzl_animation_alpha_ease_in_quad (gdouble offset)
191 {
192   return offset * offset;
193 }
194 
195 
196 /**
197  * dzl_animation_alpha_ease_out_quad:
198  * @offset: (in): The position within the animation; 0.0 to 1.0.
199  *
200  * An alpha function to transform the offset within the animation.
201  * @DZL_ANIMATION_EASE_OUT_QUAD means that the value will be transformed
202  * into a quadratic deceleration.
203  *
204  * Returns: A tranformation of @offset.
205  * Side effects: None.
206  */
207 static gdouble
dzl_animation_alpha_ease_out_quad(gdouble offset)208 dzl_animation_alpha_ease_out_quad (gdouble offset)
209 {
210   return -1.0 * offset * (offset - 2.0);
211 }
212 
213 
214 /**
215  * dzl_animation_alpha_ease_in_out_quad:
216  * @offset: (in): The position within the animation; 0.0 to 1.0.
217  *
218  * An alpha function to transform the offset within the animation.
219  * @DZL_ANIMATION_EASE_IN_OUT_QUAD means that the value will be transformed
220  * into a quadratic acceleration for the first half, and quadratic
221  * deceleration the second half.
222  *
223  * Returns: A tranformation of @offset.
224  * Side effects: None.
225  */
226 static gdouble
dzl_animation_alpha_ease_in_out_quad(gdouble offset)227 dzl_animation_alpha_ease_in_out_quad (gdouble offset)
228 {
229   offset *= 2.0;
230   if (offset < 1.0)
231     return 0.5 * offset * offset;
232   offset -= 1.0;
233   return -0.5 * (offset * (offset - 2.0) - 1.0);
234 }
235 
236 
237 /**
238  * dzl_animation_load_begin_values:
239  * @animation: (in): A #DzlAnimation.
240  *
241  * Load the begin values for all the properties we are about to
242  * animate.
243  *
244  * Side effects: None.
245  */
246 static void
dzl_animation_load_begin_values(DzlAnimation * animation)247 dzl_animation_load_begin_values (DzlAnimation *animation)
248 {
249   GtkContainer *container;
250   Tween *tween;
251   guint i;
252 
253   g_assert (DZL_IS_ANIMATION (animation));
254 
255   for (i = 0; i < animation->tweens->len; i++)
256     {
257       tween = &g_array_index (animation->tweens, Tween, i);
258       g_value_reset (&tween->begin);
259       if (tween->is_child)
260         {
261           container = GTK_CONTAINER (gtk_widget_get_parent (animation->target));
262           gtk_container_child_get_property (container,
263                                             animation->target,
264                                             tween->pspec->name,
265                                             &tween->begin);
266         }
267       else
268         {
269           g_object_get_property (animation->target,
270                                  tween->pspec->name,
271                                  &tween->begin);
272         }
273     }
274 }
275 
276 
277 /**
278  * dzl_animation_unload_begin_values:
279  * @animation: (in): A #DzlAnimation.
280  *
281  * Unloads the begin values for the animation. This might be particularly
282  * useful once we support pointer types.
283  *
284  * Side effects: None.
285  */
286 static void
dzl_animation_unload_begin_values(DzlAnimation * animation)287 dzl_animation_unload_begin_values (DzlAnimation *animation)
288 {
289   Tween *tween;
290   guint i;
291 
292   g_assert (DZL_IS_ANIMATION (animation));
293 
294   for (i = 0; i < animation->tweens->len; i++)
295     {
296       tween = &g_array_index (animation->tweens, Tween, i);
297       g_value_reset (&tween->begin);
298     }
299 }
300 
301 
302 /**
303  * dzl_animation_get_offset:
304  * @animation: A #DzlAnimation.
305  * @frame_time: the time to present the frame, or 0 for current timing.
306  *
307  * Retrieves the position within the animation from 0.0 to 1.0. This
308  * value is calculated using the msec of the beginning of the animation
309  * and the current time.
310  *
311  * Returns: The offset of the animation from 0.0 to 1.0.
312  */
313 static gdouble
dzl_animation_get_offset(DzlAnimation * animation,gint64 frame_time)314 dzl_animation_get_offset (DzlAnimation *animation,
315                           gint64        frame_time)
316 {
317   g_assert (DZL_IS_ANIMATION (animation));
318 
319   if (frame_time == 0)
320     {
321       if (animation->frame_clock != NULL)
322         frame_time = gdk_frame_clock_get_frame_time (animation->frame_clock);
323       else
324         frame_time = g_get_monotonic_time ();
325     }
326 
327   frame_time = CLAMP (frame_time, animation->begin_time, animation->end_time);
328 
329   /* Check end_time first in case end_time == begin_time */
330   if (frame_time == animation->end_time)
331     return 1.0;
332   else if (frame_time == animation->begin_time)
333     return 0.0;
334 
335   return (frame_time - animation->begin_time) / (gdouble)(animation->duration_msec * 1000L);
336 }
337 
338 
339 /**
340  * dzl_animation_update_property:
341  * @animation: (in): A #DzlAnimation.
342  * @target: (in): A #GObject.
343  * @tween: (in): a #Tween containing the property.
344  * @value: (in): The new value for the property.
345  *
346  * Updates the value of a property on an object using @value.
347  *
348  * Side effects: The property of @target is updated.
349  */
350 static void
dzl_animation_update_property(DzlAnimation * animation,gpointer target,Tween * tween,const GValue * value)351 dzl_animation_update_property (DzlAnimation  *animation,
352                               gpointer      target,
353                               Tween        *tween,
354                               const GValue *value)
355 {
356   g_assert (DZL_IS_ANIMATION (animation));
357   g_assert (G_IS_OBJECT (target));
358   g_assert (tween);
359   g_assert (value);
360 
361   g_object_set_property (target, tween->pspec->name, value);
362 }
363 
364 
365 /**
366  * dzl_animation_update_child_property:
367  * @animation: (in): A #DzlAnimation.
368  * @target: (in): A #GObject.
369  * @tween: (in): A #Tween containing the property.
370  * @value: (in): The new value for the property.
371  *
372  * Updates the value of the parent widget of the target to @value.
373  *
374  * Side effects: The property of @target<!-- -->'s parent widget is updated.
375  */
376 static void
dzl_animation_update_child_property(DzlAnimation * animation,gpointer target,Tween * tween,const GValue * value)377 dzl_animation_update_child_property (DzlAnimation *animation,
378                                      gpointer      target,
379                                      Tween        *tween,
380                                      const GValue *value)
381 {
382   GtkWidget *parent;
383 
384   g_assert (DZL_IS_ANIMATION (animation));
385   g_assert (G_IS_OBJECT (target));
386   g_assert (tween);
387   g_assert (value);
388 
389   parent = gtk_widget_get_parent (GTK_WIDGET (target));
390   gtk_container_child_set_property (GTK_CONTAINER (parent),
391                                     target,
392                                     tween->pspec->name,
393                                     value);
394 }
395 
396 
397 /**
398  * dzl_animation_get_value_at_offset:
399  * @animation: (in): A #DzlAnimation.
400  * @offset: (in): The offset in the animation from 0.0 to 1.0.
401  * @tween: (in): A #Tween containing the property.
402  * @value: (out): A #GValue in which to store the property.
403  *
404  * Retrieves a value for a particular position within the animation.
405  *
406  * Side effects: None.
407  */
408 static void
dzl_animation_get_value_at_offset(DzlAnimation * animation,gdouble offset,Tween * tween,GValue * value)409 dzl_animation_get_value_at_offset (DzlAnimation *animation,
410                                    gdouble       offset,
411                                    Tween        *tween,
412                                    GValue       *value)
413 {
414   g_assert (DZL_IS_ANIMATION (animation));
415   g_assert (tween != NULL);
416   g_assert (value != NULL);
417   g_assert (value->g_type == tween->pspec->value_type);
418 
419   if (value->g_type < LAST_FUNDAMENTAL)
420     {
421       /*
422        * If you hit the following assertion, you need to add a function
423        * to create the new value at the given offset.
424        */
425       g_assert (tween_funcs[value->g_type]);
426       tween_funcs[value->g_type](&tween->begin, &tween->end, value, offset);
427     }
428   else
429     {
430       /*
431        * TODO: Support complex transitions.
432        */
433       if (offset >= 1.0)
434         g_value_copy (&tween->end, value);
435     }
436 }
437 
438 static void
dzl_animation_set_frame_clock(DzlAnimation * animation,GdkFrameClock * frame_clock)439 dzl_animation_set_frame_clock (DzlAnimation  *animation,
440                                GdkFrameClock *frame_clock)
441 {
442   if (animation->frame_clock != frame_clock)
443     {
444       g_clear_object (&animation->frame_clock);
445       animation->frame_clock = frame_clock ? g_object_ref (frame_clock) : NULL;
446     }
447 }
448 
449 static void
dzl_animation_set_target(DzlAnimation * animation,gpointer target)450 dzl_animation_set_target (DzlAnimation *animation,
451                           gpointer      target)
452 {
453   g_assert (!animation->target);
454 
455   animation->target = g_object_ref (target);
456 
457   if (GTK_IS_WIDGET (animation->target))
458     dzl_animation_set_frame_clock (animation,
459                                   gtk_widget_get_frame_clock (animation->target));
460 }
461 
462 
463 /**
464  * dzl_animation_tick:
465  * @animation: (in): A #DzlAnimation.
466  *
467  * Moves the object properties to the next position in the animation.
468  *
469  * Returns: %TRUE if the animation has not completed; otherwise %FALSE.
470  * Side effects: None.
471  */
472 static gboolean
dzl_animation_tick(DzlAnimation * animation,gdouble offset)473 dzl_animation_tick (DzlAnimation *animation,
474                     gdouble       offset)
475 {
476   gdouble alpha;
477   GValue value = { 0 };
478   Tween *tween;
479   guint i;
480 
481   g_assert (DZL_IS_ANIMATION (animation));
482 
483   if (offset == animation->last_offset)
484     return offset < 1.0;
485 
486   alpha = alpha_funcs[animation->mode](offset);
487 
488   /*
489    * Update property values.
490    */
491   for (i = 0; i < animation->tweens->len; i++)
492     {
493       tween = &g_array_index (animation->tweens, Tween, i);
494       g_value_init (&value, tween->pspec->value_type);
495       dzl_animation_get_value_at_offset (animation, alpha, tween, &value);
496       if (!tween->is_child)
497         {
498           dzl_animation_update_property (animation,
499                                         animation->target,
500                                         tween,
501                                         &value);
502         }
503       else
504         {
505           dzl_animation_update_child_property (animation,
506                                               animation->target,
507                                               tween,
508                                               &value);
509         }
510       g_value_unset (&value);
511     }
512 
513   /*
514    * Notify anyone interested in the tick signal.
515    */
516   g_signal_emit (animation, signals[TICK], 0);
517 
518   /*
519    * Flush any outstanding events to the graphics server (in the case of X).
520    */
521 #if !GTK_CHECK_VERSION (3, 13, 0)
522   if (GTK_IS_WIDGET (animation->target))
523     {
524       GdkWindow *window;
525 
526       if ((window = gtk_widget_get_window (GTK_WIDGET (animation->target))))
527         gdk_window_flush (window);
528     }
529 #endif
530 
531   animation->last_offset = offset;
532 
533   return offset < 1.0;
534 }
535 
536 
537 /**
538  * dzl_animation_timeout_cb:
539  * @user_data: (in): A #DzlAnimation.
540  *
541  * Timeout from the main loop to move to the next step of the animation.
542  *
543  * Returns: %TRUE until the animation has completed; otherwise %FALSE.
544  * Side effects: None.
545  */
546 static gboolean
dzl_animation_timeout_cb(gpointer user_data)547 dzl_animation_timeout_cb (gpointer user_data)
548 {
549   DzlAnimation *animation = user_data;
550   gboolean ret;
551   gdouble offset;
552 
553   offset = dzl_animation_get_offset (animation, 0);
554 
555   if (!(ret = dzl_animation_tick (animation, offset)))
556     dzl_animation_stop (animation);
557 
558   return ret;
559 }
560 
561 
562 static gboolean
dzl_animation_widget_tick_cb(GdkFrameClock * frame_clock,DzlAnimation * animation)563 dzl_animation_widget_tick_cb (GdkFrameClock *frame_clock,
564                               DzlAnimation  *animation)
565 {
566   gboolean ret = G_SOURCE_REMOVE;
567 
568   g_assert (GDK_IS_FRAME_CLOCK (frame_clock));
569   g_assert (DZL_IS_ANIMATION (animation));
570 
571   if (animation->tween_handler)
572     {
573       gdouble offset;
574 
575       offset = dzl_animation_get_offset (animation, 0);
576 
577       if (!(ret = dzl_animation_tick (animation, offset)))
578         dzl_animation_stop (animation);
579     }
580 
581   return ret;
582 }
583 
584 
585 static void
dzl_animation_widget_after_paint_cb(GdkFrameClock * frame_clock,DzlAnimation * animation)586 dzl_animation_widget_after_paint_cb (GdkFrameClock *frame_clock,
587                                      DzlAnimation  *animation)
588 {
589   gint64 base_time;
590   gint64 interval;
591   gint64 next_frame_time;
592   gdouble offset;
593 
594   g_assert (GDK_IS_FRAME_CLOCK (frame_clock));
595   g_assert (DZL_IS_ANIMATION (animation));
596 
597   base_time = gdk_frame_clock_get_frame_time (frame_clock);
598   gdk_frame_clock_get_refresh_info (frame_clock, base_time, &interval, &next_frame_time);
599 
600   offset = dzl_animation_get_offset (animation, next_frame_time);
601 
602   dzl_animation_tick (animation, offset);
603 }
604 
605 
606 /**
607  * dzl_animation_start:
608  * @animation: (in): A #DzlAnimation.
609  *
610  * Start the animation. When the animation stops, the internal reference will
611  * be dropped and the animation may be finalized.
612  *
613  * Side effects: None.
614  */
615 void
dzl_animation_start(DzlAnimation * animation)616 dzl_animation_start (DzlAnimation *animation)
617 {
618   g_return_if_fail (DZL_IS_ANIMATION (animation));
619   g_return_if_fail (!animation->tween_handler);
620 
621   g_object_ref_sink (animation);
622   dzl_animation_load_begin_values (animation);
623 
624   /*
625    * We want the real current time instead of the GdkFrameClocks current time
626    * because if the clock was asleep, it could be innaccurate.
627    */
628 
629   if (animation->frame_clock)
630     {
631       animation->begin_time = gdk_frame_clock_get_frame_time (animation->frame_clock);
632       animation->end_time = animation->begin_time + (animation->duration_msec * 1000L);
633       animation->tween_handler =
634         g_signal_connect_object (animation->frame_clock,
635                                  "update",
636                                  G_CALLBACK (dzl_animation_widget_tick_cb),
637                                  animation,
638                                  0);
639       animation->after_paint_handler =
640         g_signal_connect_object (animation->frame_clock,
641                                  "after-paint",
642                                  G_CALLBACK (dzl_animation_widget_after_paint_cb),
643                                  animation,
644                                  0);
645       gdk_frame_clock_begin_updating (animation->frame_clock);
646     }
647   else
648     {
649       animation->begin_time = g_get_monotonic_time ();
650       animation->end_time = animation->begin_time + (animation->duration_msec * 1000L);
651       animation->tween_handler = dzl_frame_source_add (FALLBACK_FRAME_RATE,
652                                                        dzl_animation_timeout_cb,
653                                                        animation);
654     }
655 }
656 
657 
658 static void
dzl_animation_notify(DzlAnimation * self)659 dzl_animation_notify (DzlAnimation *self)
660 {
661   g_assert (DZL_IS_ANIMATION (self));
662 
663   if (self->notify != NULL)
664     {
665       GDestroyNotify notify = self->notify;
666       gpointer data = self->notify_data;
667 
668       self->notify = NULL;
669       self->notify_data = NULL;
670 
671       notify (data);
672     }
673 }
674 
675 
676 /**
677  * dzl_animation_stop:
678  * @animation: (nullable): A #DzlAnimation.
679  *
680  * Stops a running animation. The internal reference to the animation is
681  * dropped and therefore may cause the object to finalize.
682  *
683  * As a convenience, this function accepts %NULL for @animation but
684  * does nothing if that should occur.
685  */
686 void
dzl_animation_stop(DzlAnimation * animation)687 dzl_animation_stop (DzlAnimation *animation)
688 {
689   if (animation == NULL)
690     return;
691 
692   g_return_if_fail (DZL_IS_ANIMATION (animation));
693 
694   if (animation->stop_called)
695     return;
696 
697   animation->stop_called = TRUE;
698 
699   if (animation->tween_handler)
700     {
701       if (animation->frame_clock)
702         {
703           gdk_frame_clock_end_updating (animation->frame_clock);
704           g_signal_handler_disconnect (animation->frame_clock, animation->tween_handler);
705           g_signal_handler_disconnect (animation->frame_clock, animation->after_paint_handler);
706           animation->tween_handler = 0;
707         }
708       else
709         {
710           g_source_remove (animation->tween_handler);
711           animation->tween_handler = 0;
712         }
713       dzl_animation_unload_begin_values (animation);
714       dzl_animation_notify (animation);
715       g_object_unref (animation);
716     }
717 }
718 
719 
720 /**
721  * dzl_animation_add_property:
722  * @animation: (in): A #DzlAnimation.
723  * @pspec: (in): A #ParamSpec of @target or a #GtkWidget<!-- -->'s parent.
724  * @value: (in): The new value for the property at the end of the animation.
725  *
726  * Adds a new property to the set of properties to be animated during the
727  * lifetime of the animation.
728  *
729  * Side effects: None.
730  */
731 void
dzl_animation_add_property(DzlAnimation * animation,GParamSpec * pspec,const GValue * value)732 dzl_animation_add_property (DzlAnimation *animation,
733                             GParamSpec   *pspec,
734                             const GValue *value)
735 {
736   Tween tween = { 0 };
737   GType type;
738 
739   g_return_if_fail (DZL_IS_ANIMATION (animation));
740   g_return_if_fail (pspec != NULL);
741   g_return_if_fail (value != NULL);
742   g_return_if_fail (value->g_type);
743   g_return_if_fail (animation->target);
744   g_return_if_fail (!animation->tween_handler);
745 
746   type = G_TYPE_FROM_INSTANCE (animation->target);
747   tween.is_child = !g_type_is_a (type, pspec->owner_type);
748   if (tween.is_child)
749     {
750       if (!GTK_IS_WIDGET (animation->target))
751         {
752           g_critical (_("Cannot locate property %s in class %s"),
753                       pspec->name, g_type_name (type));
754           return;
755         }
756     }
757 
758   tween.pspec = g_param_spec_ref (pspec);
759   g_value_init (&tween.begin, pspec->value_type);
760   g_value_init (&tween.end, pspec->value_type);
761   g_value_copy (value, &tween.end);
762   g_array_append_val (animation->tweens, tween);
763 }
764 
765 
766 /**
767  * dzl_animation_dispose:
768  * @object: (in): A #DzlAnimation.
769  *
770  * Releases any object references the animation contains.
771  *
772  * Side effects: None.
773  */
774 static void
dzl_animation_dispose(GObject * object)775 dzl_animation_dispose (GObject *object)
776 {
777   DzlAnimation *self = DZL_ANIMATION (object);
778 
779   g_clear_object (&self->target);
780   g_clear_object (&self->frame_clock);
781 
782   G_OBJECT_CLASS (dzl_animation_parent_class)->dispose (object);
783 }
784 
785 
786 /**
787  * dzl_animation_finalize:
788  * @object: (in): A #DzlAnimation.
789  *
790  * Finalizes the object and releases any resources allocated.
791  *
792  * Side effects: None.
793  */
794 static void
dzl_animation_finalize(GObject * object)795 dzl_animation_finalize (GObject *object)
796 {
797   DzlAnimation *self = DZL_ANIMATION (object);
798   Tween *tween;
799   guint i;
800 
801   for (i = 0; i < self->tweens->len; i++)
802     {
803       tween = &g_array_index (self->tweens, Tween, i);
804       g_value_unset (&tween->begin);
805       g_value_unset (&tween->end);
806       g_param_spec_unref (tween->pspec);
807     }
808 
809   g_array_unref (self->tweens);
810 
811   G_OBJECT_CLASS (dzl_animation_parent_class)->finalize (object);
812 }
813 
814 
815 /**
816  * dzl_animation_set_property:
817  * @object: (in): A #GObject.
818  * @prop_id: (in): The property identifier.
819  * @value: (in): The given property.
820  * @pspec: (in): A #ParamSpec.
821  *
822  * Set a given #GObject property.
823  */
824 static void
dzl_animation_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)825 dzl_animation_set_property (GObject      *object,
826                             guint         prop_id,
827                             const GValue *value,
828                             GParamSpec   *pspec)
829 {
830   DzlAnimation *animation = DZL_ANIMATION (object);
831 
832   switch (prop_id)
833     {
834     case PROP_DURATION:
835       animation->duration_msec = g_value_get_uint (value) * slow_down_factor;
836       break;
837 
838     case PROP_FRAME_CLOCK:
839       dzl_animation_set_frame_clock (animation, g_value_get_object (value));
840       break;
841 
842     case PROP_MODE:
843       animation->mode = g_value_get_enum (value);
844       break;
845 
846     case PROP_TARGET:
847       dzl_animation_set_target (animation, g_value_get_object (value));
848       break;
849 
850     default:
851       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
852     }
853 }
854 
855 
856 /**
857  * dzl_animation_class_init:
858  * @klass: (in): A #DzlAnimationClass.
859  *
860  * Initializes the GObjectClass.
861  *
862  * Side effects: Properties, signals, and vtables are initialized.
863  */
864 static void
dzl_animation_class_init(DzlAnimationClass * klass)865 dzl_animation_class_init (DzlAnimationClass *klass)
866 {
867   GObjectClass *object_class;
868   const gchar *slow_down_factor_env;
869 
870   debug = !!g_getenv ("DZL_ANIMATION_DEBUG");
871   slow_down_factor_env = g_getenv ("DZL_ANIMATION_SLOW_DOWN_FACTOR");
872 
873   if (slow_down_factor_env)
874     slow_down_factor = MAX (1, atoi (slow_down_factor_env));
875 
876   object_class = G_OBJECT_CLASS (klass);
877   object_class->dispose = dzl_animation_dispose;
878   object_class->finalize = dzl_animation_finalize;
879   object_class->set_property = dzl_animation_set_property;
880 
881   /**
882    * DzlAnimation:duration:
883    *
884    * The "duration" property is the total number of milliseconds that the
885    * animation should run before being completed.
886    */
887   properties[PROP_DURATION] =
888     g_param_spec_uint ("duration",
889                        "Duration",
890                        "The duration of the animation",
891                        0,
892                        G_MAXUINT,
893                        250,
894                        (G_PARAM_WRITABLE |
895                         G_PARAM_CONSTRUCT_ONLY |
896                         G_PARAM_STATIC_STRINGS));
897 
898   properties[PROP_FRAME_CLOCK] =
899     g_param_spec_object ("frame-clock",
900                          "Frame Clock",
901                          "An optional frame-clock to synchronize with.",
902                          GDK_TYPE_FRAME_CLOCK,
903                          (G_PARAM_WRITABLE |
904                           G_PARAM_CONSTRUCT_ONLY |
905                           G_PARAM_STATIC_STRINGS));
906 
907   /**
908    * DzlAnimation:mode:
909    *
910    * The "mode" property is the Alpha function that should be used to
911    * determine the offset within the animation based on the current
912    * offset in the animations duration.
913    */
914   properties[PROP_MODE] =
915     g_param_spec_enum ("mode",
916                        "Mode",
917                        "The animation mode",
918                        DZL_TYPE_ANIMATION_MODE,
919                        DZL_ANIMATION_LINEAR,
920                        (G_PARAM_WRITABLE |
921                         G_PARAM_CONSTRUCT_ONLY |
922                         G_PARAM_STATIC_STRINGS));
923 
924   /**
925    * DzlAnimation:target:
926    *
927    * The "target" property is the #GObject that should have its properties
928    * animated.
929    */
930   properties[PROP_TARGET] =
931     g_param_spec_object ("target",
932                          "Target",
933                          "The target of the animation",
934                          G_TYPE_OBJECT,
935                          (G_PARAM_WRITABLE |
936                           G_PARAM_CONSTRUCT_ONLY |
937                           G_PARAM_STATIC_STRINGS));
938 
939   g_object_class_install_properties (object_class, LAST_PROP, properties);
940 
941   /**
942    * DzlAnimation::tick:
943    *
944    * The "tick" signal is emitted on each frame in the animation.
945    */
946   signals[TICK] = g_signal_new ("tick",
947                                  DZL_TYPE_ANIMATION,
948                                  G_SIGNAL_RUN_FIRST,
949                                  0,
950                                  NULL, NULL, NULL,
951                                  G_TYPE_NONE,
952                                  0);
953 
954 #define SET_ALPHA(_T, _t) \
955   alpha_funcs[DZL_ANIMATION_ ## _T] = dzl_animation_alpha_ ## _t
956 
957   SET_ALPHA (LINEAR, linear);
958   SET_ALPHA (EASE_IN_QUAD, ease_in_quad);
959   SET_ALPHA (EASE_OUT_QUAD, ease_out_quad);
960   SET_ALPHA (EASE_IN_OUT_QUAD, ease_in_out_quad);
961   SET_ALPHA (EASE_IN_CUBIC, ease_in_cubic);
962   SET_ALPHA (EASE_OUT_CUBIC, ease_out_cubic);
963   SET_ALPHA (EASE_IN_OUT_CUBIC, ease_in_out_cubic);
964 
965 #define SET_TWEEN(_T, _t) \
966   G_STMT_START { \
967     guint idx = G_TYPE_ ## _T; \
968     tween_funcs[idx] = tween_ ## _t; \
969   } G_STMT_END
970 
971   SET_TWEEN (INT, int);
972   SET_TWEEN (UINT, uint);
973   SET_TWEEN (LONG, long);
974   SET_TWEEN (ULONG, ulong);
975   SET_TWEEN (FLOAT, float);
976   SET_TWEEN (DOUBLE, double);
977 }
978 
979 
980 /**
981  * dzl_animation_init:
982  * @animation: (in): A #DzlAnimation.
983  *
984  * Initializes the #DzlAnimation instance.
985  *
986  * Side effects: Everything.
987  */
988 static void
dzl_animation_init(DzlAnimation * animation)989 dzl_animation_init (DzlAnimation *animation)
990 {
991   animation->duration_msec = 250;
992   animation->mode = DZL_ANIMATION_EASE_IN_OUT_QUAD;
993   animation->tweens = g_array_new (FALSE, FALSE, sizeof (Tween));
994   animation->last_offset = -G_MINDOUBLE;
995 }
996 
997 
998 /**
999  * dzl_animation_mode_get_type:
1000  *
1001  * Retrieves the GType for #DzlAnimationMode.
1002  *
1003  * Returns: A GType.
1004  * Side effects: GType registered on first call.
1005  */
1006 GType
dzl_animation_mode_get_type(void)1007 dzl_animation_mode_get_type (void)
1008 {
1009   static GType type_id = 0;
1010   static const GEnumValue values[] = {
1011     { DZL_ANIMATION_LINEAR, "DZL_ANIMATION_LINEAR", "linear" },
1012     { DZL_ANIMATION_EASE_IN_QUAD, "DZL_ANIMATION_EASE_IN_QUAD", "ease-in-quad" },
1013     { DZL_ANIMATION_EASE_IN_OUT_QUAD, "DZL_ANIMATION_EASE_IN_OUT_QUAD", "ease-in-out-quad" },
1014     { DZL_ANIMATION_EASE_OUT_QUAD, "DZL_ANIMATION_EASE_OUT_QUAD", "ease-out-quad" },
1015     { DZL_ANIMATION_EASE_IN_CUBIC, "DZL_ANIMATION_EASE_IN_CUBIC", "ease-in-cubic" },
1016     { DZL_ANIMATION_EASE_OUT_CUBIC, "DZL_ANIMATION_EASE_OUT_CUBIC", "ease-out-cubic" },
1017     { DZL_ANIMATION_EASE_IN_OUT_CUBIC, "DZL_ANIMATION_EASE_IN_OUT_CUBIC", "ease-in-out-cubic" },
1018     { 0 }
1019   };
1020 
1021   if (G_UNLIKELY (!type_id))
1022     type_id = g_enum_register_static ("DzlAnimationMode", values);
1023   return type_id;
1024 }
1025 
1026 /**
1027  * dzl_object_animatev:
1028  * @object: A #GObject.
1029  * @mode: The animation mode.
1030  * @duration_msec: The duration in milliseconds.
1031  * @frame_clock: (nullable): The #GdkFrameClock to synchronize to.
1032  * @first_property: The first property to animate.
1033  * @args: A variadac list of arguments
1034  *
1035  * Returns: (transfer none): A #DzlAnimation.
1036  */
1037 DzlAnimation *
dzl_object_animatev(gpointer object,DzlAnimationMode mode,guint duration_msec,GdkFrameClock * frame_clock,const gchar * first_property,va_list args)1038 dzl_object_animatev (gpointer          object,
1039                      DzlAnimationMode  mode,
1040                      guint             duration_msec,
1041                      GdkFrameClock    *frame_clock,
1042                      const gchar      *first_property,
1043                      va_list           args)
1044 {
1045   DzlAnimation *animation;
1046   GObjectClass *klass;
1047   GObjectClass *pklass;
1048   const gchar *name;
1049   GParamSpec *pspec;
1050   GtkWidget *parent;
1051   GValue value = { 0 };
1052   gchar *error = NULL;
1053   GType type;
1054   GType ptype;
1055   gboolean enable_animations;
1056 
1057   g_return_val_if_fail (first_property != NULL, NULL);
1058   g_return_val_if_fail (mode < DZL_ANIMATION_LAST, NULL);
1059 
1060   if ((frame_clock == NULL) && GTK_IS_WIDGET (object))
1061     frame_clock = gtk_widget_get_frame_clock (GTK_WIDGET (object));
1062 
1063   /*
1064    * If we have a frame clock, then we must be in the gtk thread and we
1065    * should check GtkSettings for disabled animations. If we are disabled,
1066    * we will just make the timeout immediate.
1067    */
1068   if (frame_clock != NULL)
1069     {
1070       g_object_get (gtk_settings_get_default (),
1071                     "gtk-enable-animations", &enable_animations,
1072                     NULL);
1073 
1074       if (enable_animations == FALSE)
1075         duration_msec = 0;
1076     }
1077 
1078   name = first_property;
1079   type = G_TYPE_FROM_INSTANCE (object);
1080   klass = G_OBJECT_GET_CLASS (object);
1081   animation = g_object_new (DZL_TYPE_ANIMATION,
1082                             "duration", duration_msec,
1083                             "frame-clock", frame_clock,
1084                             "mode", mode,
1085                             "target", object,
1086                             NULL);
1087 
1088   do
1089     {
1090       /*
1091        * First check for the property on the object. If that does not exist
1092        * then check if the object has a parent and look at its child
1093        * properties (if it's a GtkWidget).
1094        */
1095       if (!(pspec = g_object_class_find_property (klass, name)))
1096         {
1097           if (!g_type_is_a (type, GTK_TYPE_WIDGET))
1098             {
1099               g_critical (_("Failed to find property %s in %s"),
1100                           name, g_type_name (type));
1101               goto failure;
1102             }
1103           if (!(parent = gtk_widget_get_parent (object)))
1104             {
1105               g_critical (_("Failed to find property %s in %s"),
1106                           name, g_type_name (type));
1107               goto failure;
1108             }
1109           pklass = G_OBJECT_GET_CLASS (parent);
1110           ptype = G_TYPE_FROM_INSTANCE (parent);
1111           if (!(pspec = gtk_container_class_find_child_property (pklass, name)))
1112             {
1113               g_critical (_("Failed to find property %s in %s or parent %s"),
1114                           name, g_type_name (type), g_type_name (ptype));
1115               goto failure;
1116             }
1117         }
1118 
1119       g_value_init (&value, pspec->value_type);
1120       G_VALUE_COLLECT (&value, args, 0, &error);
1121       if (error != NULL)
1122         {
1123           g_critical (_("Failed to retrieve va_list value: %s"), error);
1124           g_free (error);
1125           goto failure;
1126         }
1127 
1128       dzl_animation_add_property (animation, pspec, &value);
1129       g_value_unset (&value);
1130     }
1131   while ((name = va_arg (args, const gchar *)));
1132 
1133   dzl_animation_start (animation);
1134 
1135   return animation;
1136 
1137 failure:
1138   g_object_ref_sink (animation);
1139   g_object_unref (animation);
1140   return NULL;
1141 }
1142 
1143 /**
1144  * dzl_object_animate:
1145  * @object: (in): A #GObject.
1146  * @mode: (in): The animation mode.
1147  * @duration_msec: (in): The duration in milliseconds.
1148  * @first_property: (in): The first property to animate.
1149  *
1150  * Animates the properties of @object. The can be set in a similar manner to g_object_set(). They
1151  * will be animated from their current value to the target value over the time period.
1152  *
1153  * Return value: (transfer none): A #DzlAnimation.
1154  * Side effects: None.
1155  */
1156 DzlAnimation*
dzl_object_animate(gpointer object,DzlAnimationMode mode,guint duration_msec,GdkFrameClock * frame_clock,const gchar * first_property,...)1157 dzl_object_animate (gpointer        object,
1158                     DzlAnimationMode mode,
1159                     guint           duration_msec,
1160                     GdkFrameClock  *frame_clock,
1161                     const gchar    *first_property,
1162                     ...)
1163 {
1164   DzlAnimation *animation;
1165   va_list args;
1166 
1167   va_start (args, first_property);
1168   animation = dzl_object_animatev (object,
1169                                    mode,
1170                                    duration_msec,
1171                                    frame_clock,
1172                                    first_property,
1173                                    args);
1174   va_end (args);
1175 
1176   return animation;
1177 }
1178 
1179 /**
1180  * dzl_object_animate_full:
1181  *
1182  * Return value: (transfer none): A #DzlAnimation.
1183  */
1184 DzlAnimation*
dzl_object_animate_full(gpointer object,DzlAnimationMode mode,guint duration_msec,GdkFrameClock * frame_clock,GDestroyNotify notify,gpointer notify_data,const gchar * first_property,...)1185 dzl_object_animate_full (gpointer        object,
1186                          DzlAnimationMode mode,
1187                          guint           duration_msec,
1188                          GdkFrameClock  *frame_clock,
1189                          GDestroyNotify  notify,
1190                          gpointer        notify_data,
1191                          const gchar    *first_property,
1192                          ...)
1193 {
1194   DzlAnimation *animation;
1195   va_list args;
1196 
1197   va_start (args, first_property);
1198   animation = dzl_object_animatev (object,
1199                                    mode,
1200                                    duration_msec,
1201                                    frame_clock,
1202                                    first_property,
1203                                    args);
1204   va_end (args);
1205 
1206   animation->notify = notify;
1207   animation->notify_data = notify_data;
1208 
1209   return animation;
1210 }
1211 
1212 guint
dzl_animation_calculate_duration(GdkMonitor * monitor,gdouble from_value,gdouble to_value)1213 dzl_animation_calculate_duration (GdkMonitor *monitor,
1214                                   gdouble     from_value,
1215                                   gdouble     to_value)
1216 {
1217   GdkRectangle geom;
1218   gdouble distance_units;
1219   gdouble distance_mm;
1220   gdouble mm_per_frame;
1221   gint height_mm;
1222   gint refresh_rate;
1223   gint n_frames;
1224   guint ret;
1225 
1226 #define MM_PER_SECOND       (150.0)
1227 #define MIN_FRAMES_PER_ANIM (5)
1228 #define MAX_FRAMES_PER_ANIM (500)
1229 
1230   g_assert (GDK_IS_MONITOR (monitor));
1231   g_assert (from_value >= 0.0);
1232   g_assert (to_value >= 0.0);
1233 
1234   /*
1235    * Get various monitor information we'll need to calculate the duration of
1236    * the animation. We need the physical space of the monitor, the refresh
1237    * rate, and geometry so that we can limit how many device units we will
1238    * traverse per-frame of the animation. Failure to deal with the physical
1239    * space results in jittery animations to the user.
1240    *
1241    * It would also be nice to take into account the acceleration curve so that
1242    * we know the max amount of jump per frame, but that is getting into
1243    * diminishing returns since we can just average it out.
1244    */
1245   height_mm = gdk_monitor_get_height_mm (monitor);
1246   gdk_monitor_get_geometry (monitor, &geom);
1247   refresh_rate = gdk_monitor_get_refresh_rate (monitor);
1248   if (refresh_rate == 0)
1249     refresh_rate = 60000;
1250 
1251   /*
1252    * The goal here is to determine the number of millimeters that we need to
1253    * animate given a transition of distance_unit pixels. Since we are dealing
1254    * with physical units (mm), we don't need to take into account the device
1255    * scale underneath the widget. The equation comes out the same.
1256    */
1257 
1258   distance_units = ABS (from_value - to_value);
1259   distance_mm = distance_units / (gdouble)geom.height * height_mm;
1260   mm_per_frame = MM_PER_SECOND / (refresh_rate / 1000.0);
1261   n_frames = (distance_mm / mm_per_frame) + 1;
1262 
1263   ret = n_frames * (1000.0 / (refresh_rate / 1000.0));
1264   ret = CLAMP (ret,
1265                MIN_FRAMES_PER_ANIM * (1000000.0 / refresh_rate),
1266                MAX_FRAMES_PER_ANIM * (1000000.0 / refresh_rate));
1267 
1268   return ret;
1269 
1270 #undef MM_PER_SECOND
1271 #undef MIN_FRAMES_PER_ANIM
1272 #undef MAX_FRAMES_PER_ANIM
1273 }
1274