Author: Frédéric Bonnet <[email protected]>Author: Frédéric Bonnet <[email protected]>State: FinalType: ProjectVote: DoneCreated: 23-Jul-2001Post-History: Discussions-To: news:comp.lang.tclTcl-Version: 8.4Implementation-URL: http://www.purl.org/NET/bonnet/pub/style.patchThe Tk Toolkit is one of the last major GUI toolkits lacking themessupport. This TIP proposes several changes to widget design thatallows custom code to be provided for widget element handling in atransparent and extensible fashion. User-provided code may then beused to alter the widgets' look without the need to alter the Tk core.The proposed changes induce no loss of compatibility, and only slightcore changes are needed with no side effect on existing functionality.
The Tk Toolkit appeared on X-Window systems at a time where Motif wasthede facto standard for GUI development. It thus naturallyadopted Motif's look&feel and its famous 3D border style. First portsto 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 theseplatforms.
Recently, other Open Source toolkits such as Qt (used by the KDEproject) and GTK (used by the GIMP graphics editing software and theGnome project) emerged as powerful and free alternatives to Motif forX-Window GUI development. The rapidly growing success of Open Sourcesystems such as GNU/Linux helped both toolkits attract a vastcommunity of developers, and the firm (and sometimes friendly)competition between both communities led to an explosion of newfeatures. Thirst for freedom and customizability created the need forthemeability.
The current implementation of Tk only provides native look&feel onsupported platforms (Windows, X-Window, MacOS). This lack partlyexplains 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 visualuniformity (Qt and GTK borrowed much of their visual appearance fromWindows, which borrowed earlier from NeXTStep), it is now quite commonto find huge visual differences on today's desktops, even on similarsystems. Screenshot contests are quite common nowadays.
Tk first kept away from the toolkit war. Tk's and its competitors'philosophies are radically opposite. Tk favors high levelabstractions and scripting languages such as Tcl, whereas Qt and GTKdevelopments are primarily done using C or C++ (which Tcl/Tk advocatesbelieve to be The Wrong Way). But despite Tk's power, flexibility andease of use, it has lost serious mind-share, especially amongstnewcomers and Linux users who don't care about its cross-platformcapabilities.
Many Tk users may see themes support as cosmetic or of lowerimportance than much needed features such as megawidgets orobjectification. Nevertheless, this is a critical feature to beimplemented for the long-term viability of Tk. Many courses are nowpromoting Qt, GTK or (aarggg!) Swing in place of Motif, leaving noroom for Tk. Whatever its qualities (cross-platform, performance,ease of use, internationalization and Unicode support), the lack ofthemeability will always be seen as one of the main reasons for notusing Tk. Applications using Tk instead of GTK will look as "foreign"on pixmap-themed Linux desktop, or even on newer MacOS and Windowsversions, 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 bespecified for all widgets. What is currently missing is pixmapthemeing and border styles. The current proposal describes therequired building blocks for theme support that are both easy toimplement and backward compatible.
A straightforward solution would be the one introduced by theDash-patch in the form of new widget options such as-tile. Thisapproach suffers from several major drawbacks:
A lot of new options are needed to handle the many ways of drawingpixmap tiles, such as anchoring, repeating, or scaling.
With the introduction of new options such as-activebackground, tile-related options must be duplicated foreach widget state (normal, active, disabled...), thus clutteringthe options namespace more and thus raising the learning curve.
Applying a theme to a whole widget hierarchy implies traversingthe whole tree and applying a lot of options to each widget.
Memory consumption is increased for all widgets, even in the casewhen these options are not used.
Moreover, one of the main goals of a theme being to enforce overallvisual consistency, multiplying new options should be avoided. Atheme is designed to gather these options into one place so that theycan be shared by numerous widgets while avoiding performance or memoryhit. A carefully designed theme engine should then only add one newoption per widget to set itsstyle (an essential part of a theme).
How far should themeabitily go? A previous version of this documentproposed to extend the current 3D border mechanism to allow customdrawing code. Although this proposal was simple, backward compatibleand covered most of the needs for themeability (border style oftenrepresents the largest part of the visual appearance), it failed toaddress other significant parts of the user interface. These includeradio and check marks, scrollbar arrows, sliders, and other widgetelements. From this point of view, the border is only anelement of a widget. A complete theme engine should then alloweach UI element to be customized, while maximizing code reuse andpreserving compatibility. To suit this model, widgets should then bethought of as assembly of elements, and no more as monolithicconstructs. This implies a paradigm shift in the way widgets aredesigned (but not necessarily in the way they areused).Actually, the notion ofelement is not foreign to Tk, since somewidgets (scrollbars) use the same term to identify their subparts.
The two major toolkits supporting widget styles are Qt and GTK+. Bothseem to follow the same path, but in slightly distinct manners: theydefine a fixed set of common elements (arrows, checkmarks...) andassociate each with one or several API calls. While Qt follows theOO-path, GTK+ uses a more traditional procedural API model.
Qt defines a genericQStyle class which is the base class for allstyles (Windows, Motif...). QStyle-derived classes implement a numberof virtual member methods, each being used to draw or compute thegeometry of the many elements. Thanks to polymorphism, widgets canthen use any style derived from this base class.
Contrary to the C++ -based Qt that defines a class gathering allstyle-related methods, GTK+ is C-based and defines individualprocedures (e.g.gtk_draw_slider).
But overall, both use the same model: a predefined (albeit potentiallyextensible) set of elements, and associated overloadablemethods/procs. Adding new elements implies recompilation and/or codechanges. While it is hardly seen as a problem with Qt and GTK+, sinceboth target C/C++ programming, it doesn't fit the Tcl/Tk model at all.
This document describes a generic and extensible element handlingmechanism. This mechanism allows elements to be created and/oroverloaded at run-time in a modular fashion.
Widgets are composed of elements. For instance, a scrollbar is madeout of arrows, a trough, and a slider. Each element must be declaredprior to being used. Elements are designated by a unique name andform a global pool that can be accessed by any widget. Elements maybe generic or derived. Elements and class names are arbitrary, anduse a recursive dotted notation. For example, "arrow" identifies ageneric arrow element, and "Scrollbar.arrow" and "Combobox.arrow"identify derived, widget specific elements.
Elements are declared along with an implementation. This declarationcan be made by the system or by widgets themselves, and at run-time,thus allowing extensions to create new and use or derive existingelements.
Implementations are registered in a given style engine. A styleengine is thus a collection of element implementations. Style enginescan be declared at run-time as well, but are static (since theyprovide compiled code). Style engines can be layered in order toreuse and redefine existing elements implementations, falling back tothe default, core-defined engine.
A style is an instance of a style engine. Styles can be given clientdata information that would carry style engine-specific data. Forexample, a style engine implementing pixmapped elements could be giventhe pixmaps to use. Styles can be created and deleted at run-time.
Using this scheme, a widget can register elements and their defaultimplementation, but actually use a custom implementation code in atransparent manner depending on its currently applied style.Moreover, elements can be shared across widgets, new elements can beregistered dynamically and used transparently. New widgets could alsobe built in a modular fashion and easily reuse other widget'selements. The proposed mechanism could then be used in amegawidget-like fashion (we could speak about megaelement widgets).Last, it provides a dynamic hook mechanism for overriding the corewidget code from loadable extensions, avoiding the need formaintaining core patches.
Style engines: Style engines gather code for handling a set ofelements. 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 engineswill usually be provided by loadable packages, and that Tcldoes not support library unloading.
Styles: Styles are instances of style engines. While engines arestatic, styles can be dynamic. All styles of the same engineuse the same code for handling elements, but using differentdata provided at creation-time. For example, a generic pixmapengine may be instantiated by several styles providing adifferent set of pixmaps. Styles can be created at run-time,queried, and freed. Since they are user-visible entities, aTcl_Obj-based interface is also provided.
Elements: Elements are virtual entities. An element only exists ifan implementation has been provided. Thus, elements arecreated implicitly. They can be queried, but not destroyed.Upon creation, elements are given a unique ID that remainsvalid for the entire application life time and is usedsubsequently for all related calls. It serves as a numericalindex for fast lookup into internal tables.
Styled elements: Styled elements provide implementations of elementsfor a given style engine. For this reason, they areinherently static. They can be registered at run-time,queried, but never unregistered. Upon registration,corresponding elements are implicitly registered. A styledelement must provide a set of functions for various operationson elements, such as geometry computation and drawing. Sinceelements can be used on various widgets, a styled element mustalso provide a list of required widget options. Elementswould then pick the option values from the widget recordaccording to the widget's option table. In the case when thedesired option is missing from the option table, the elementwould have to either try other options of fail gracefully anduse sensible default values.
The proposal introduces a set of new public types and APIs, exportedfromgeneric/tk.h and the stubs table. The implementation inducesvery slight and limited changes to the existing code, with only onenew private API added (TkGetOptionSpec ingeneric/tkConfig.c).Most of the new code is concentrated into one file. There is no sideeffect on existing functionality.
Types and constants.
TK_OPTION_STYLE:NewTk_OptionType usually associated with the-stylewidget 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 tothe current version. Future extensions may introduce new version numbers.
Tk_StyleEngine:Opaque token for handling style engines. May be NULL, meaningthe default system engine.
Tk_StyledElement:Opaque token holding a style-specific implementation of agiven element. Subsequently used for performing element ops.
Tk_Style:Opaque token for handling styles. May be NULL, meaning thedefault 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 withtheir type. This info will be subsequently used to get optionvalues 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 adifferent visual appearance depending on their state.However, it should be noted that the element size is notaffected 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 ona per-application basis.
void TkStylePkgInit (TkMainInfo *mainPtr) void TkStylePkgFree (TkMainInfo *mainPtr)TkGetOptionSpec:Internal function used to retrieve an option specifier from acompiled 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 defaultengine. 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 styleengine.
int Tk_RegisterStyledElement (Tk_StyleEngine engine, Tk_ElementSpec *templatePtr)Element names use a dotted notation that gives a hierarchicalsearch order. For example, a widget requiring an elementnamed "Scrollbar.vslider" can actually use the "vslider"generic element. Apart from this dotted notation, elementnames 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 thereforeregister an element named "Scrollbar".
The given pointer is not stored into internal structures, butis instead used to fill them. Styled element specs can thusbe allocated on the stack or dynamically, but in most casesthey will be statically defined.
Tk_GetElementId:Returns the unique numerical ID for an already registeredelement.
int Tk_GetElementId (char *name)Tk_CreateStyle:Creates a new style as an instance of an existing styleengine.
Tk_Style Tk_CreateStyle (CONST char *name, Tk_StyleEngine engine, ClientData clientData)Client data may be provided, that will be passed as is toelement 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 ifnone was found. In the latter case, leaves an error messageininterp if it is not NULL.
Tk_FreeStyle:Frees a style returned byTk_CreateStyle orTk_GetStyle.
void Tk_FreeStyle (Tk_Style style)It actually decrements an internal reference count so thatstyles 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 existingstyle from an object.Tk_GetStyleFromObj returns thestyle already stored in the object's internal representation.The object must have been returned byTk_AllocStyleFromObj.Tk_FreeStyleFromObj frees thestyle held by the object.
Tk_GetStyledElement:Returns a token for the styled element for use with widgetshaving the givenoptionTable.
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 tokenis persistent and doesn't need to be freed, so it can besafely stored if needed (although using element IDs is thepreferred method). It is used in subsequent elementoperations and avoids repeated lookups. The lookup algorithmworks as follows:
Look for an implementation of the given element in the currentstyle engine.
If none was found, traverse the chain of engines (each but thedefault engine has a parent) until the default engine isreached.
Restart at step 1 with the base element name instead. Forexample, 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 thegeneral case, this won't happen for core widgets (because theyonly use core elements), and new widgets either have to relyon 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 oneonly computes the size, while second one computes the boxcoordinates. Theinner parameter is a boolean thatcontrols whether the inner (FALSE) or outer (TRUE) geometry isrequested from the maximum outer/minimum inner geometry.Third one returns the uniform internal border width of theelement and is mostly intended for whole widgets. Last onedraws the element using the given geometry and state.
An implementation has been written and completed with respect to thepresent specification. A patch for Tk 8.4a3 is available at:
Thesquare widget implemented in the test filegeneric/tkSquare.c has also been rewritten to use the new API forits square element. It demonstrates basic features. Patch file:
The sample code registers an element "Square.square" in the defaultstyle engine. This element is used by the square widget in itsdrawing code. A new style engine "fixedborder" is registered, andcode is provided for the "Square.square" element. This style enginedraws the element's border using a fixed border width given as clientdata by instantiated styles. Four styles are created as instances ofthe "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 border8The provided design and implementation is geared towards the bestcompromise between performance and memory consumption.
Critical performance bottleneck is element querying. In order tominimize element access times, elements are identified by unique IDsthat act as indexes within internal tables, allowing directaddressing. Hash tables are used internally by all name pools(engines, styles, elements). Static structures are used wheneverpossible (for styled element registration, indirectly throughwidgets' option tables...). Widget processing times are increased bythe extra procedure calls and indirections, but that is the price topay for better modularity anyway. Additional calls are kept minimal.
Per-widget memory consumption is minimal. A widget usually only needsto store its current style. Element IDs can (should?) be sharedglobally across widgets of the same class and don't need to be storedin the widget record. Moreover, most information is shared internallyacross widgets of the same class (identified by their option table).Many caching & fast lookup techniques are used throughout the code.
Existing widgets will need to be rewritten in order to becomestyle-aware. The required code changes may be significant (implyingcode modularization). However, no incompatibility is introduced.Thus, migrating widgets from the old to the new model can follow asmooth path, similar to that needed for the transition toTcl_Objinterfaces. Besides, widgets as a whole can act as elements, whichmay shift the amount of work from the core to the style engines at theexpense of a lesser modularity and code reuse.
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 neededelement options, e.g.-elementborderwidth =>-borderwidth.
How to handle native widgets? They will certainly have to beprovided as whole elements.
Current implementation uses thread-local storage for holdingdynamic data. Since most data is not thread-specific, this couldbe 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 optiontables.
Element: Part of a widget (e.g. a checkbox mark or a scrollbararrow), 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.
This document has been placed in the public domain.