1/* GTK - The GIMP Toolkit
2 * Copyright (C) 2017 Benjamin Otte <otte@gnome.org>
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public
15 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
16 */
17
18#include "config.h"
19
20#include "gtkfishbowl.h"
21
22typedef struct _GtkFishbowlPrivate GtkFishbowlPrivate;
23typedef struct _GtkFishbowlChild GtkFishbowlChild;
24
25struct _GtkFishbowlPrivate
26{
27 GtkFishCreationFunc creation_func;
28 GHashTable *children;
29 guint count;
30
31 gint64 last_frame_time;
32 gint64 update_delay;
33 guint tick_id;
34
35 double framerate;
36 int last_benchmark_change;
37
38 guint benchmark : 1;
39};
40
41struct _GtkFishbowlChild
42{
43 GtkWidget *widget;
44 double x;
45 double y;
46 double dx;
47 double dy;
48};
49
50enum {
51 PROP_0,
52 PROP_ANIMATING,
53 PROP_BENCHMARK,
54 PROP_COUNT,
55 PROP_FRAMERATE,
56 PROP_UPDATE_DELAY,
57 NUM_PROPERTIES
58};
59
60static GParamSpec *props[NUM_PROPERTIES] = { NULL, };
61
62G_DEFINE_TYPE_WITH_PRIVATE (GtkFishbowl, gtk_fishbowl, GTK_TYPE_WIDGET)
63
64static void
65gtk_fishbowl_init (GtkFishbowl *fishbowl)
66{
67 GtkFishbowlPrivate *priv = gtk_fishbowl_get_instance_private (self: fishbowl);
68
69 priv->update_delay = G_USEC_PER_SEC;
70 priv->children = g_hash_table_new_full (NULL, NULL,
71 NULL, value_destroy_func: (GDestroyNotify) g_free);
72}
73
74/**
75 * gtk_fishbowl_new:
76 *
77 * Creates a new `GtkFishbowl`.
78 *
79 * Returns: a new `GtkFishbowl`.
80 */
81GtkWidget*
82gtk_fishbowl_new (void)
83{
84 return g_object_new (GTK_TYPE_FISHBOWL, NULL);
85}
86
87static void
88gtk_fishbowl_measure (GtkWidget *widget,
89 GtkOrientation orientation,
90 int for_size,
91 int *minimum,
92 int *natural,
93 int *minimum_baseline,
94 int *natural_baseline)
95{
96 GtkFishbowl *fishbowl = GTK_FISHBOWL (widget);
97 GtkFishbowlPrivate *priv = gtk_fishbowl_get_instance_private (self: fishbowl);
98 GHashTableIter iter;
99 gpointer key, value;
100 GtkFishbowlChild *child;
101 int child_min, child_nat;
102
103 *minimum = 0;
104 *natural = 0;
105
106 g_hash_table_iter_init (iter: &iter, hash_table: priv->children);
107 while (g_hash_table_iter_next (iter: &iter, key: &key, value: &value))
108 {
109 child = value;
110
111 if (orientation == GTK_ORIENTATION_HORIZONTAL)
112 {
113 gtk_widget_measure (widget: child->widget, orientation, for_size: -1, minimum: &child_min, natural: &child_nat, NULL, NULL);
114 }
115 else
116 {
117 int min_width;
118
119 gtk_widget_measure (widget: child->widget, orientation: GTK_ORIENTATION_HORIZONTAL, for_size: -1, minimum: &min_width, NULL, NULL, NULL);
120 gtk_widget_measure (widget: child->widget, orientation, for_size: min_width, minimum: &child_min, natural: &child_nat, NULL, NULL);
121 }
122
123 *minimum = MAX (*minimum, child_min);
124 *natural = MAX (*natural, child_nat);
125 }
126}
127
128static void
129gtk_fishbowl_size_allocate (GtkWidget *widget,
130 int width,
131 int height,
132 int baseline)
133{
134 GtkFishbowl *fishbowl = GTK_FISHBOWL (widget);
135 GtkFishbowlPrivate *priv = gtk_fishbowl_get_instance_private (self: fishbowl);
136 GtkFishbowlChild *child;
137 GtkAllocation child_allocation;
138 GtkRequisition child_requisition;
139 GHashTableIter iter;
140 gpointer key, value;
141
142 g_hash_table_iter_init (iter: &iter, hash_table: priv->children);
143 while (g_hash_table_iter_next (iter: &iter, key: &key, value: &value))
144 {
145 child = value;
146
147 gtk_widget_get_preferred_size (widget: child->widget, minimum_size: &child_requisition, NULL);
148 child_allocation.x = round (x: child->x * (width - child_requisition.width));
149 child_allocation.y = round (x: child->y * (height - child_requisition.height));
150 child_allocation.width = child_requisition.width;
151 child_allocation.height = child_requisition.height;
152
153 gtk_widget_size_allocate (widget: child->widget, allocation: &child_allocation, baseline: -1);
154 }
155}
156
157static double
158new_speed (void)
159{
160 /* 5s to 50s to cross screen seems fair */
161 return g_random_double_range (begin: 0.02, end: 0.2);
162}
163
164static void
165gtk_fishbowl_add (GtkFishbowl *fishbowl,
166 GtkWidget *widget)
167{
168 GtkFishbowlPrivate *priv = gtk_fishbowl_get_instance_private (self: fishbowl);
169 GtkFishbowlChild *child_info;
170
171 g_return_if_fail (GTK_IS_FISHBOWL (fishbowl));
172 g_return_if_fail (GTK_IS_WIDGET (widget));
173
174 child_info = g_new0 (GtkFishbowlChild, 1);
175 child_info->widget = widget;
176 child_info->x = 0;
177 child_info->y = 0;
178 child_info->dx = new_speed ();
179 child_info->dy = new_speed ();
180
181 gtk_widget_set_parent (widget, GTK_WIDGET (fishbowl));
182 gtk_accessible_update_state (self: GTK_ACCESSIBLE (ptr: widget),
183 first_state: GTK_ACCESSIBLE_STATE_HIDDEN, TRUE,
184 -1);
185
186 g_hash_table_insert (hash_table: priv->children, key: widget, value: child_info);
187 priv->count++;
188 g_object_notify_by_pspec (G_OBJECT (fishbowl), pspec: props[PROP_COUNT]);
189}
190
191static void
192gtk_fishbowl_remove (GtkFishbowl *fishbowl,
193 GtkWidget *widget)
194{
195 GtkFishbowlPrivate *priv = gtk_fishbowl_get_instance_private (self: fishbowl);
196
197 if (g_hash_table_remove (hash_table: priv->children, key: widget))
198 {
199 gtk_widget_unparent (widget);
200
201 priv->count--;
202 g_object_notify_by_pspec (G_OBJECT (fishbowl), pspec: props[PROP_COUNT]);
203 }
204}
205
206static void
207gtk_fishbowl_finalize (GObject *object)
208{
209 GtkFishbowl *fishbowl = GTK_FISHBOWL (object);
210 GtkFishbowlPrivate *priv = gtk_fishbowl_get_instance_private (self: fishbowl);
211
212 g_hash_table_destroy (hash_table: priv->children);
213 priv->children = NULL;
214}
215
216static void
217gtk_fishbowl_dispose (GObject *object)
218{
219 GtkFishbowl *fishbowl = GTK_FISHBOWL (object);
220
221 gtk_fishbowl_set_animating (fishbowl, FALSE);
222 gtk_fishbowl_set_count (fishbowl, count: 0);
223
224 G_OBJECT_CLASS (gtk_fishbowl_parent_class)->dispose (object);
225}
226
227static void
228gtk_fishbowl_set_property (GObject *object,
229 guint prop_id,
230 const GValue *value,
231 GParamSpec *pspec)
232{
233 GtkFishbowl *fishbowl = GTK_FISHBOWL (object);
234
235 switch (prop_id)
236 {
237 case PROP_ANIMATING:
238 gtk_fishbowl_set_animating (fishbowl, animating: g_value_get_boolean (value));
239 break;
240
241 case PROP_BENCHMARK:
242 gtk_fishbowl_set_benchmark (fishbowl, animating: g_value_get_boolean (value));
243 break;
244
245 case PROP_COUNT:
246 gtk_fishbowl_set_count (fishbowl, count: g_value_get_uint (value));
247 break;
248
249 case PROP_UPDATE_DELAY:
250 gtk_fishbowl_set_update_delay (fishbowl, update_delay: g_value_get_int64 (value));
251 break;
252
253 default:
254 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
255 break;
256 }
257}
258
259static void
260gtk_fishbowl_get_property (GObject *object,
261 guint prop_id,
262 GValue *value,
263 GParamSpec *pspec)
264{
265 GtkFishbowl *fishbowl = GTK_FISHBOWL (object);
266
267 switch (prop_id)
268 {
269 case PROP_ANIMATING:
270 g_value_set_boolean (value, v_boolean: gtk_fishbowl_get_animating (fishbowl));
271 break;
272
273 case PROP_BENCHMARK:
274 g_value_set_boolean (value, v_boolean: gtk_fishbowl_get_benchmark (fishbowl));
275 break;
276
277 case PROP_COUNT:
278 g_value_set_uint (value, v_uint: gtk_fishbowl_get_count (fishbowl));
279 break;
280
281 case PROP_FRAMERATE:
282 g_value_set_double (value, v_double: gtk_fishbowl_get_framerate (fishbowl));
283 break;
284
285 case PROP_UPDATE_DELAY:
286 g_value_set_int64 (value, v_int64: gtk_fishbowl_get_update_delay (fishbowl));
287 break;
288
289 default:
290 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
291 break;
292 }
293}
294
295static void
296gtk_fishbowl_class_init (GtkFishbowlClass *klass)
297{
298 GObjectClass *object_class = G_OBJECT_CLASS (klass);
299 GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
300
301 object_class->finalize = gtk_fishbowl_finalize;
302 object_class->dispose = gtk_fishbowl_dispose;
303 object_class->set_property = gtk_fishbowl_set_property;
304 object_class->get_property = gtk_fishbowl_get_property;
305
306 widget_class->measure = gtk_fishbowl_measure;
307 widget_class->size_allocate = gtk_fishbowl_size_allocate;
308
309 props[PROP_ANIMATING] =
310 g_param_spec_boolean (name: "animating",
311 nick: "animating",
312 blurb: "Whether children are moving around",
313 FALSE,
314 flags: G_PARAM_READWRITE);
315
316 props[PROP_BENCHMARK] =
317 g_param_spec_boolean (name: "benchmark",
318 nick: "Benchmark",
319 blurb: "Adapt the count property to hit the maximum framerate",
320 FALSE,
321 flags: G_PARAM_READWRITE);
322
323 props[PROP_COUNT] =
324 g_param_spec_uint (name: "count",
325 nick: "Count",
326 blurb: "Number of widgets",
327 minimum: 0, G_MAXUINT,
328 default_value: 0,
329 flags: G_PARAM_READWRITE);
330
331 props[PROP_FRAMERATE] =
332 g_param_spec_double (name: "framerate",
333 nick: "Framerate",
334 blurb: "Framerate of this widget in frames per second",
335 minimum: 0, G_MAXDOUBLE,
336 default_value: 0,
337 flags: G_PARAM_READABLE);
338
339 props[PROP_UPDATE_DELAY] =
340 g_param_spec_int64 (name: "update-delay",
341 nick: "Update delay",
342 blurb: "Number of usecs between updates",
343 minimum: 0, G_MAXINT64,
344 G_USEC_PER_SEC,
345 flags: G_PARAM_READWRITE);
346
347 g_object_class_install_properties (oclass: object_class, n_pspecs: NUM_PROPERTIES, pspecs: props);
348
349 gtk_widget_class_set_accessible_role (widget_class, accessible_role: GTK_ACCESSIBLE_ROLE_PRESENTATION);
350}
351
352guint
353gtk_fishbowl_get_count (GtkFishbowl *fishbowl)
354{
355 GtkFishbowlPrivate *priv = gtk_fishbowl_get_instance_private (self: fishbowl);
356
357 return priv->count;
358}
359
360void
361gtk_fishbowl_set_count (GtkFishbowl *fishbowl,
362 guint count)
363{
364 GtkFishbowlPrivate *priv = gtk_fishbowl_get_instance_private (self: fishbowl);
365
366 if (priv->count == count)
367 return;
368
369 g_object_freeze_notify (G_OBJECT (fishbowl));
370
371 while (priv->count > count)
372 {
373 gtk_fishbowl_remove (fishbowl, widget: gtk_widget_get_first_child (GTK_WIDGET (fishbowl)));
374 }
375
376 while (priv->count < count)
377 {
378 GtkWidget *new_widget;
379
380 new_widget = priv->creation_func ();
381
382 gtk_fishbowl_add (fishbowl, widget: new_widget);
383 }
384
385 g_object_thaw_notify (G_OBJECT (fishbowl));
386}
387
388gboolean
389gtk_fishbowl_get_benchmark (GtkFishbowl *fishbowl)
390{
391 GtkFishbowlPrivate *priv = gtk_fishbowl_get_instance_private (self: fishbowl);
392
393 return priv->benchmark;
394}
395
396void
397gtk_fishbowl_set_benchmark (GtkFishbowl *fishbowl,
398 gboolean benchmark)
399{
400 GtkFishbowlPrivate *priv = gtk_fishbowl_get_instance_private (self: fishbowl);
401
402 if (priv->benchmark == benchmark)
403 return;
404
405 priv->benchmark = benchmark;
406 if (!benchmark)
407 priv->last_benchmark_change = 0;
408
409 g_object_notify_by_pspec (G_OBJECT (fishbowl), pspec: props[PROP_BENCHMARK]);
410}
411
412gboolean
413gtk_fishbowl_get_animating (GtkFishbowl *fishbowl)
414{
415 GtkFishbowlPrivate *priv = gtk_fishbowl_get_instance_private (self: fishbowl);
416
417 return priv->tick_id != 0;
418}
419
420static gint64
421guess_refresh_interval (GdkFrameClock *frame_clock)
422{
423 gint64 interval;
424 gint64 i;
425
426 interval = G_MAXINT64;
427
428 for (i = gdk_frame_clock_get_history_start (frame_clock);
429 i < gdk_frame_clock_get_frame_counter (frame_clock);
430 i++)
431 {
432 GdkFrameTimings *t, *before;
433 gint64 ts, before_ts;
434
435 t = gdk_frame_clock_get_timings (frame_clock, frame_counter: i);
436 before = gdk_frame_clock_get_timings (frame_clock, frame_counter: i - 1);
437 if (t == NULL || before == NULL)
438 continue;
439
440 ts = gdk_frame_timings_get_frame_time (timings: t);
441 before_ts = gdk_frame_timings_get_frame_time (timings: before);
442 if (ts == 0 || before_ts == 0)
443 continue;
444
445 interval = MIN (interval, ts - before_ts);
446 }
447
448 if (interval == G_MAXINT64)
449 return 0;
450
451 return interval;
452}
453
454static void
455gtk_fishbowl_do_update (GtkFishbowl *fishbowl)
456{
457 GtkFishbowlPrivate *priv = gtk_fishbowl_get_instance_private (self: fishbowl);
458 GdkFrameClock *frame_clock;
459 GdkFrameTimings *start, *end;
460 gint64 start_counter, end_counter;
461 gint64 n_frames, expected_frames;
462 gint64 start_timestamp, end_timestamp;
463 gint64 interval;
464
465 frame_clock = gtk_widget_get_frame_clock (GTK_WIDGET (fishbowl));
466 if (frame_clock == NULL)
467 return;
468
469 start_counter = gdk_frame_clock_get_history_start (frame_clock);
470 end_counter = gdk_frame_clock_get_frame_counter (frame_clock);
471 start = gdk_frame_clock_get_timings (frame_clock, frame_counter: start_counter);
472 for (end = gdk_frame_clock_get_timings (frame_clock, frame_counter: end_counter);
473 end_counter > start_counter && end != NULL && !gdk_frame_timings_get_complete (timings: end);
474 end = gdk_frame_clock_get_timings (frame_clock, frame_counter: end_counter))
475 end_counter--;
476 if (end_counter - start_counter < 4)
477 return;
478
479 start_timestamp = gdk_frame_timings_get_presentation_time (timings: start);
480 end_timestamp = gdk_frame_timings_get_presentation_time (timings: end);
481 if (start_timestamp == 0 || end_timestamp == 0)
482 {
483 start_timestamp = gdk_frame_timings_get_frame_time (timings: start);
484 end_timestamp = gdk_frame_timings_get_frame_time (timings: end);
485 }
486
487 n_frames = end_counter - start_counter;
488 priv->framerate = ((double) n_frames) * G_USEC_PER_SEC / (end_timestamp - start_timestamp);
489 priv->framerate = ((int)(priv->framerate * 100))/100.0;
490
491 g_object_notify_by_pspec (G_OBJECT (fishbowl), pspec: props[PROP_FRAMERATE]);
492
493 if (!priv->benchmark)
494 return;
495
496 interval = gdk_frame_timings_get_refresh_interval (timings: end);
497 if (interval == 0)
498 {
499 interval = guess_refresh_interval (frame_clock);
500 if (interval == 0)
501 return;
502 }
503 expected_frames = round (x: (double) (end_timestamp - start_timestamp) / interval);
504
505 if (n_frames >= expected_frames)
506 {
507 if (priv->last_benchmark_change > 0)
508 priv->last_benchmark_change *= 2;
509 else
510 priv->last_benchmark_change = 1;
511 }
512 else if (n_frames + 1 < expected_frames)
513 {
514 if (priv->last_benchmark_change < 0)
515 priv->last_benchmark_change--;
516 else
517 priv->last_benchmark_change = -1;
518 }
519 else
520 {
521 priv->last_benchmark_change = 0;
522 }
523
524 gtk_fishbowl_set_count (fishbowl, MAX (1, (int) priv->count + priv->last_benchmark_change));
525}
526
527static gboolean
528gtk_fishbowl_tick (GtkWidget *widget,
529 GdkFrameClock *frame_clock,
530 gpointer unused)
531{
532 GtkFishbowl *fishbowl = GTK_FISHBOWL (widget);
533 GtkFishbowlPrivate *priv = gtk_fishbowl_get_instance_private (self: fishbowl);
534 GtkFishbowlChild *child;
535 gint64 frame_time, elapsed;
536 GHashTableIter iter;
537 gpointer key, value;
538 gboolean do_update;
539
540 frame_time = gdk_frame_clock_get_frame_time (frame_clock: gtk_widget_get_frame_clock (widget));
541 elapsed = frame_time - priv->last_frame_time;
542 do_update = frame_time / priv->update_delay != priv->last_frame_time / priv->update_delay;
543 priv->last_frame_time = frame_time;
544
545 /* last frame was 0, so we're just starting to animate */
546 if (elapsed == frame_time)
547 return G_SOURCE_CONTINUE;
548
549 g_hash_table_iter_init (iter: &iter, hash_table: priv->children);
550 while (g_hash_table_iter_next (iter: &iter, key: &key, value: &value))
551 {
552 child = value;
553
554 child->x += child->dx * ((double) elapsed / G_USEC_PER_SEC);
555 child->y += child->dy * ((double) elapsed / G_USEC_PER_SEC);
556
557 if (child->x <= 0)
558 {
559 child->x = 0;
560 child->dx = new_speed ();
561 }
562 else if (child->x >= 1)
563 {
564 child->x = 1;
565 child->dx = - new_speed ();
566 }
567
568 if (child->y <= 0)
569 {
570 child->y = 0;
571 child->dy = new_speed ();
572 }
573 else if (child->y >= 1)
574 {
575 child->y = 1;
576 child->dy = - new_speed ();
577 }
578 }
579
580 gtk_widget_queue_allocate (widget);
581
582 if (do_update)
583 gtk_fishbowl_do_update (fishbowl);
584
585 return G_SOURCE_CONTINUE;
586}
587
588void
589gtk_fishbowl_set_animating (GtkFishbowl *fishbowl,
590 gboolean animating)
591{
592 GtkFishbowlPrivate *priv = gtk_fishbowl_get_instance_private (self: fishbowl);
593
594 if (gtk_fishbowl_get_animating (fishbowl) == animating)
595 return;
596
597 if (animating)
598 {
599 priv->tick_id = gtk_widget_add_tick_callback (GTK_WIDGET (fishbowl),
600 callback: gtk_fishbowl_tick,
601 NULL,
602 NULL);
603 }
604 else
605 {
606 priv->last_frame_time = 0;
607 gtk_widget_remove_tick_callback (GTK_WIDGET (fishbowl), id: priv->tick_id);
608 priv->tick_id = 0;
609 priv->framerate = 0;
610 g_object_notify_by_pspec (G_OBJECT (fishbowl), pspec: props[PROP_FRAMERATE]);
611 }
612
613 g_object_notify_by_pspec (G_OBJECT (fishbowl), pspec: props[PROP_ANIMATING]);
614}
615
616double
617gtk_fishbowl_get_framerate (GtkFishbowl *fishbowl)
618{
619 GtkFishbowlPrivate *priv = gtk_fishbowl_get_instance_private (self: fishbowl);
620
621 return priv->framerate;
622}
623
624gint64
625gtk_fishbowl_get_update_delay (GtkFishbowl *fishbowl)
626{
627 GtkFishbowlPrivate *priv = gtk_fishbowl_get_instance_private (self: fishbowl);
628
629 return priv->update_delay;
630}
631
632void
633gtk_fishbowl_set_update_delay (GtkFishbowl *fishbowl,
634 gint64 update_delay)
635{
636 GtkFishbowlPrivate *priv = gtk_fishbowl_get_instance_private (self: fishbowl);
637
638 if (priv->update_delay == update_delay)
639 return;
640
641 priv->update_delay = update_delay;
642
643 g_object_notify_by_pspec (G_OBJECT (fishbowl), pspec: props[PROP_UPDATE_DELAY]);
644}
645
646void
647gtk_fishbowl_set_creation_func (GtkFishbowl *fishbowl,
648 GtkFishCreationFunc creation_func)
649{
650 GtkFishbowlPrivate *priv = gtk_fishbowl_get_instance_private (self: fishbowl);
651
652 g_object_freeze_notify (G_OBJECT (fishbowl));
653
654 gtk_fishbowl_set_count (fishbowl, count: 0);
655 priv->last_benchmark_change = 0;
656
657 priv->creation_func = creation_func;
658
659 gtk_fishbowl_set_count (fishbowl, count: 1);
660
661 g_object_thaw_notify (G_OBJECT (fishbowl));
662}
663

source code of gtk/demos/gtk-demo/gtkfishbowl.c