cookbook: Added recipe for non-rectangular actor

Added a new recipe for creating a non-rectangular
actor using ClutterPath (aka "shaped pick") and
the Cogl primitives API.

Also cleaned up XML alignment in the actors.xml
file.
This commit is contained in:
Elliot Smith 2010-08-10 11:02:17 +01:00
parent de8276105f
commit bfa10f629f

View File

@ -462,10 +462,10 @@ clutter_rectangle_set_color (CLUTTER_RECTANGLE (actor),
<informalexample>
<programlisting>
/*
* raise actorB so it is above actorA in the depth order;
* NB actorA and actorB both need to be in the same container
* for this to work
*/
* raise actorB so it is above actorA in the depth order;
* NB actorA and actorB both need to be in the same container
* for this to work
*/
clutter_actor_raise (actorB, actorA);
</programlisting>
</informalexample>
@ -486,4 +486,359 @@ clutter_actor_raise (actorB, actorA);
</section>
<section id="actors-non-rectangular">
<title>Creating an actor with a non-rectangular shape</title>
<section>
<title>Problem</title>
<para>You want to create a <type>ClutterActor</type> subclass,
but don't want it to be rectangular; for example, you want a
star-shaped actor.</para>
</section>
<section>
<title>Solution</title>
<para>Use Cogl primitives to draw the actor.</para>
<para>Below is an example of the pick and paint implementations for a
star-shaped <type>StarActor</type> class (an extension of
<type>ClutterActor</type>).</para>
<para>Like <type>ClutterRectangle</type>, it has a private
struct internally, which contains a <type>ClutterColor</type>
denoting the color it should be painted. This is used to set the Cogl
source color.</para>
<informalexample>
<programlisting>
<![CDATA[
static void
star_actor_paint (ClutterActor *actor)
{
ClutterActorBox allocation = { 0, };
gfloat width, height;
guint tmp_alpha;
/* priv is a private internal struct */
ClutterColor color = STAR_ACTOR (actor)->priv->color;
clutter_actor_get_allocation_box (actor, &allocation);
clutter_actor_box_get_size (&allocation, &width, &height);
tmp_alpha = clutter_actor_get_paint_opacity (actor)
* color.alpha
/ 255;
cogl_path_new ();
cogl_set_source_color4ub (color.red,
color.green,
color.blue,
tmp_alpha);
/* create and store a path describing a star */
cogl_path_move_to (width * 0.5, 0);
cogl_path_line_to (width, height * 0.75);
cogl_path_line_to (0, height * 0.75);
cogl_path_move_to (width * 0.5, height);
cogl_path_line_to (0, height * 0.25);
cogl_path_line_to (width, height * 0.25);
cogl_path_line_to (width * 0.5, height);
cogl_path_fill ();
}
static void
star_actor_pick (ClutterActor *actor,
const ClutterColor *pick_color)
{
if (!clutter_actor_should_pick_paint (actor))
return;
ClutterActorBox allocation = { 0, };
gfloat width, height;
clutter_actor_get_allocation_box (actor, &allocation);
clutter_actor_box_get_size (&allocation, &width, &height);
cogl_path_new ();
cogl_set_source_color4ub (pick_color->red,
pick_color->green,
pick_color->blue,
pick_color->alpha);
/* create and store a path describing a star */
cogl_path_move_to (width * 0.5, 0);
cogl_path_line_to (width, height * 0.75);
cogl_path_line_to (0, height * 0.75);
cogl_path_move_to (width * 0.5, height);
cogl_path_line_to (0, height * 0.25);
cogl_path_line_to (width, height * 0.25);
cogl_path_line_to (width * 0.5, height);
cogl_path_fill ();
}
]]>
</programlisting>
</informalexample>
<para>If you need more information about how to implement your own
<type>ClutterActor</type>, see the Clutter reference
manual.</para>
<para>Note that the code in these two functions is virtually identical:
the Discussion section suggests how to remove this redundancy.</para>
</section>
<section>
<title>Discussion</title>
<para>The above is one approach to creating a non-rectangular
actor. But it's also possible to get a similar effect by
subclassing an existing actor (like <type>ClutterRectangle</type>)
and giving it a non-rectangular appearance. You could do this by
making the underlying rectangle transparent and then drawing on
top of it (e.g. using Cairo or Cogl).</para>
<para>However, if you then made such an actor reactive, events
like mouse button presses would be triggered from anywhere on
the underlying rectangle. This is true even if the visible part
of the actor only partially fills the rectangle (underneath, it's
still a rectangle).</para>
<para>The advantage of using Cogl paths is that the reactive area
of the actor is defined by the Cogl path. So if you have a
star-shaped actor, only clicks (or other events) directly on the
star will have any effect on it.</para>
<section>
<title>Cogl path coordinates</title>
<para>In the example shown, <function>cogl_path_move_to()</function>
and <function>cogl_path_line_to()</function> are used. These
take absolute <code>x</code> and <code>y</code> coordinates as
arguments, relative to the GL 'modelview' transform matrix; in
the case of an actor's <function>paint</function> implementation,
relative to the bounding box for the actor. So if an actor has
width and height of 50 pixels, and you used
<function>cogl_move_to (25, 25)</function> in its
<function>paint</function> implementation, the "pen"
moves to the centre of the actor, regardless of where the actor
is positioned on the stage. Similarly, using
<function>cogl_path_line_to()</function> creates a line segment
from the current pen position to the absolute coordinates
(<code>x</code>, <code>y</code>) specified.</para>
<para>The Cogl API also provides various "rel" variants of the path
functions (e.g. <function>cogl_path_rel_line_to()</function>), which
create path segments relative to the current pen position (i.e.
<code>pen_x + x</code>, <code>pen_y + y</code>).</para>
<para>It's important to note that the path isn't drawn until you
call <function>cogl_path_stroke()</function> (to draw the path segments)
or <function>cogl_path_fill()</function> (to fill the area enclosed by
the path). The path is cleared once it's been drawn.
Using the <function>*_preserve</function> variants of these functions draws
the path and retains it (so it could be drawn again).</para>
</section>
<section>
<title>Other Cogl primitives</title>
<para>Note that the Cogl primitives API provides other types of path
segment beyond straight lines that we didn't use here, including:</para>
<itemizedlist>
<listitem>
<para>Bezier curves (<function>cogl_path_curve_to()</function>)</para>
</listitem>
<listitem>
<para>Arcs (<function>cogl_path_arc()</function>)</para>
</listitem>
<listitem>
<para>Polygons (<function>cogl_path_polygon()</function>)</para>
</listitem>
<listitem>
<para>Rectangles (<function>cogl_path_rectangle()</function>)</para>
</listitem>
<listitem>
<para>Rectangles with rounded corners
(<function>cogl_path_round_rectangle()</function>)</para>
</listitem>
<listitem>
<para>Ellipses (<function>cogl_path_ellipse()</function>)</para>
</listitem>
</itemizedlist>
<para>If you need more flexibility than is available in the Cogl path
API, you can make direct use of the <type>CoglVertexBuffer</type>
API instead. This is a lower-level API, but could potentially
be used to draw more complex shapes.</para>
</section>
<section>
<title>Using <type>ClutterPath</type> to store the path</title>
<para>The disadvantage of the code above is that the paths are stored in two
places: once for <function>pick</function>, and once for
<function>paint</function>. It would make sense to store the
path in one place and reference it from both of these functions to
prevent duplication.</para>
<para>Clutter provides a <type>ClutterPath</type> API for storing
generic path descriptions. It can be used to describe paths
which translate to Cogl or Cairo paths, and can also be used to
describe animation paths.</para>
<para>We can use a <type>ClutterPath</type> instance stored
inside the actor to define the path for <function>pick</function> and
<function>paint</function>; then, inside those functions, we
translate the <type>ClutterPath</type> into Cogl path function calls
(NB <type>ClutterPath</type> is effectively a declarative method
for defining a path, while the Cogl path API is imperative).</para>
<para>First we add a <varname>path</varname> member to the private
struct for the <type>StarActor</type> class (using standard
GObject mechanisms). The <function>init</function> implementation for
<type>StarActor</type> creates an empty path:</para>
<informalexample>
<programlisting>
static void
star_actor_init (StarActor *self)
{
self->priv = STAR_ACTOR_GET_PRIVATE (self);
self->priv->path = clutter_path_new ();
clutter_actor_set_reactive (CLUTTER_ACTOR (self), TRUE);
}
</programlisting>
</informalexample>
<para>One consideration is that the path coordinates need to
fit inside the actor's bounding box. So as the actor's allocation
changes, <varname>path</varname> also needs to change. We can do this
by implementing <function>allocate</function> for the
<type>StarActor</type> class:</para>
<informalexample>
<programlisting>
<![CDATA[
static void
star_actor_allocate (ClutterActor *actor,
const ClutterActorBox *box,
ClutterAllocationFlags flags)
{
ClutterPath *path = STAR_ACTOR (actor)->priv->path;
gfloat width, height;
clutter_actor_box_get_size (box, &width, &height);
/* create and store a path describing a star */
clutter_path_clear (path);
clutter_path_add_move_to (path, width * 0.5, 0);
clutter_path_add_line_to (path, width, height * 0.75);
clutter_path_add_line_to (path, 0, height * 0.75);
clutter_path_add_move_to (path, width * 0.5, height);
clutter_path_add_line_to (path, 0, height * 0.25);
clutter_path_add_line_to (path, width, height * 0.25);
clutter_path_add_line_to (path, width * 0.5, height);
CLUTTER_ACTOR_CLASS (star_actor_parent_class)->allocate (actor, box, flags);
}
]]>
</programlisting>
</informalexample>
<para>This clears then adds segments to the
<type>ClutterPath</type> stored with the
<type>StarActor</type> instance. The positioning and
lengths of the segments are relative to the size of the actor when
its allocation changes.</para>
<para>The <function>pick</function> and <function>paint</function>
functions now reference the <type>ClutterPath</type> (only the
<function>pick</function> is shown below); and
to turn the path into drawing operations, we implement a
<function>star_actor_convert_clutter_path_node()</function> function
which takes a <type>ClutterPathNode</type> and converts it
into its Cogl equivalent:</para>
<informalexample>
<programlisting>
<![CDATA[
static void
star_actor_convert_clutter_path_node (const ClutterPathNode *node,
gpointer data)
{
g_return_if_fail (node != NULL);
ClutterKnot knot;
switch (node->type)
{
case CLUTTER_PATH_MOVE_TO:
knot = node->points[0];
cogl_path_move_to (knot.x, knot.y);
break;
case CLUTTER_PATH_LINE_TO:
knot = node->points[0];
cogl_path_line_to (knot.x, knot.y);
break;
default:
break;
}
}
static void
star_actor_pick (ClutterActor *actor,
const ClutterColor *pick_color)
{
if (!clutter_actor_should_pick_paint (actor))
return;
ClutterActorBox allocation = { 0, };
gfloat width, height;
ClutterPath *path = STAR_ACTOR (actor)->priv->path;
clutter_actor_get_allocation_box (actor, &allocation);
clutter_actor_box_get_size (&allocation, &width, &height);
cogl_path_new ();
cogl_set_source_color4ub (pick_color->red,
pick_color->green,
pick_color->blue,
pick_color->alpha);
clutter_path_foreach (path, star_actor_convert_clutter_path_node, NULL);
cogl_path_fill ();
}
]]>
</programlisting>
</informalexample>
<note>
<para>The conversion function only covers
<type>ClutterPathNode</type> types encountered in this
actor.</para>
</note>
<para>Instead of converting to Cogl path operations, another alternative
would be to use the <function>clutter_path_to_cairo_path()</function>
function to write directly from the <type>ClutterPath</type>
onto a Cairo context.</para>
</section>
</section>
</section>
</chapter>