1 #include <stdio.h>
2 #include <stdlib.h>
3
4 #include <gmodule.h>
5 #include <cogl/cogl.h>
6
7 #include <clutter/clutter.h>
8 #include "test-utils.h"
9 #include "tests/clutter-test-utils.h"
10
11 /* layout actor, by Lucas Rocha */
12
13 #define MY_TYPE_THING (my_thing_get_type ())
14 #define MY_THING(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), MY_TYPE_THING, MyThing))
15 #define MY_IS_THING(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), MY_TYPE_THING))
16 #define MY_THING_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), MY_TYPE_THING, MyThingClass))
17 #define MY_IS_THING_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), MY_TYPE_THING))
18 #define MY_THING_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), MY_TYPE_THING, MyThingClass))
19
20 typedef struct _MyThing MyThing;
21 typedef struct _MyThingPrivate MyThingPrivate;
22 typedef struct _MyThingClass MyThingClass;
23
24 struct _MyThing
25 {
26 ClutterActor parent_instance;
27
28 MyThingPrivate *priv;
29 };
30
31 struct _MyThingClass
32 {
33 ClutterActorClass parent_class;
34 };
35
36 enum
37 {
38 PROP_0,
39
40 PROP_SPACING,
41 PROP_PADDING,
42 PROP_USE_TRANSFORMED_BOX
43 };
44
45 struct _MyThingPrivate
46 {
47 gfloat spacing;
48 gfloat padding;
49
50 guint use_transformed_box : 1;
51 };
52
53 GType my_thing_get_type (void);
54
55 int
56 test_layout_main (int argc, char *argv[]);
57
58 const char *
59 test_layout_describe (void);
60
G_DEFINE_TYPE_WITH_PRIVATE(MyThing,my_thing,CLUTTER_TYPE_ACTOR)61 G_DEFINE_TYPE_WITH_PRIVATE (MyThing, my_thing, CLUTTER_TYPE_ACTOR)
62
63 #define MY_THING_GET_PRIVATE(obj) \
64 (G_TYPE_INSTANCE_GET_PRIVATE ((obj), MY_TYPE_THING, MyThingPrivate))
65
66 static void
67 my_thing_set_property (GObject *gobject,
68 guint prop_id,
69 const GValue *value,
70 GParamSpec *pspec)
71 {
72 MyThingPrivate *priv = MY_THING (gobject)->priv;
73 gboolean needs_relayout = TRUE;
74
75 switch (prop_id)
76 {
77 case PROP_SPACING:
78 priv->spacing = g_value_get_float (value);
79 break;
80
81 case PROP_PADDING:
82 priv->padding = g_value_get_float (value);
83 break;
84
85 case PROP_USE_TRANSFORMED_BOX:
86 priv->use_transformed_box = g_value_get_boolean (value);
87 break;
88
89 default:
90 G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
91 needs_relayout = FALSE;
92 break;
93 }
94
95 /* setting spacing or padding queues a relayout
96 because they are supposed to change the internal
97 allocation of children */
98 if (needs_relayout)
99 clutter_actor_queue_relayout (CLUTTER_ACTOR (gobject));
100 }
101
102 static void
my_thing_get_property(GObject * gobject,guint prop_id,GValue * value,GParamSpec * pspec)103 my_thing_get_property (GObject *gobject,
104 guint prop_id,
105 GValue *value,
106 GParamSpec *pspec)
107 {
108 MyThingPrivate *priv = MY_THING (gobject)->priv;
109
110 switch (prop_id)
111 {
112 case PROP_SPACING:
113 g_value_set_float (value, priv->spacing);
114 break;
115
116 case PROP_PADDING:
117 g_value_set_float (value, priv->padding);
118 break;
119
120 case PROP_USE_TRANSFORMED_BOX:
121 g_value_set_boolean (value, priv->use_transformed_box);
122 break;
123
124 default:
125 G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
126 break;
127 }
128 }
129
130 static void
my_thing_get_preferred_width(ClutterActor * self,gfloat for_height,gfloat * min_width_p,gfloat * natural_width_p)131 my_thing_get_preferred_width (ClutterActor *self,
132 gfloat for_height,
133 gfloat *min_width_p,
134 gfloat *natural_width_p)
135 {
136 ClutterActorIter iter;
137 ClutterActor *child;
138 gfloat min_left, min_right;
139 gfloat natural_left, natural_right;
140
141 min_left = 0;
142 min_right = 0;
143 natural_left = 0;
144 natural_right = 0;
145
146 clutter_actor_iter_init (&iter, self);
147 while (clutter_actor_iter_next (&iter, &child))
148 {
149 gfloat child_x, child_min, child_natural;
150
151 child_x = clutter_actor_get_x (child);
152
153 clutter_actor_get_preferred_size (child,
154 &child_min, NULL,
155 &child_natural, NULL);
156
157 if (child == clutter_actor_get_first_child (self))
158 {
159 /* First child */
160 min_left = child_x;
161 natural_left = child_x;
162 min_right = min_left + child_min;
163 natural_right = natural_left + child_natural;
164 }
165 else
166 {
167 /* Union of extents with previous children */
168 if (child_x < min_left)
169 min_left = child_x;
170
171 if (child_x < natural_left)
172 natural_left = child_x;
173
174 if (child_x + child_min > min_right)
175 min_right = child_x + child_min;
176
177 if (child_x + child_natural > natural_right)
178 natural_right = child_x + child_natural;
179 }
180 }
181
182 if (min_left < 0)
183 min_left = 0;
184
185 if (natural_left < 0)
186 natural_left = 0;
187
188 if (min_right < 0)
189 min_right = 0;
190
191 if (natural_right < 0)
192 natural_right = 0;
193
194 g_assert (min_right >= min_left);
195 g_assert (natural_right >= natural_left);
196
197 if (min_width_p)
198 *min_width_p = min_right - min_left;
199
200 if (natural_width_p)
201 *natural_width_p = natural_right - min_left;
202 }
203
204 static void
my_thing_get_preferred_height(ClutterActor * self,gfloat for_width,gfloat * min_height_p,gfloat * natural_height_p)205 my_thing_get_preferred_height (ClutterActor *self,
206 gfloat for_width,
207 gfloat *min_height_p,
208 gfloat *natural_height_p)
209 {
210 ClutterActorIter iter;
211 ClutterActor *child;
212 gfloat min_top, min_bottom;
213 gfloat natural_top, natural_bottom;
214
215 min_top = 0;
216 min_bottom = 0;
217 natural_top = 0;
218 natural_bottom = 0;
219
220 clutter_actor_iter_init (&iter, self);
221 while (clutter_actor_iter_next (&iter, &child))
222 {
223 gfloat child_y, child_min, child_natural;
224
225 child_y = clutter_actor_get_y (child);
226
227 clutter_actor_get_preferred_size (child,
228 NULL, &child_min,
229 NULL, &child_natural);
230
231 if (child == clutter_actor_get_first_child (self))
232 {
233 /* First child */
234 min_top = child_y;
235 natural_top = child_y;
236 min_bottom = min_top + child_min;
237 natural_bottom = natural_top + child_natural;
238 }
239 else
240 {
241 /* Union of extents with previous children */
242 if (child_y < min_top)
243 min_top = child_y;
244
245 if (child_y < natural_top)
246 natural_top = child_y;
247
248 if (child_y + child_min > min_bottom)
249 min_bottom = child_y + child_min;
250
251 if (child_y + child_natural > natural_bottom)
252 natural_bottom = child_y + child_natural;
253 }
254 }
255
256 if (min_top < 0)
257 min_top = 0;
258
259 if (natural_top < 0)
260 natural_top = 0;
261
262 if (min_bottom < 0)
263 min_bottom = 0;
264
265 if (natural_bottom < 0)
266 natural_bottom = 0;
267
268 g_assert (min_bottom >= min_top);
269 g_assert (natural_bottom >= natural_top);
270
271 if (min_height_p)
272 *min_height_p = min_bottom - min_top;
273
274 if (natural_height_p)
275 *natural_height_p = natural_bottom - min_top;
276 }
277
278 static void
my_thing_allocate(ClutterActor * self,const ClutterActorBox * box)279 my_thing_allocate (ClutterActor *self,
280 const ClutterActorBox *box)
281 {
282 MyThingPrivate *priv;
283 gfloat current_x, current_y, max_row_height;
284 ClutterActorIter iter;
285 ClutterActor *child;
286
287 clutter_actor_set_allocation (self, box);
288
289 priv = MY_THING (self)->priv;
290
291 current_x = priv->padding;
292 current_y = priv->padding;
293 max_row_height = 0;
294
295 /* The allocation logic here is to horizontally place children
296 * side-by-side and reflow into a new row when we run out of
297 * space
298 */
299 clutter_actor_iter_init (&iter, self);
300 while (clutter_actor_iter_next (&iter, &child))
301 {
302 gfloat natural_width, natural_height;
303 ClutterActorBox child_box;
304
305 clutter_actor_get_preferred_size (child,
306 NULL, NULL,
307 &natural_width,
308 &natural_height);
309
310 /* if it fits in the current row, keep it there; otherwise
311 * reflow into another row
312 */
313 if (current_x + natural_width > box->x2 - box->x1 - priv->padding)
314 {
315 current_x = priv->padding;
316 current_y += max_row_height + priv->spacing;
317 max_row_height = 0;
318 }
319
320 child_box.x1 = current_x;
321 child_box.y1 = current_y;
322 child_box.x2 = child_box.x1 + natural_width;
323 child_box.y2 = child_box.y1 + natural_height;
324
325 clutter_actor_allocate (child, &child_box);
326
327 /* if we take into account the transformation of the children
328 * then we first check if it's transformed; then we get the
329 * onscreen coordinates of the two points of the bounding box
330 * of the actor (origin(x, y) and (origin + size)(x,y)) and
331 * we update the coordinates and area given to the next child
332 */
333 if (priv->use_transformed_box)
334 {
335 if (clutter_actor_is_scaled (child) ||
336 clutter_actor_is_rotated (child))
337 {
338 graphene_point3d_t v1 = { 0, }, v2 = { 0, };
339 ClutterActorBox transformed_box = { 0, };
340
341 v1.x = box->x1;
342 v1.y = box->y1;
343
344 clutter_actor_apply_transform_to_point (child, &v1, &v2);
345 transformed_box.x1 = v2.x;
346 transformed_box.y1 = v2.y;
347
348 /* size */
349 v1.x = natural_width;
350 v1.y = natural_height;
351 clutter_actor_apply_transform_to_point (child, &v1, &v2);
352 transformed_box.x2 = v2.x;
353 transformed_box.y2 = v2.y;
354
355 natural_width = transformed_box.x2 - transformed_box.x1;
356 natural_height = transformed_box.y2 - transformed_box.y1;
357 }
358 }
359
360 /* Record the maximum child height on current row to know
361 * what's the increment that should be used for the next
362 * row
363 */
364 if (natural_height > max_row_height)
365 max_row_height = natural_height;
366
367 current_x += natural_width + priv->spacing;
368 }
369 }
370
371 #define MIN_SIZE 24
372 #define MAX_SIZE 64
373
374 static void
my_thing_class_init(MyThingClass * klass)375 my_thing_class_init (MyThingClass *klass)
376 {
377 GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
378 ClutterActorClass *actor_class = CLUTTER_ACTOR_CLASS (klass);
379
380 gobject_class->set_property = my_thing_set_property;
381 gobject_class->get_property = my_thing_get_property;
382
383 actor_class->get_preferred_width = my_thing_get_preferred_width;
384 actor_class->get_preferred_height = my_thing_get_preferred_height;
385 actor_class->allocate = my_thing_allocate;
386
387 g_object_class_install_property (gobject_class,
388 PROP_SPACING,
389 g_param_spec_float ("spacing",
390 "Spacing",
391 "Spacing of the thing",
392 0, G_MAXFLOAT,
393 0,
394 G_PARAM_READWRITE));
395
396 g_object_class_install_property (gobject_class,
397 PROP_PADDING,
398 g_param_spec_float ("padding",
399 "Padding",
400 "Padding around the thing",
401 0, G_MAXFLOAT,
402 0,
403 G_PARAM_READWRITE));
404
405 g_object_class_install_property (gobject_class,
406 PROP_USE_TRANSFORMED_BOX,
407 g_param_spec_boolean ("use-transformed-box",
408 "Use Transformed Box",
409 "Use transformed box when allocating",
410 FALSE,
411 G_PARAM_READWRITE));
412 }
413
414 static void
my_thing_init(MyThing * thing)415 my_thing_init (MyThing *thing)
416 {
417 thing->priv = MY_THING_GET_PRIVATE (thing);
418 }
419
420 static ClutterActor *
my_thing_new(gfloat padding,gfloat spacing)421 my_thing_new (gfloat padding,
422 gfloat spacing)
423 {
424 return g_object_new (MY_TYPE_THING,
425 "padding", padding,
426 "spacing", spacing,
427 NULL);
428 }
429
430 /* test code */
431
432 static ClutterActor *box = NULL;
433 static ClutterActor *icon = NULL;
434 static ClutterTimeline *main_timeline = NULL;
435
436 static void
toggle_property_value(ClutterActor * actor,const gchar * property_name)437 toggle_property_value (ClutterActor *actor,
438 const gchar *property_name)
439 {
440 gboolean value;
441
442 g_object_get (actor, property_name, &value, NULL);
443
444 value = !value;
445
446 g_object_set (box, property_name, value, NULL);
447 }
448
449 static void
increase_property_value(ClutterActor * actor,const char * property_name)450 increase_property_value (ClutterActor *actor,
451 const char *property_name)
452 {
453 gfloat value;
454
455 g_object_get (actor, property_name, &value, NULL);
456
457 value = value + 10.0;
458
459 g_object_set (box, property_name, value, NULL);
460 }
461
462 static void
decrease_property_value(ClutterActor * actor,const char * property_name)463 decrease_property_value (ClutterActor *actor,
464 const char *property_name)
465 {
466 gfloat value;
467
468 g_object_get (actor, property_name, &value, NULL);
469
470 value = MAX (0, value - 10.0);
471
472 g_object_set (box, property_name, value, NULL);
473 }
474
475 static ClutterActor *
create_item(void)476 create_item (void)
477 {
478 ClutterActor *clone = clutter_clone_new (icon);
479
480 gint32 size = g_random_int_range (MIN_SIZE, MAX_SIZE);
481
482 clutter_actor_set_size (clone, size, size);
483 clutter_actor_animate_with_timeline (clone, CLUTTER_EASE_OUT_CUBIC,
484 main_timeline,
485 "scale-x", 2.0,
486 "scale-y", 2.0,
487 "fixed::scale-gravity", CLUTTER_GRAVITY_CENTER,
488 NULL);
489
490 return clone;
491 }
492
493 static gboolean
keypress_cb(ClutterActor * actor,ClutterEvent * event,gpointer data)494 keypress_cb (ClutterActor *actor,
495 ClutterEvent *event,
496 gpointer data)
497 {
498 switch (clutter_event_get_key_symbol (event))
499 {
500 case CLUTTER_KEY_q:
501 clutter_test_quit ();
502 break;
503
504 case CLUTTER_KEY_a:
505 {
506 if (icon != NULL)
507 {
508 ClutterActor *clone = create_item ();
509
510 /* Add one item to container */
511 clutter_actor_add_child (box, clone);
512 }
513 break;
514 }
515
516 case CLUTTER_KEY_d:
517 {
518 ClutterActor *last_child;
519
520 last_child = clutter_actor_get_last_child (box);
521 if (last_child != NULL)
522 {
523 /* Remove last item on container */
524 clutter_actor_remove_child (box, last_child);
525 }
526 break;
527 }
528
529 case CLUTTER_KEY_w:
530 {
531 decrease_property_value (box, "padding");
532 break;
533 }
534
535 case CLUTTER_KEY_e:
536 {
537 increase_property_value (box, "padding");
538 break;
539 }
540
541 case CLUTTER_KEY_r:
542 {
543 decrease_property_value (box, "spacing");
544 break;
545 }
546
547 case CLUTTER_KEY_s:
548 {
549 toggle_property_value (box, "use-transformed-box");
550 break;
551 }
552
553 case CLUTTER_KEY_t:
554 {
555 increase_property_value (box, "spacing");
556 break;
557 }
558
559 case CLUTTER_KEY_z:
560 {
561 if (clutter_timeline_is_playing (main_timeline))
562 clutter_timeline_pause (main_timeline);
563 else
564 clutter_timeline_start (main_timeline);
565
566 break;
567 }
568
569 default:
570 break;
571 }
572
573 return FALSE;
574 }
575
576 static void
relayout_on_frame(ClutterTimeline * timeline)577 relayout_on_frame (ClutterTimeline *timeline)
578 {
579 gboolean use_transformed_box;
580
581 /* if we care about transformations updating the layout, we need to inform
582 * the layout that a transformation is happening; this is either done by
583 * attaching a notification on the transformation properties or by simply
584 * queuing a relayout on each frame of the timeline used to drive the
585 * behaviour. for simplicity's sake, we used the latter
586 */
587
588 g_object_get (G_OBJECT (box),
589 "use-transformed-box", &use_transformed_box,
590 NULL);
591
592 if (use_transformed_box)
593 clutter_actor_queue_relayout (box);
594 }
595
596 G_MODULE_EXPORT int
test_layout_main(int argc,char * argv[])597 test_layout_main (int argc, char *argv[])
598 {
599 ClutterActor *stage, *instructions;
600 gint i, size;
601 GError *error = NULL;
602
603 clutter_test_init (&argc, &argv);
604
605 stage = clutter_test_get_stage ();
606 clutter_actor_set_size (stage, 800, 600);
607 clutter_stage_set_title (CLUTTER_STAGE (stage), "Layout");
608 g_signal_connect (stage, "destroy", G_CALLBACK (clutter_test_quit), NULL);
609
610 main_timeline = clutter_timeline_new_for_actor (stage, 2000);
611 clutter_timeline_set_repeat_count (main_timeline, -1);
612 clutter_timeline_set_auto_reverse (main_timeline, TRUE);
613 g_signal_connect (main_timeline, "new-frame",
614 G_CALLBACK (relayout_on_frame),
615 NULL);
616
617
618 box = my_thing_new (10, 10);
619
620 clutter_actor_set_position (box, 20, 20);
621 clutter_actor_set_size (box, 350, -1);
622
623 icon = clutter_test_utils_create_texture_from_file (TESTS_DATADIR
624 G_DIR_SEPARATOR_S
625 "redhand.png",
626 &error);
627 if (error)
628 g_error ("Unable to load 'redhand.png': %s", error->message);
629
630 size = g_random_int_range (MIN_SIZE, MAX_SIZE);
631 clutter_actor_set_size (icon, size, size);
632 clutter_actor_add_child (box, icon);
633 clutter_actor_animate_with_timeline (icon, CLUTTER_EASE_OUT_CUBIC,
634 main_timeline,
635 "scale-x", 2.0,
636 "scale-y", 2.0,
637 "fixed::scale-gravity", CLUTTER_GRAVITY_CENTER,
638 NULL);
639
640 for (i = 1; i < 33; i++)
641 {
642 ClutterActor *clone = create_item ();
643
644 clutter_actor_add_child (box, clone);
645 }
646
647 clutter_actor_add_child (stage, box);
648
649 instructions = clutter_text_new_with_text (NULL,
650 "<b>Instructions:</b>\n"
651 "a - add a new item\n"
652 "d - remove last item\n"
653 "z - start/pause behaviour\n"
654 "w - decrease padding\n"
655 "e - increase padding\n"
656 "r - decrease spacing\n"
657 "t - increase spacing\n"
658 "s - use transformed box\n"
659 "q - quit");
660
661 clutter_text_set_use_markup (CLUTTER_TEXT (instructions), TRUE);
662 clutter_actor_set_position (instructions, 450, 10);
663 clutter_actor_add_child (stage, instructions);
664
665 g_signal_connect (stage, "key-release-event",
666 G_CALLBACK (keypress_cb),
667 NULL);
668
669 clutter_timeline_stop (main_timeline);
670
671 clutter_actor_show (stage);
672
673 clutter_test_main ();
674
675 g_object_unref (main_timeline);
676
677 return EXIT_SUCCESS;
678 }
679
680 G_MODULE_EXPORT const char *
test_layout_describe(void)681 test_layout_describe (void)
682 {
683 return "Container implementing a layout policy.";
684 }
685