Suboptimal Theming in GTK

Published on Monday, July 2, 2007

I am a fan of custom widgets. I write a lot of them. Most are not over the top, or even noticeable, but are useful. In fact, most of my widgets I more often call "composite" widgets instead of "custom." That is, a widget that does some common things to a group of child widgets. Not much involved, but reusable and useful in some context. Gtk# makes this kind of widget writing actually a simple and natural thing to do, unlike C, where you start your work by despising the task at hand. Ahhh - the usefulness of a good binding.

However, I have also written a number of full blown custom widgets, wherein I handle all input and drawing. It's this kind of custom widget I'd like to talk about today. Actually, let's just skip to the drawing part!

Ideally, when a custom widget needs to draw some control part, the first thing the author should do to accomplish this is look to see if there is some style that can be reused in GTK. Adhering to this guideline means the custom widget should look like the rest of GTK. It's good for integration on a number of levels. It's at this level that the problem arises: in many cases the theme engine will completely ignore the draw request!

In GTK there are a number of theme drawing primitives: hline, vline, shadow, arrow, box, check, focus, tab - to name a few. It's then up to the theme engine to actually draw the requested style. A "detail" can be passed to each of the drawing functions that the engine can use as a hint. I think these details are somewhat standard, but I can't find any documentation of them.

So what's the problem? I have come to the conclusion that either I do not know what I am doing or theme engine authors do not understand the object model and power of GTK itself.

For instanced, when I try to ask GTK to draw a box in the same style that it's drawn for the GtkTreeView, I get just a regular plain box. No special theming. Why? Because the engine itself is written to not allow this!

Take this snippet, from Clearlooks:

else if (DETAIL ("button") && widget && widget->parent &&
(GE_IS_TREE_VIEW(widget->parent) ||
GE_IS_CLIST (widget->parent) ||
ge_object_is_a (G_OBJECT(widget->parent), "ETree"))) /* ECanvas inside ETree */

Here it will only draw the requested style if it has the proper detail, and the widget happens to be a real GtkTreeView (among other special cases). This makes it impossible for custom widgets to be written which emulate the look of stock GTK. Evidence of this can be seen throughout the desktop.

Let's start with a stock GtkTreeView. Observe very carefully how the header looks. This is what the three custom widgets following have to try to mimic because it is impossible to actually request this style to be drawn.

GTK Headers in GtkTreeView

Here's Evolution's ETree headers. They use the "button" detail to actually draw each column header, probably because the author ran into this larger issue when writing the theming for the widget. In fact, there's a separate hard coded hack in Clearlooks itself (which can be seen in the snippet above) to draw the button in a special way just for ETree, implying that even the engine author knows this is an issue!

GTK Headers in ETree

For XUL, there is a theme that uses the GTK theming engine. Most widgets look okay, after padding and spacing hacks, but I point back to their list view. Here's a screenshot of how their headers are drawn. I talked with Garrett about this, and he said he remembers running into this very same issue when working on the Firefox/GTK theme.

GTK Headers in XUL

Finally, when working on my new high performance data binding 2.0 synergistic .NET list view widget, I again ran into this problem (I have run against similar problems in other custom widgets) when working on the drawing code for my headers. I used another hack to best replicate what I observed in most themes. I drew one large "button" box for the entire header at something like (-10, -10, Allocation.Width + 20, Allocation.Height + 20). This way there would be no button border or rounding, but you could get the subtle gradient/raised look that themes often draw. A nice hack that worked relatively well, but was still a nasty hack at the end of the day.

GTK Headers in Managed List View

This is just one example of inconsistency regarding custom widgets due to limitations in the theme engine. It's not just Clearlooks however. In the gtk-engines module, there is a support library of common functions that theme engines can use. There are a bunch of macros taking the form GE_IS*. These macros are used to determine the type of an object, so that theme parts can be drawn in different ways depending on what widget they are to be drawn.

Here's a really crappy shell command that counts all of the GE_IS* instances in gtk-engines to show how ingrained the idea of using an object's type to conditionally style it is:

(s=0; for v in $(grep -c GE_IS find -iregex "^.*.[ch]\(" | xargs` | cut -d':' -f 2); do s=\)((s + v)); done; echo $s)`

I'm sure there's an easier way to get the sum of the counts from grep, but I am just that lame. Anyway, I am counting 286. Wow.

My widget does not derive from GtkTreeView, so I cannot use the styles defined for it by the engine. Because of this, custom widgets immediately become second class citizens. This is bad for complex applications that may do their own theming, and probably ISVs if they care, and most certainly for other toolkits (XUL, QT, Wx) which can use the GTK engine to provide a theme that integrates applications consuming another toolkit with the rest of GTK.

This limitation is pointless to me. All of this object-hierarchy-bound conditional theming can be replaced with a much better detail system that would allow any widget to draw any style. Details should also be published and made standard so it is easy to replicate the styles of a particular widget. The bottom line is that the theme engine should never need to know, or at least never require a widget to be of a particular type for the desired style to be drawn.

There are also a lot of other minor issues, mainly regarding RC styles and proper descriptions of states and colors. There are many themes that do not follow the directions when defining colors for states, which leads to many inconsistencies in custom widgets. This just needs to be better documented.

Of course, and again, maybe I just "don't get it." After all I am no theming expert... What I do know is that if I can't achieve consistent widget theming in GTK, I might as well stop trying to fake it. I have decided, although tentatively, to just use Cairo to do all my drawing with my new list widget, and make it blend the best I can by reusing RC colors. Good-bye GTK Style API.