# TIP 48: Tk Widget Styling Support
Author: Frédéric Bonnet <[email protected]>
Author: Frédéric Bonnet <[email protected]>
State: Final
Type: Project
Vote: Done
Created: 23-Jul-2001
Post-History:
Discussions-To: news:comp.lang.tcl
Tcl-Version: 8.4
Implementation-URL: http://www.purl.org/NET/bonnet/pub/style.patch
-----
# Abstract
The Tk Toolkit is one of the last major GUI toolkits lacking themes
support. This TIP proposes several changes to widget design that
allows custom code to be provided for widget element handling in a
transparent and extensible fashion. User-provided code may then be
used to alter the widgets' look without the need to alter the Tk core.
The proposed changes induce no loss of compatibility, and only slight
core changes are needed with no side effect on existing functionality.
# Background
The Tk Toolkit appeared on X-Window systems at a time where Motif was
the _de facto_ standard for GUI development. It thus naturally
adopted Motif's look&feel and its famous 3D border style. First ports
to non-X platforms such as Windows and MacOS kept the Motif style,
which disappointed many users who felt Tk applications look "foreign".
Version 8.0 released around 1996 added native look&feel on these
platforms.
Recently, other Open Source toolkits such as Qt \(used by the KDE
project\) and GTK \(used by the GIMP graphics editing software and the
Gnome project\) emerged as powerful and free alternatives to Motif for
X-Window GUI development. The rapidly growing success of Open Source
systems such as GNU/Linux helped both toolkits attract a vast
community of developers, and the firm \(and sometimes friendly\)
competition between both communities led to an explosion of new
features. Thirst for freedom and customizability created the need for
themeability.
The current implementation of Tk only provides native look&feel on
supported platforms \(Windows, X-Window, MacOS\). This lack partly
explains Tk's loss of mind-share, especially amongst Linux developers,
where theme support is considered a "cool" or must-have feature.
While yesterday's goal of many GUIs was cross-platform visual
uniformity \(Qt and GTK borrowed much of their visual appearance from
Windows, which borrowed earlier from NeXTStep\), it is now quite common
to find huge visual differences on today's desktops, even on similar
systems. Screenshot contests are quite common nowadays.
# Rationale
Tk first kept away from the toolkit war. Tk's and its competitors'
philosophies are radically opposite. Tk favors high level
abstractions and scripting languages such as Tcl, whereas Qt and GTK
developments are primarily done using C or C\+\+ \(which Tcl/Tk advocates
believe to be The Wrong Way\). But despite Tk's power, flexibility and
ease of use, it has lost serious mind-share, especially amongst
newcomers and Linux users who don't care about its cross-platform
capabilities.
Many Tk users may see themes support as cosmetic or of lower
importance than much needed features such as megawidgets or
objectification. Nevertheless, this is a critical feature to be
implemented for the long-term viability of Tk. Many courses are now
promoting Qt, GTK or \(aarggg!\) Swing in place of Motif, leaving no
room for Tk. Whatever its qualities \(cross-platform, performance,
ease of use, internationalization and Unicode support\), the lack of
themeability will always be seen as one of the main reasons for not
using Tk. Applications using Tk instead of GTK will look as "foreign"
on pixmap-themed Linux desktop, or even on newer MacOS and Windows
versions, as pre-8.0 applications were on non-X desktops.
The lack of themeability is neither a fatality nor difficult to solve.
Tk already allows colors, fonts and border width and relief to be
specified for all widgets. What is currently missing is pixmap
themeing and border styles. The current proposal describes the
required building blocks for theme support that are both easy to
implement and backward compatible.
A straightforward solution would be the one introduced by the
Dash-patch in the form of new widget options such as _-tile_. This
approach suffers from several major drawbacks:
* A lot of new options are needed to handle the many ways of drawing
pixmap tiles, such as anchoring, repeating, or scaling.
* With the introduction of new options such as
_-activebackground_, tile-related options must be duplicated for
each widget state \(normal, active, disabled...\), thus cluttering
the options namespace more and thus raising the learning curve.
* Applying a theme to a whole widget hierarchy implies traversing
the whole tree and applying a lot of options to each widget.
* Memory consumption is increased for all widgets, even in the case
when these options are not used.
Moreover, one of the main goals of a theme being to enforce overall
visual consistency, multiplying new options should be avoided. A
theme is designed to gather these options into one place so that they
can be shared by numerous widgets while avoiding performance or memory
hit. A carefully designed theme engine should then only add one new
option per widget to set its _style_ \(an essential part of a theme\).
How far should themeabitily go? A previous version of this document
proposed to extend the current 3D border mechanism to allow custom
drawing code. Although this proposal was simple, backward compatible
and covered most of the needs for themeability \(border style often
represents the largest part of the visual appearance\), it failed to
address other significant parts of the user interface. These include
radio and check marks, scrollbar arrows, sliders, and other widget
_elements_. From this point of view, the border is only an
_element_ of a widget. A complete theme engine should then allow
each UI element to be customized, while maximizing code reuse and
preserving compatibility. To suit this model, widgets should then be
thought of as assembly of elements, and no more as monolithic
constructs. This implies a paradigm shift in the way widgets are
_designed_ \(but not necessarily in the way they are _used_\).
Actually, the notion of _element_ is not foreign to Tk, since some
widgets \(scrollbars\) use the same term to identify their subparts.
# A quick look at existing implementations
The two major toolkits supporting widget styles are Qt and GTK\+. Both
seem to follow the same path, but in slightly distinct manners: they
define a fixed set of common elements \(arrows, checkmarks...\) and
associate each with one or several API calls. While Qt follows the
OO-path, GTK\+ uses a more traditional procedural API model.
Qt defines a generic _QStyle_ class which is the base class for all
styles \(Windows, Motif...\). QStyle-derived classes implement a number
of virtual member methods, each being used to draw or compute the
geometry of the many elements. Thanks to polymorphism, widgets can
then use any style derived from this base class.
Contrary to the C\+\+ -based Qt that defines a class gathering all
style-related methods, GTK\+ is C-based and defines individual
procedures \(e.g. _gtk\_draw\_slider_\).
But overall, both use the same model: a predefined \(albeit potentially
extensible\) set of elements, and associated overloadable
methods/procs. Adding new elements implies recompilation and/or code
changes. While it is hardly seen as a problem with Qt and GTK\+, since
both target C/C\+\+ programming, it doesn't fit the Tcl/Tk model at all.
# Proposal \(or There Must Be A Better Way\)
This document describes a generic and extensible element handling
mechanism. This mechanism allows elements to be created and/or
overloaded at run-time in a modular fashion.
Widgets are composed of elements. For instance, a scrollbar is made
out of arrows, a trough, and a slider. Each element must be declared
prior to being used. Elements are designated by a unique name and
form a global pool that can be accessed by any widget. Elements may
be generic or derived. Elements and class names are arbitrary, and
use a recursive dotted notation. For example, "arrow" identifies a
generic arrow element, and "Scrollbar.arrow" and "Combobox.arrow"
identify derived, widget specific elements.
Elements are declared along with an implementation. This declaration
can be made by the system or by widgets themselves, and at run-time,
thus allowing extensions to create new and use or derive existing
elements.
Implementations are registered in a given style engine. A style
engine is thus a collection of element implementations. Style engines
can be declared at run-time as well, but are static \(since they
provide compiled code\). Style engines can be layered in order to
reuse and redefine existing elements implementations, falling back to
the default, core-defined engine.
A style is an instance of a style engine. Styles can be given client
data information that would carry style engine-specific data. For
example, a style engine implementing pixmapped elements could be given
the pixmaps to use. Styles can be created and deleted at run-time.
Using this scheme, a widget can register elements and their default
implementation, but actually use a custom implementation code in a
transparent manner depending on its currently applied style.
Moreover, elements can be shared across widgets, new elements can be
registered dynamically and used transparently. New widgets could also
be built in a modular fashion and easily reuse other widget's
elements. The proposed mechanism could then be used in a
megawidget-like fashion \(we could speak about megaelement widgets\).
Last, it provides a dynamic hook mechanism for overriding the core
widget code from loadable extensions, avoiding the need for
maintaining core patches.
# Functional Specification
Style engines: Style engines gather code for handling a set of
elements. For this reason, they are inherently static, alike
_Tcl\_ObjType_s. They can be registered at run-time,
queried, but never unregistered, since external style engines
will usually be provided by loadable packages, and that Tcl
does not support library unloading.
Styles: Styles are instances of style engines. While engines are
static, styles can be dynamic. All styles of the same engine
use the same code for handling elements, but using different
data provided at creation-time. For example, a generic pixmap
engine may be instantiated by several styles providing a
different set of pixmaps. Styles can be created at run-time,
queried, and freed. Since they are user-visible entities, a
_Tcl\_Obj_-based interface is also provided.
Elements: Elements are virtual entities. An element only exists if
an implementation has been provided. Thus, elements are
created implicitly. They can be queried, but not destroyed.
Upon creation, elements are given a unique ID that remains
valid for the entire application life time and is used
subsequently for all related calls. It serves as a numerical
index for fast lookup into internal tables.
Styled elements: Styled elements provide implementations of elements
for a given style engine. For this reason, they are
inherently static. They can be registered at run-time,
queried, but never unregistered. Upon registration,
corresponding elements are implicitly registered. A styled
element must provide a set of functions for various operations
on elements, such as geometry computation and drawing. Since
elements can be used on various widgets, a styled element must
also provide a list of required widget options. Elements
would then pick the option values from the widget record
according to the widget's option table. In the case when the
desired option is missing from the option table, the element
would have to either try other options of fail gracefully and
use sensible default values.
# Detailed Specification
The proposal introduces a set of new public types and APIs, exported
from _generic/tk.h_ and the stubs table. The implementation induces
very slight and limited changes to the existing code, with only one
new private API added \(_TkGetOptionSpec_ in _generic/tkConfig.c_\).
Most of the new code is concentrated into one file. There is no side
effect on existing functionality.
_Types and constants_.
TK\_OPTION\_STYLE:
New _Tk\_OptionType_ usually associated with the _-style_
widget option.
TK\_STYLE\_VERSION\_1, TK\_STYLE\_VERSION:
Version numbers of Tk style support. The former matches the
implementation described in this proposal. The latter is a shortcut to
the current version. Future extensions may introduce new version numbers.
Tk\_StyleEngine:
Opaque token for handling style engines. May be NULL, meaning
the default system engine.
Tk\_StyledElement:
Opaque token holding a style-specific implementation of a
given element. Subsequently used for performing element ops.
Tk\_Style:
Opaque token for handling styles. May be NULL, meaning the
default system style.
Tk\_GetElementSizeProc, Tk\_GetElementBoxProc, Tk\_GetElementBorderWidthProc, Tk\_DrawElementProc:
Implementations of various element operations.
typedef void (Tk_GetElementSizeProc) _ANSI_ARGS_((ClientData clientData,
char *recordPtr, CONST Tk_OptionSpec **optionsPtr, Tk_Window tkwin,
int width, int height, int inner, int *widthPtr, int *heightPtr));
typedef void (Tk_GetElementBoxProc) _ANSI_ARGS_((ClientData clientData,
char *recordPtr, CONST Tk_OptionSpec **optionsPtr, Tk_Window tkwin,
int x, int y, int width, int height, int inner, int *xPtr, int *yPtr,
int *widthPtr, int *heightPtr));
typedef int (Tk_GetElementBorderWidthProc) _ANSI_ARGS_((ClientData clientData,
char *recordPtr, CONST Tk_OptionSpec **optionsPtr, Tk_Window tkwin));
typedef void (Tk_DrawElementProc) _ANSI_ARGS_((ClientData clientData,
char *recordPtr, CONST Tk_OptionSpec **optionsPtr, Tk_Window tkwin,
Drawable d, int x, int y, int width, int height, int state));
Tk\_ElementOptionSpec:
Used to specify a list of required widget options, along with
their type. This info will be subsequently used to get option
values from the widget record using its option table.
typedef struct Tk_ElementOptionSpec {
char *name;
Tk_OptionType type;
} Tk_ElementOptionSpec;
Tk\_ElementSpec:
Static styled element definition. The version field must be set to
TK\_STYLE\_VERSION\_1 in order to match the following structure.
typedef struct Tk_ElementSpec {
int version;
char *name;
Tk_ElementOptionSpec *options;
Tk_GetElementSizeProc *getSize;
Tk_GetElementBoxProc *getBox;
Tk_GetElementBorderWidthProc *getBorderWidth;
Tk_DrawElementProc *draw;
} Tk_ElementSpec;
TK\_ELEMENT\_STATE\_\*:
Flags used when drawing elements. Elements may have a
different visual appearance depending on their state.
However, it should be noted that the element size is not
affected by state changes.
#define TK_ELEMENT_STATE_ACTIVE (1<<0)
#define TK_ELEMENT_STATE_DISABLED (1<<1)
#define TK_ELEMENT_STATE_FOCUS (1<<2)
#define TK_ELEMENT_STATE_PRESSED (1<<3)
_Functions_.
TkStylePkgInit, TkStylePkgFree:
Internal procedures used to initialize the style subpackage on
a per-application basis.
void TkStylePkgInit (TkMainInfo *mainPtr)
void TkStylePkgFree (TkMainInfo *mainPtr)
TkGetOptionSpec:
Internal function used to retrieve an option specifier from a
compiled option table.
CONST Tk_OptionSpec * TkGetOptionSpec (CONST char *name,
Tk_OptionTable optionTable);
Tk\_RegisterStyleEngine:
Registers a new style engine.
Tk_StyleEngine Tk_RegisterStyleEngine (char *name, Tk_StyleEngine parent)
> Name may be NULL, in which case it registers the default
engine. Returns a NULL token if an error occurred \(e.g.
registering an existing engine\).
Tk\_GetStyleEngine:
Returns a token to an existing style engine, or NULL.
Tk_StyleEngine Tk_GetStyleEngine (char *name)
Tk\_RegisterStyledElement:
Registers the implementation of an element for a given style
engine.
int Tk_RegisterStyledElement (Tk_StyleEngine engine,
Tk_ElementSpec *templatePtr)
> Element names use a dotted notation that gives a hierarchical
search order. For example, a widget requiring an element
named "Scrollbar.vslider" can actually use the "vslider"
generic element. Apart from this dotted notation, element
names are free-form. However, conventions should be defined,
such as capitalized widget classes, and lower case elements.
Since whole widgets can act as elements, one can therefore
register an element named "Scrollbar".
> The given pointer is not stored into internal structures, but
is instead used to fill them. Styled element specs can thus
be allocated on the stack or dynamically, but in most cases
they will be statically defined.
Tk\_GetElementId:
Returns the unique numerical ID for an already registered
element.
int Tk_GetElementId (char *name)
Tk\_CreateStyle:
Creates a new style as an instance of an existing style
engine.
Tk_Style Tk_CreateStyle (CONST char *name, Tk_StyleEngine engine,
ClientData clientData)
> Client data may be provided, that will be passed as is to
element operations.
Tk\_GetStyle:
Retrieves an existing style by its name.
Tk_Style Tk_GetStyle (Tcl_Interp *interp, CONST char *name)
> Retrieves either an existing style by its name, or NULL if
none was found. In the latter case, leaves an error message
in _interp_ if it is not NULL.
Tk\_FreeStyle:
Frees a style returned by _Tk\_CreateStyle_ or
_Tk\_GetStyle_.
void Tk_FreeStyle (Tk_Style style)
> It actually decrements an internal reference count so that
styles can be shared and deleted safely.
Tk\_NameOfStyle:
Gets a style's name.
CONST char * Tk_NameOfStyle (Tk_Style style)
Tk\_AllocStyleFromObj, Tk\_GetStyleFromObj, Tk\_FreeStyleFromObj:
_Tcl\_Obj_ based interface to styles.
Tk_Style Tk_AllocStyleFromObj (Tcl_Interp *interp, Tcl_Obj *objPtr)
Tk_Style Tk_GetStyleFromObj (Tcl_Obj *objPtr)
void Tk_FreeStyleFromObj (Tcl_Obj *objPtr)
> _Tk\_AllocStyleFromObj_ gets \(doesn't create\) an existing
style from an object. _Tk\_GetStyleFromObj_ returns the
style already stored in the object's internal representation.
The object must have been returned by
_Tk\_AllocStyleFromObj_. _Tk\_FreeStyleFromObj_ frees the
style held by the object.
Tk\_GetStyledElement:
Returns a token for the styled element for use with widgets
having the given _optionTable_.
Tk_StyledElement Tk_GetStyledElement (Tk_Style style, int elementId,
Tk_OptionTable optionTable)
> Returns a token for the styled element \(or NULL if not found\),
for use with widgets having the given optionTable. The token
is persistent and doesn't need to be freed, so it can be
safely stored if needed \(although using element IDs is the
preferred method\). It is used in subsequent element
operations and avoids repeated lookups. The lookup algorithm
works as follows:
> 1. Look for an implementation of the given element in the current
style engine.
> 2. If none was found, traverse the chain of engines \(each but the
default engine has a parent\) until the default engine is
reached.
> 3. Restart at step 1 with the base element name instead. For
example, if we are looking for "foo.bar.baz", then look for
"bar.baz" then "baz", until we find an implementation.
> If no implementation was found, then a panic is generated,
meaning that some dependency has not been resolved. In the
general case, this won't happen for core widgets \(because they
only use core elements\), and new widgets either have to rely
on core or package-provided elements, or define their own.
Tk\_GetElementSize, Tk\_GetElementBox, Tk\_GetElementBorderWidth, Tk\_DrawElement:
Various element operations.
void Tk_GetElementSize (Tk_Style style, Tk_StyledElement element,
char *recordPtr, Tk_Window tkwin, int width, int height,
int inner, int *widthPtr, int *heightPtr)
void Tk_GetElementBox (Tk_Style style, Tk_StyledElement element,
char *recordPtr, Tk_Window tkwin, int x, int y, int width,
int height, int inner, int *xPtr, int *yPtr, int *widthPtr,
int *heightPtr)
int Tk_GetElementBorderWidth (Tk_Style style, Tk_StyledElement element,
char *recordPtr, Tk_Window tkwin)
void Tk_DrawElement (Tk_Style style, Tk_StyledElement element,
char *recordPtr, Tk_Window tkwin, Drawable d, int x, int y,
int width, int height, int state)
> The first two are used for geometry management. First one
only computes the size, while second one computes the box
coordinates. The _inner_ parameter is a boolean that
controls whether the inner \(FALSE\) or outer \(TRUE\) geometry is
requested from the maximum outer/minimum inner geometry.
Third one returns the uniform internal border width of the
element and is mostly intended for whole widgets. Last one
draws the element using the given geometry and state.
# Implementation
An implementation has been written and completed with respect to the
present specification. A patch for Tk 8.4a3 is available at:
> <http://www.purl.org/NET/bonnet/pub/style.patch>
The _square_ widget implemented in the test file
_generic/tkSquare.c_ has also been rewritten to use the new API for
its square element. It demonstrates basic features. Patch file:
> <http://www.purl.org/NET/bonnet/pub/squarestyle.patch>
The sample code registers an element "Square.square" in the default
style engine. This element is used by the square widget in its
drawing code. A new style engine "fixedborder" is registered, and
code is provided for the "Square.square" element. This style engine
draws the element's border using a fixed border width given as client
data by instantiated styles. Four styles are created as instances of
the "fixedborder" element: "flat", "border2", "border4" and "border8"
\(0, 2, 4 and 8 pixel-wide borders\).
Sample test session:
pack [square .s]
.s config -style
.s config -style flat
.s config -style border2
.s config -style border4
.s config -style border8
.s config -style ""
pack [square .s2]
.s2 config -style border2
.s2 config -style border8
# Performance and memory usage
The provided design and implementation is geared towards the best
compromise between performance and memory consumption.
Critical performance bottleneck is element querying. In order to
minimize element access times, elements are identified by unique IDs
that act as indexes within internal tables, allowing direct
addressing. Hash tables are used internally by all name pools
\(engines, styles, elements\). Static structures are used whenever
possible \(for styled element registration, indirectly through
widgets' option tables...\). Widget processing times are increased by
the extra procedure calls and indirections, but that is the price to
pay for better modularity anyway. Additional calls are kept minimal.
Per-widget memory consumption is minimal. A widget usually only needs
to store its current style. Element IDs can \(should?\) be shared
globally across widgets of the same class and don't need to be stored
in the widget record. Moreover, most information is shared internally
across widgets of the same class \(identified by their option table\).
Many caching & fast lookup techniques are used throughout the code.
# Compatibility
Existing widgets will need to be rewritten in order to become
style-aware. The required code changes may be significant \(implying
code modularization\). However, no incompatibility is introduced.
Thus, migrating widgets from the old to the new model can follow a
smooth path, similar to that needed for the transition to _Tcl\_Obj_
interfaces. Besides, widgets as a whole can act as elements, which
may shift the amount of work from the core to the style engines at the
expense of a lesser modularity and code reuse.
# Future improvements or changes
* Additional APIs for querying the list of engines, styles,
elements...
* Additional operations for elements, e.g. hit tests.
* Script-level interfaces.
* Optional translation tables between real widget options and needed
element options, e.g. _-elementborderwidth_ => _-borderwidth_.
* How to handle native widgets? They will certainly have to be
provided as whole elements.
* Current implementation uses thread-local storage for holding
dynamic data. Since most data is not thread-specific, this could
be changed to a more memory-efficient scheme.
* Provide man pages and tests.
* Additional hidden/private option flag for accessing some widgets'
non-configurable data \(e.g. scrollbar position\) through option
tables.
# Glossary
Element: Part of a widget \(e.g. a checkbox mark or a scrollbar
arrow\), usually active.
Style: The visual appearance of a widget. May include colors, skins,
tiles, border drawing style \(Windows, Motif...\), element
pictures.
Styled element: A style-specific implementation of a widget element.
Style engine: A visually consistent collection of styled elements.
Theme: A collection of graphical elements giving a consistent
appearance to a whole widget hierarchy, application, or
desktop. A theme is usually made up out of icons, colors,
fonts, widget styles, or even desktop background and sounds.
# Copyright
This document has been placed in the public domain.