Crash Years

to continue the series about actions and constraints, here’s an example showing how to use the ClutterDragAction to create a scrolling container that pans the contents using dragging.

we can start with the usual set up of the stage:

  clutter_init (&argc, &argv);

  stage = clutter_stage_new ();
  clutter_stage_set_title (CLUTTER_STAGE (stage), "Scrolling");
  clutter_actor_set_size (stage, 800, 600);
  g_signal_connect (stage, "destroy",
		    G_CALLBACK (clutter_main_quit),
		    NULL);
  clutter_actor_show (stage);

then we set up the group that will contain the visible portion of the panning content:

  scroll = clutter_group_new ();
  clutter_container_add_actor (CLUTTER_CONTAINER (stage), scroll);
  clutter_actor_set_size (scroll, RECT_WIDTH, RECT_HEIGHT);

  constraint = clutter_align_constraint_new (stage, CLUTTER_ALIGN_X_AXIS, 0.5)
  clutter_actor_add_constraint (scroll, constraint);
  constraint = clutter_align_constraint_new (stage, CLUTTER_ALIGN_Y_AXIS, 0.5)
  clutter_actor_add_constraint (scroll, constraint);
  clutter_actor_set_clip_to_allocation (scroll, TRUE);

the RECT_WIDTH and RECT_HEIGHT are the two size constants for the each “page” of the content. we use ClutterAlignConstraints to keep the group centered on the stage1.

the important bit is clutter_actor_set_clip_to_allocation() which will use (and track) the actor’s allocation as the clipping area.

  viewport = clutter_box_new (clutter_box_layout_new ());
  clutter_container_add_actor (CLUTTER_CONTAINER (scroll), viewport);

  for (i = 0; i < N_RECTS; i++)
    {
      ClutterColor color;

      clutter_color_from_string (&color, rect_color[i]);

      rectangle[i] = clutter_rectangle_new_with_color (&color);
      clutter_container_add_actor (CLUTTER_CONTAINER (viewport), rectangle[i]);
      clutter_actor_set_size (rectangle[i], RECT_WIDTH, RECT_HEIGHT);
    }

this is the content area, composed of a ClutterBox using a ClutterBoxLayout to lay out a list of rectangles.

let’s start the main loop, and the result should look like this:

Panning Container
does not do much

now we need to enable the panning logic. to do so, we use the ClutterDragAction on the viewport actor, and we constrain it to the horizontal axis:

  action = clutter_drag_action_new ();
  clutter_actor_add_action (viewport, action);
  clutter_drag_action_set_drag_axis (CLUTTER_DRAG_ACTION (action), CLUTTER_DRAG_X_AXIS);
  clutter_actor_set_reactive (viewport, TRUE);

and that’s it:

it's done! ship it!

well, except that it isn’t really done; the behaviour at the edges of the viewport can lead to simply have no way to pan back, and the whole thing is a bit static. we should add some “kinetic-style” animation depending on the position of the content at the end of the panning action. to do so, we can use the ClutterDragAction::drag-end signal:

g_signal_connect (action, "drag-end", G_CALLBACK (on_drag_end), NULL);

and have our logic there; first, the edges:

  float viewport_x = clutter_actor_get_x (viewport);

  /* check if we're at the viewport edges */
  if (viewport_x > 0)
    {
      clutter_actor_animate (viewport, CLUTTER_EASE_OUT_BOUNCE, 250,
                             "x", 0.0,
                             NULL);
      return;
    }

  if (viewport_x < (-1.0f * (RECT_WIDTH * (N_RECTS - 1))))
    {
      clutter_actor_animate (viewport, CLUTTER_EASE_OUT_BOUNCE, 250,
                             "x", (-1.0f * (RECT_WIDTH * (N_RECTS - 1))),
                             NULL);
      return;
    }

then the content:

  float offset_x;
  int child_visible;

  /* animate the viewport to fully show the child once we pass
   * a certain threshold with the dragging action
   */
  offset_x = fabsf (viewport_x) / RECT_WIDTH + 0.5f;
  if (offset_x > (RECT_WIDTH * 0.33))
    child_visible = (int) offset_x + 1;
  else
    child_visible = (int) offset_x;

  /* sanity check on the children number */
  child_visible = CLAMP (child_visible, 0, N_RECTS);

  clutter_actor_animate (viewport, CLUTTER_EASE_OUT_QUAD, 250,
                         "x", (-1.0f * RECT_WIDTH * child_visible),
                         NULL);

and here‘s the result:

that wasn’t very hard, was it?

I’m going to submit this as a recipe for the Clutter Cookbook; the reference source code is available here.

  1. this will work even if you set the stage as user resizable, as the constraints are recomputed at each allocation cycle []

Constraints

last time, I described the Effects in Clutter as anything that should affect the way an actor paints itself without sub-classing.

this kind of modification of the behaviour of an Actor can also be applied to other areas, and leads to defining three different kinds of modifiers:

  • Effects, which modify the way an actor paints itself
  • Actions, which modify the way an actor responds to user events
  • Constraints, which modify the way an actor is positioned or sized

being capable of being dragged using the pointer can be defined as an action: it changes the way an Actor responds to user events (button press, motion, button release). in the same way, update the X position using the X position of another actor is a constraint placed on an Actor.

Clutter already has signals, properties and virtual functions for dealing with these modifiers, and they can be seen as the building blocks. What’s missing is a set of classes that wrap those building blocks into something that leads to run-time composition of effects, actions and constraints on top of an existing Actor class.

last week I started working on the implementation of these modifiers, and now I’m pretty close to a finalized API for Clutter 1.4. the usage of an action is pretty simple:

  ClutterAction *action = clutter_drag_action_new ();
  g_signal_connect (action, "drag-motion",
                    G_CALLBACK (on_drag_motion),
                    NULL);

  clutter_actor_add_action (some_actor, action)
  clutter_actor_set_reactive (actor, TRUE);

the simplest implementation of a ClutterDragAction::drag-motion signal handler is:

  static void
  on_drag_motion (ClutterDragAction *action,
                  ClutterActor      *actor,
                  float              delta_x,
                  float              delta_y)
  {
    clutter_actor_move_by (actor, delta_x, delta_y);
  }

there’s no fundamental difference for the constraints API; for instance, this positions an actor in the middle of another:

  ClutterConstraint *constraint;

  constraint = clutter_align_constraint_new (parent, CLUTTER_ALIGN_X_AXIS, 0.5);
  clutter_actor_add_constraint (some_actor, constraint);

  constraint = clutter_align_constraint_new (parent, CLUTTER_ALIGN_Y_AXIS, 0.5);
  clutter_actor_add_constraint (some_actor, constraint);

it’s important to note that parent in this case can be any actor, and not necessarily the scene graph parent of some_actor.

this example binds the X coordinate of an actor to the X coordinate of another one, while keeping it Y aligned at the center of its parent:

  ClutterConstraint *constraint;

  constraint = clutter_bind_constraint_new (source, CLUTTER_BIND_X, 0.0);
  clutter_actor_add_constraint_with_name (some_actor, "x-bind", source);

  constraint = clutter_align_constraint_new (parent, CLUTTER_ALIGN_Y_AXIS, 0.5);
  clutter_actor_add_constraint_with_name (some_actor, "y-bind", constraint);

Update: I’ve updated the example to use the newly added add_constraint_with_name() instead of set_name()+add_constraint(); I also changed the constraint names.

now, I’ve added a call to clutter_actor_meta_set_name() in the mix because I might decide to animate the offset property of the ClutterBindConstraint using a specific syntax for ClutterAnimation1:

  float new_offset = clutter_actor_get_width (source) + h_padding;
  clutter_actor_animate (some_actor, CLUTTER_EASE_OUT_CUBIC, 500,
                         "@constraints.x-bind.offset", new_offset,
                         "opacity", 128,
                         NULL);

the new syntax is still type checked, so you’ll get warnings if you’re using the wrong type or the wrong interval2.

caveat: obviously, a constraint only applies to an actor that is in a fixed positioning layout manager.

the new classes and API are available in the wip/actor-actions branch here and wip/constraints branch here; there are also a couple of interactive tests that show you how actions and constraints can be used with existing actors. if all goes according to plan, I’ll merge them by the end of the week — following Frederic’s blog post, I want to make a 1.3 snapshot release in time for GNOME 2.31.2.

  1. the change is contained in ClutterActor so I’m not over-complicating the parsing code in the animation class itself; which also means that ClutterAnimator and the upcoming ClutterState classes will be able to use the same syntax to address action, effect and constraint properties []
  2. I’m still considering using a dotted notation; I might switch to a colon notation, or a slash notation. and yes: I know that it looks like the syntax for key access in CoreAnimation; it turns out that there aren’t many new ways to access an item in a path []

Control

three years ago Neil and I wrote the Model API, to be included in Clutter. We tried encode in the design what we learned from the experience with GtkTreeModel, and while it could be said that there are shortcomings (a base class instead of an interface, no bulk operations, some corner cases in the iterators API) I think we managed well enough not to repeat the same issues (boxed types, volatile iterators, and conflating a Tree and a List API into one) on top of those.

still, we made what I now think is the same design mistake all over again: we tried to provide a way to write MVC applications with Clutter, and we ended up collapsing the model with the controller — that is: we added a new data storage class that notifies you when something changed inside it1.

I now think it’s a mistake trying to conflate the data storage with the actual object notifying the views about changes: the controller should just be notified by the model and notify the views

GLib already provides a lot of data storage types: GArray, GPtrArray, GHashTable, etc. — it would seem sensible to just use them and just wrap the insertion and removal functions instead of:

  • create a GObject wrapper around a data structure;
  • wrap insertion, removal and iteration operations;
  • add specialized code and signals to handle the changes and notify the views;

on top of these, if you want to write a generic storage you’ll have to:

  • make every entry point a virtual function, to allow sub-classes overriding or providing a default behaviour;
  • provide a generic iterator API;
  • wrap everything with GValues;

in essence, the complexity of the storage quickly balloons out of control — and all because you wanted to notify another object that you added a new item inside a GPtrArray2.

CONTROL! - by fatbwoy, CC-by-nd-2.0

wouldn’t it be good to have the “notify somebody that $SOMETHING changed inside a data storage” thing nailed down with a generic API?

I did think it would be good, so I spent some free cycles last week to implement a generic Controller — the thing that notifies a view that something changed. it requires minimal additions to already existing data storage types provided by GLib — to the point that you don’t really need a GObject wrapper around your model altogether.

the overall design is explained on the GNOME wiki, so I won’t rewrite it down here; and yes: if, by a cursory glance, it looks a lot like a certain platform’s API it’s because I think it’s a good representation of a correct approach.

the code lives in the GNOME Git repository; it currently has a stub LGPLv2.1 license because I think it should be seen as a 1700 lines patch to GObject/GIO under version control, and not as a stand-alone project3.

there are some things left to do, notably a GObjectController which I think I described to a colleague as GObject::notify on PCP; for that to happen, though, I’ll need some changes in GObject itself.

  1. gtk+, in a way, allows you to bolt a controller on top< of a data storage, but you still need the data storage to be a class inside the type system otherwise you won’t be able to implement the GtkTreeModel interface []
  2. and no: I don’t think that keeping the complexity under check by losing generality is a good trade-off; it’s just going to bite you in the rear later on []
  3. it also gave me the chance to play with non-recursive autotools layouts, but that was just a side-effect []