Control surfaces are geometrical regions within a widget that can position, size and draw themselves. Additionally, they can define their own behavior. They do all this via callbacks and event-handling flags that are supplied when the surface is created.
Control surfaces let you redefine the behavior for any area within a widget's drawable extent. Additionally, they can draw themselves as well as calculate their own geometry. Conceptually, they can be considered as lightweight “widgets within widgets.”
For example, consider a scroll bar. You get different actions, depending on where you click on it: the arrow buttons step up and down; clicking in the trough pages up and down; dragging on the handle scrolls as you move. PtScrollbar is implemented as a single widget with several control surfaces on it.
You could also use control surfaces for:
It's important to note that control surfaces are a property of a widget; they require a widget in order to exist. However, a widget can possess any number of control surfaces, making it possible to implement a whole user interface using only one widget (say a PtWindow) at a fraction of the runtime data size (8% being a reasonable upper bound) as opposed to implementing the same UI using widgets.
There are a few limitations to control surfaces:
You can bind control surfaces to any of a widget's predefined actions or to user-defined actions.
The types of control surfaces are:
You can refer to a control surface via:
While the pointer method is more direct and therefore quicker, it's not as safe as the ID method. To understand why, consider how control surfaces are organized and stored in memory.
Unlike the widget hierarchy, which is implemented as a linked list, control surfaces are stored as an array of surface structures (PtSurface_t). The major reasons for storing them this way are:
As you physically move control surfaces around in the stacking order, their placement in the array changes, affecting their address in memory. In addition, as you add or remove control surfaces to or from a widget, the array needs to be reallocated, which also may cause the array itself to move around in memory. With all this possibility of memory movement, numerical identifiers are the only reliable way of locating a surface.
If you're pretty certain that a widget's surface configuration isn't going to change, then the pointer method is safe (and quicker, since the ID method needs to do a linear lookup in the surface array).
The functions listed below are described in the Photon Library Reference.
The following functions create and destroy control surfaces:
To find surface and action IDs, use these functions:
You must provide a function that calculates the control surface's geometry. Control surfaces are asked to calculate their geometry twice when the widget that owns them is asked to calculate its geometry:
The post argument that's passed to the geometry function tells you which case is in progress.
A surface may also calculate its geometry based on the geometry of other surfaces. Using PtCalcSurface() or PtCalcSurfaceById(), you can ensure that the surface you're interested in has calculated its geometry prior to examining it.
The actual recording of the surface's geometry is simply a matter of directly modifying the surface's points array. Be sure you know how this array is organized before proceeding. This organization is detailed in the documentation for PtCreateSurface().
These functions deal with a control surface's geometry:
Control surfaces are asked to draw themselves from back to front, after the widget itself has drawn. No clipping is done for you. If you want clipping, you have to implement the necessary logic to adjust the clipping list as surfaces are traversed, and then reinstate the clipping stack after the last surface is drawn. Otherwise, you'll get some unexpected results.
The following functions damage control surfaces so they'll be redrawn:
This function activates a control surface:
You can enable and disable control surfaces, like widgets:
To find a control surface, use these functions:
You can hide and show control surfaces, too:
Like widgets, you can stack control surfaces:
There's no callback data in the function associated with control surfaces; you can store user data with control surfaces by calling:
Here's a program that creates some control surfaces:
#include <Pt.h> /* This is the function that's called when an event occurs for our rectangular control surface. When a user clicks on this surface, we'll tally up the clicks and print how many have occurred. */ static int rect_surface_callback( PtWidget_t *widget, PtSurface_t *surface, PhEvent_t *event) { static int rclicks = 1; printf("Rectangle clicks: %d\n", rclicks++); return(Pt_END); } /* This is the function which draws the contents of our rectangular control surface. This is a very simple example; it draws a red rectangle. */ static void rect_surface_draw( PtWidget_t *widget, PtSurface_t *surface, PhTile_t *damage) { PgSetFillColor(Pg_RED); PgDrawRect(PtSurfaceRect(surface, NULL), Pg_DRAW_FILL); } /* This is the function keeps the size of the control surface in sync with the size of the widget. PtWidgetExtent() returns a rect containing the current size of the widget. PtSurfaceRect() is a macro; this means that you have direct access to the data within your control surface. You do not need to call any functions to change its size. Change the data directly. */ static void rect_surface_calc( PtWidget_t *widget, PtSurface_t *surface, uint8_t post) { /* Do this only after widget has extented. */ if(post) { /* The rect occupies the top left quadrant of the window. */ PhRect_t *extent; PhRect_t *srect; extent = PtWidgetExtent(widget, NULL); srect = PtSurfaceRect(surface, NULL); srect->ul = extent->ul; srect->lr.x = (extent->ul.x + extent->lr.x) / 2; srect->lr.y = (extent->ul.y + extent->lr.y) / 2; } } /* This is the function that's called when an event occurs for our elliptical control surface. When a user clicks on this surface, we'll tally up the clicks and print how many have occurred. */ static int ell_surface_callback( PtWidget_t *widget, PtSurface_t *surface, PhEvent_t *event) { static int eclicks = 1; printf("Ellipse clicks: %d\n", eclicks++); return(Pt_END); } /* This is the function that draws the contents of our elliptical control surface. This is a very simple example; it draws a green ellipse. */ static void ell_surface_draw( PtWidget_t *widget, PtSurface_t *surface, PhTile_t *damage) { PhRect_t *s = PtSurfaceRect(surface, NULL); PgSetFillColor(Pg_GREEN); PgDrawEllipse(&(s->ul), &(s->lr), Pg_DRAW_FILL | Pg_EXTENT_BASED); } /* This is our main function. We create a window, initialize our application with the Photon server and create two control surfaces. Notice that the second surface doesn't supply the last parameter, the extent calculation function. This isn't needed because of the fifth parameter, the height and width stored in a point structure. This is a pointer to the actual point structure within the window widget. Thus, if the window's extent changes, changing the extent point structure, the control surface is automatically updated with the new values! */ int main(int argc, char **argv) { PtArg_t args[1]; PtWidget_t *window; const PhDim_t dim = { 200, 200 }; PtSetArg(&args[0], Pt_ARG_DIM, &dim, 0); window = PtAppInit(NULL, &argc, argv, 1, args); /* Create a rectangular control surface. */ PtCreateSurface( window, 0, 0, Pt_SURFACE_RECT, NULL, Ph_EV_BUT_PRESS, rect_surface_callback, rect_surface_draw, rect_surface_calc); /* Create an elliptical control surface to fill the window. */ PtCreateSurface( window, 0, 0, Pt_SURFACE_ELLIPSE, (PhPoint_t*)PtWidgetExtent(window, NULL), Ph_EV_BUT_PRESS, ell_surface_callback, ell_surface_draw, NULL); PtRealizeWidget(window); PtMainLoop(); return(EXIT_SUCCESS); }