Flexible UI Toolkit Implementation

by havoc

I’m not the sort of programmer that runs around quoting design
patterns
and drawing UML diagrams.

However, maybe it’s useful to discuss this rule of thumb, in the
context of toolkits such as GTK+ and Clutter: It’s best to avoid
code that “knows about” the scene graph or toolkit as a whole.

Here are some approaches I like, along with examples that came to
mind. I hope nobody takes anything personally.

Containers forward recursively to children.

At the root of the tree, the root container (such as GtkWindow or
ClutterStage) gets context from the platform. It then passes
information or operations down to children of the root, the children
pass on beyond that, etc.

Example: the new GtkWidget draw() method passes down a cairo
context which is transformed as it goes down recursively.

Example: HippoCanvas
does both drawing and events in this way. Events are translated as
they go down through the tree.

Bad: gtk_propagate_event()
just walks the widget tree directly from outside the widget tree. This
logic makes it annoying to nicely integrate a widget in a new context,
such as in a Clutter scene graph.

Children bubble up information to containers.

Children can report news up to their parent container, which can
in turn hand it up the chain.

Example: clutter_actor_queue_redraw()
works by having each child notify its parent that it needs to
repaint.

Bad: gtk_widget_queue_draw_area()
walks up the widget tree to find a parent with a GdkWindow it can poke
at. Instead, the container that has the window should contain the
logic to invalidate the window, and nobody else in the tree should
know anything about GdkWindow. (Note how the existence of GdkWindow has
been leaked out so all widgets have to know about it. In the
alternative design, only widgets that have windows need to
know about them.)

Interfaces provided by containers.

A special case of bubbling up to containers is to ask containers for
an interface to use – delegate to the parent, in effect.

Rather than using global singletons, child actors or widgets can ask
their container for what they need. Containers then have the
opportunity to override anything that the child sees by reimplementing
the interface, possibly delegating most methods back up to the
implementation they received from their own parent.

Example: HippoCanvasContext
is a grab-bag interface provided by containers to children, providing
a very reduced and simplified set of things a widget might need, such
as a Cairo surface or Pango layout specialized for the window system –
or print-to-PDF operation – currently in progress. The
context is used to obtain a PangoLayout or a Cairo context, which are
also abstractions. The parent container can pick the right settings
and backend for Pango and Cairo.

Bad: gtk_grab_add()
goes straight to the toplevel window and then to a global list of
window groups. Instead, widgets could ask their container for a grab,
each container could do grabbing within that container, and it would
recursively move up to the toplevel; the toplevel would then deal with
the window groups. By delegating this, it could even be possible to
make grabs work on a tree that contains non-GtkWidget in it.

Use interfaces rather than concrete objects.

I don’t see a lot of value to making a specific control, such
as a GtkTextView, into an interface. However, the main “touch points”
that form the core of a toolkit really ought to be. This includes
GtkWidget, ClutterActor, and all the global state getters and setters
(which in turn should come from the parent container, rather than
using singletons).

Interfaces let you make bridges between “worlds.” If you’re putting a
widget or actor into a nonstandard context, whether it’s a PDF
printout, or a container that rotates or clips or clones, or drawing
an actor inside a widget or a widget inside an actor, the cleanest
solution will involve reimplementing interfaces to match the context.

Use model-view rather than omniscient objects.

This one seems obvious, but isn’t always done.

Bad: gtk_main_do_event()
hardcodes knowledge of other bits of GTK+, for example invoking _gtk_settings_handle_event() and
_gtk_clipboard_handle_event(). These should be connecting to a signal
so they don’t leak out of their own modules. gtk_propagate_event() is
another nasty piece of non-modularity. Future direction: add event
signals to GdkWindow and GdkDisplay and drop this central dispatch mess.

Bad: Clutter
master clock
should be a model, not a controller. It knows about
stages and timelines specifically and just tells them what to do.
Instead of saying “anything that does repainting, repaint now” it says
“repaint the stage.” If this code were model-view, it could simply be
dropped into GTK+ and used there as well, for example. Flexibility.

Summary

Replace code that knows about “the world” with:

  • Containers that know about only their immediate children.
  • Children that know only about their parent and purpose-built interfaces
    provided by the parent.
  • Models with multiple views.

Widgets and actors should know about their parents and their children,
never their grandparents, never their grandchildren, and certainly not
strangers they met in a singleton bar.

Getting this right in Clutter and GTK+ would make
both toolkits more robust in situations where the two toolkits are
mixed – something I’d like to see much more of – and in situations
where the toolkits are used in odd ways, such as in a
window/compositing manager, or just in any creative way that isn’t
quite what was intended. In short, getting this right makes the code
more modular and reusable and clear, not to mention less fragile.

Disentangling widgets and actors from the “global view of the world”
is great for unit
testing
, as well.

(This post was originally found at http://log.ometer.com/2010-09.html#19)

My Twitter account is @havocp.
Interested in becoming a better software developer? Sign up for my email list and I'll let you know when I write something new.