3. Detecting mouse scrolling on an actor

3.1. Problem

You want to detect when the mouse is scrolled on an actor (e.g. the pointer is over an actor when a mouse wheel is scrolled).

3.2. Solution

Connect a callback handler to the scroll-event signal of an actor.

First, ensure that the actor is reactive (i.e. will respond to events):

clutter_actor_set_reactive (actor, TRUE);

Next, create a callback handler to examine the scroll event and respond to it:

static gboolean
_scroll_event_cb (ClutterActor *actor,
                  ClutterEvent *event,
                  gpointer      user_data)
{
  /* determine the direction the mouse was scrolled */
  ClutterScrollDirection direction;
  direction = clutter_event_get_scroll_direction (event);

  /* replace these stubs with real code to move the actor etc. */
  switch (direction)
    {
    case CLUTTER_SCROLL_UP:
      g_debug ("Scrolled up");
      break;
    case CLUTTER_SCROLL_DOWN:
      g_debug ("Scrolled down");
      break;
    case CLUTTER_SCROLL_RIGHT:
      g_debug ("Scrolled right");
      break;
    case CLUTTER_SCROLL_LEFT:
      g_debug ("Scrolled left");
      break;
    }

  return CLUTTER_EVENT_STOP; /* event has been handled */
}

Finally, connect the callback handler to the scroll-event signal of the actor:

g_signal_connect (actor,
                  "scroll-event",
                  G_CALLBACK (_scroll_event_cb),
                  NULL);

3.3. Discussion

A standard mouse wheel will only return up and down movements; but in cases where the mouse has left and right scrolling (e.g. a trackball mouse or trackpad), left and right scroll events may also be emitted.

3.3.1. Creating a scrolling viewport for an actor

While the simple outline above explains the basics of how to connect to scroll events, it doesn't do much to help with really implementing scrolling over an actor. That's what we'll do in this section.

Note

The full code for the example we'll walk through here is available in this later section.

Scrolling over an actor actually requires coordination between two components:

  1. Scrollable actor. An actor which is too large to fit on the stage or inside the area of the UI assigned to it (otherwise there's no need to scroll over it...).

  2. Viewport. This displays a cropped view of part of the scrollable actor, revealing different parts of it as scroll events occur.

Here are the steps required to set up the two actors:

  1. Create the scrollable actor; it should be larger than the viewport. This example uses a ClutterTexture, but any ClutterActor will work:

    /* get image file path, set up stage etc. */
    
    ClutterActor *texture;
    texture = clutter_texture_new ();
    clutter_texture_set_keep_aspect_ratio (CLUTTER_TEXTURE (texture),
                                           TRUE);
    
    /*
     * set the texture's height so it's as tall as the stage
     * (STAGE_HEIGHT is define'd at the top of the file)
     */
    clutter_actor_set_request_mode (texture, CLUTTER_REQUEST_WIDTH_FOR_HEIGHT);
    clutter_actor_set_height (texture, STAGE_HEIGHT);
    
    /*
     * load the image file;
     * see this recipe for more about loading images into textures
     */
    clutter_texture_set_from_file (CLUTTER_TEXTURE (texture),
                                   image_file_path,
                                   NULL);
  2. Create the viewport. The simplest way to do this is with a ClutterGroup:

    ClutterActor *viewport;
    viewport = clutter_group_new ();
    
    /* viewport is _shorter_ than the stage (and the texture) */
    clutter_actor_set_size (viewport, STAGE_WIDTH, STAGE_HEIGHT * 0.5);
    
    /* align the viewport to the center of the stage's y axis */
    clutter_actor_add_constraint (viewport,
                                  clutter_align_constraint_new (stage, CLUTTER_BIND_Y, 0.5));
    
    /* viewport needs to respond to scroll events */
    clutter_actor_set_reactive (viewport, TRUE);
    
    /* clip all actors inside the viewport to that group's allocation */
    clutter_actor_set_clip_to_allocation (viewport, TRUE);

    The key here is calling clutter_actor_set_clip_to_allocation (viewport, TRUE). This configures the viewport group so that any of its children are clipped: i.e. only parts of its children which fit inside its allocation are visible. This in turn requires setting an explicit size on the group, rather than allowing it to size itself to fit its children (the latter is the default).

  3. Put the scrollable actor into the viewport; and the viewport into its container (in this case, the default stage):

    clutter_actor_add_child (viewport, texture);
    
    clutter_actor_add_child (stage, viewport);
  4. Create a callback handler for scroll-event signals emitted by the viewport:

    static gboolean
    _scroll_event_cb (ClutterActor *viewport,
                      ClutterEvent *event,
                      gpointer      user_data)
    {
      ClutterActor *scrollable = CLUTTER_ACTOR (user_data);
    
      gfloat viewport_height = clutter_actor_get_height (viewport);
      gfloat scrollable_height = clutter_actor_get_height (scrollable);
    
      /* no need to scroll if the scrollable is shorter than the viewport */
      if (scrollable_height < viewport_height)
        return CLUTTER_EVENT_STOP;
    
      gfloat y = clutter_actor_get_y (scrollable);
    
      ClutterScrollDirection direction;
      direction = clutter_event_get_scroll_direction (event);
    
      switch (direction)
        {
        case CLUTTER_SCROLL_UP:
          y -= SCROLL_AMOUNT;
          break;
        case CLUTTER_SCROLL_DOWN:
          y += SCROLL_AMOUNT;
          break;
    
        /* we're only interested in up and down */
        case CLUTTER_SCROLL_LEFT:
        case CLUTTER_SCROLL_RIGHT:
          break;
        }
    
      /*
       * the CLAMP macro returns a value for the first argument
       * that falls within the range specified by the second and
       * third arguments
       *
       * we allow the scrollable's y position to be decremented to the point
       * where its base is aligned with the base of the viewport
       */
      y = CLAMP (y,
                 viewport_height - scrollable_height,
                 0.0);
    
      /* animate the change to the scrollable's y coordinate */
      clutter_actor_animate (scrollable,
                             CLUTTER_EASE_OUT_CUBIC,
                             300,
                             "y", y,
                             NULL);
    
      return CLUTTER_EVENT_STOP;
    }

    The approach taken here is to move the scrollable actor up, relative to the viewport. Initially, the scrollable will have a y coordinate value of 0.0 (aligned to the top of the viewport). Scrolling up decrements the y coordinate (down to a minumum of viewport_height - scrollable_height). This moves the top of the scrollable actor "outside" the clip area of the viewport; simultaneously, more of the bottom part of the scrollable moves into the clip area, becoming visible.

    Scrolling down increments the y coordinate (but only up to a maximum value of 0.0).

    To see how this works in practice, look at the code sample. There, the height of the scrollable actor is set to 300 and the height of the viewport to 150. This means that the y coordinate value for the scrollable actor will vary between -150.0: 150 (the viewport's height) - 300 (the scrollable actor's height), making its base visible and clipping its top; and 0.0, where its top is visible and its base clipped.

  5. Connect the callback handler to the signal; note that we pass the scrollable actor (the texture) to the callback, as we're moving the texture relative to the viewport to create the scrolling effect:

    g_signal_connect (viewport,
                      "scroll-event",
                      G_CALLBACK (_scroll_event_cb),
                      texture);

Here's a video of the result:

3.4. Full example

Example 3.1. Mouse scrolling over a ClutterActor

#include <clutter/clutter.h>

#define STAGE_HEIGHT 300
#define STAGE_WIDTH STAGE_HEIGHT
#define SCROLL_AMOUNT STAGE_HEIGHT * 0.125

static gboolean
_scroll_event_cb (ClutterActor *viewport,
                  ClutterEvent *event,
                  gpointer      user_data)
{
  ClutterActor *scrollable = CLUTTER_ACTOR (user_data);

  gfloat viewport_height = clutter_actor_get_height (viewport);
  gfloat scrollable_height = clutter_actor_get_height (scrollable);
  gfloat y;
  ClutterScrollDirection direction;

  /* no need to scroll if the scrollable is shorter than the viewport */
  if (scrollable_height < viewport_height)
    return TRUE;

  y = clutter_actor_get_y (scrollable);

  direction = clutter_event_get_scroll_direction (event);

  switch (direction)
    {
    case CLUTTER_SCROLL_UP:
      y -= SCROLL_AMOUNT;
      break;
    case CLUTTER_SCROLL_DOWN:
      y += SCROLL_AMOUNT;
      break;

    /* we're only interested in up and down */
    case CLUTTER_SCROLL_LEFT:
    case CLUTTER_SCROLL_RIGHT:
      break;
    }

  /*
   * the CLAMP macro returns a value for the first argument
   * that falls within the range specified by the second and
   * third arguments
   *
   * we allow the scrollable's y position to be decremented to the point
   * where its base is aligned with the base of the viewport
   */
  y = CLAMP (y,
             viewport_height - scrollable_height,
             0.0);

  /* animate the change to the scrollable's y coordinate */
  clutter_actor_animate (scrollable,
                         CLUTTER_EASE_OUT_CUBIC,
                         300,
                         "y", y,
                         NULL);

  return TRUE;
}

int
main (int argc, char *argv[])
{
  ClutterActor *stage;
  ClutterActor *viewport;
  ClutterActor *texture;

  const gchar *image_file_path = "redhand.png";

  if (argc > 1)
    {
      image_file_path = argv[1];
    }

  if (clutter_init (&argc, &argv) != CLUTTER_INIT_SUCCESS)
    return 1;

  stage = clutter_stage_new ();
  clutter_actor_set_size (stage, STAGE_WIDTH, STAGE_HEIGHT);
  g_signal_connect (stage, "destroy", G_CALLBACK (clutter_main_quit), NULL);

  /* the scrollable actor */
  texture = clutter_texture_new ();
  clutter_texture_set_keep_aspect_ratio (CLUTTER_TEXTURE (texture),
                                         TRUE);

  /* set the texture's height so it's as tall as the stage */
  clutter_actor_set_request_mode (texture, CLUTTER_REQUEST_WIDTH_FOR_HEIGHT);
  clutter_actor_set_height (texture, STAGE_HEIGHT);

  clutter_texture_set_from_file (CLUTTER_TEXTURE (texture),
                                 image_file_path,
                                 NULL);

  /* the viewport which the box is scrolled within */
  viewport = clutter_actor_new ();

  /* viewport is shorter than the stage */
  clutter_actor_set_size (viewport, STAGE_WIDTH, STAGE_HEIGHT * 0.5);

  /* align the viewport to the center of the stage's y axis */
  clutter_actor_add_constraint (viewport, clutter_align_constraint_new (stage, CLUTTER_BIND_Y, 0.5));

  /* viewport needs to respond to scroll events */
  clutter_actor_set_reactive (viewport, TRUE);

  /* clip all actors inside the viewport to that group's allocation */
  clutter_actor_set_clip_to_allocation (viewport, TRUE);

  /* put the texture inside the viewport */
  clutter_actor_add_child (viewport, texture);

  /* add the viewport to the stage */
  clutter_actor_add_child (stage, viewport);

  g_signal_connect (viewport,
                    "scroll-event",
                    G_CALLBACK (_scroll_event_cb),
                    texture);

  clutter_actor_show (stage);

  clutter_main ();

  return 0;
}