Artifact [f708ee15e7]

Login

Artifact f708ee15e7518c79bae21049b2c44de5cdc1b3205b983c3f5dd83cea6699eb16:


TIP:            48
Title:          Basic themeing infrastructure for Tk
Version:        $Revision: 1.8 $
Author:         Fr�d�ric Bonnet <[email protected]>
Author:         Fr�d�ric Bonnet <[email protected]>
State:          Draft
Type:           Project
Vote:           Pending
Created:        23-Jul-2001
Post-History:   
Discussions-To: news:comp.lang.tcl
Tcl-Version:    8.4

~ 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
mindshare, 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 to Windows, which borrowed
earlier to NeXTStep), it is now quite common to find huge visual differences
on today's desktops, even on same 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 mindshare, 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 suc 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 to 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 needed building blocks for theme
support that are both easy to implement and backward compatible.

A straightforward solution would be that introduced by the Dash-patch in the
form of new widget options such as ''-tile''. But 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 a little more and 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 mechanically 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 model 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 designate their subparts.

~ A quick look at existing implementations

The two most used tookits 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 (eg. 
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 (aka 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 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 classes 
names are arbitrary, and use a recursive dotted notation. For example, "arrow" 
designates a generic arrow element, and "Scrollbar.arrow" and "Combobox.arrow" 
designate 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.

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_ObjTypes. They can be 
registered at run-time, queried, but never unregistered.

 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 instanciated 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 into the widget record according to the widget's option 
table.

~ Detailed Specification

The proposal introduces a set of new public types and APIs, exported from 
''tk.h'' and the stubs table. The implementation induced very slight and limited 
changes to the existing code, with only one new private API added 
(''TkGetOptionSpec'' in tkConfig.c). Most added 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_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.

| typedef struct Tk_ElementOptionSpec {
|     char *name;
|     Tk_OptionType type;
| } 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 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));

 > Implementations of various element operations.

| typedef struct Tk_ElementSpec {
|     Tk_ElementOptionSpec *options;
|     Tk_GetElementSizeProc *getSize;
|     Tk_GetElementBoxProc *getBox;
|     Tk_GetElementBorderWidthProc *getBorderWidth;
|     Tk_DrawElementProc *draw;
| } Tk_ElementSpec;

 > Static styled element definition.

| #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

 > Flags used when drawing elements. Elements may have a slightly different 
visual appearance depending on their state.

''Functions''.

| void TkStylePkgInit (TkMainInfo *mainPtr)

| void TkStylePkgFree (TkMainInfo *mainPtr)

 > Internal procedures used to initialize the style subpackage on a
 per-application basis.

| CONST Tk_OptionSpec * TkGetOptionSpec (CONST char *name, 
| 	Tk_OptionTable optionTable);

 > Internal function used to retrieve an option specifier from a compiled option 
table.

| Tk_StyleEngine Tk_RegisterStyleEngine (char *name, Tk_StyleEngine parent)

 > Registers a new style engine. Name may be NULL, in which case it registers
the default engine. Returns a NULL token if an error occurred (eg, registering
an existing engine).

| Tk_StyleEngine Tk_GetStyleEngine (char *name)

 > Returns a token to an existing style engine, or NULL.

| int Tk_RegisterStyledElement (Tk_StyleEngine engine, char *name, 
| 	CONST Tk_ElementSpec *templatePtr)

 > Registers the implementation of an element for a given style engine. 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".

| int Tk_GetElementId (char *name)

 > Returns the unique numerical ID for a already registered element.

| Tk_Style Tk_CreateStyle (CONST char *name, Tk_StyleEngine engine, 
| 	ClientData clientData)

 > Creates a new style as an instance of an existing style engine. Client data
may be provided, that will be passed as is to element ops.

| Tk_Style Tk_GetStyle (Tcl_Interp *interp, CONST char *name)

 > Retrieves an existing style by its name, or NULL. In the latter case, leaves 
an error message in ''interp'' if not NULL.

| void Tk_FreeStyle (Tk_Style style)

 > Frees a style returned by ''Tk_CreateStyle'' or ''Tk_GetStyle''. It actually 
decrements an internal reference count so that styles can be shared and deleted 
safely.

| CONST char * Tk_NameOfStyle (Tk_Style style)

 > Gets a style's name.

| Tk_Style  Tk_AllocStyleFromObj (Tcl_Interp *interp, Tcl_Obj *objPtr)

| Tk_Style Tk_GetStyleFromObj (Tcl_Obj *objPtr)

| void  Tk_FreeStyleFromObj (Tcl_Obj *objPtr)

 > Tcl_Obj based interface to styles. ''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_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 found, traverse the engine chain (each engine has a parent) until 
the default engine is reached.

 > 3. Restart at 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

| 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)

 > Various element operations. 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 to 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 tk8.4a3 is available at:

 > http://www.purl.org/NET/bonnet/pub/style.patch

The ''square'' widget implemented in the test file ''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 instanciated 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

~ Performances and memory usage

The provided design and implementation is geared towards the best compromise 
between performances 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 
withing 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's option tables...). Widget processing times are mechanically 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 
needed 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 need 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 for 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 (eg 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
        of icons, colors, fonts, widget styles, or even desktop background and
        sounds. 

~ Copyright

This document has been placed in the public domain.