TIP 48: Tk Widget Styling Support

Login
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:

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

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.