Tutorials
Hello, World!
The GUI version of the 'Hello, World!' program:
ui = require "tek.ui" ui.Application:new { Children = { ui.Window:new { Title = "Hello", Children = { ui.Text:new { Text = "Hello, World!", Class = "button", Mode = "button", Width = "auto" } } } } }:run()
As can be seen, tekUI allows a fully functional application to be
written in a single nested expression. The
UI library comes with an on-demand class
loader, so whenever a class (like
Application,
Window or
Text) is accessed for the first
time, it will be loaded from tek/ui/class/
in the file system.
Note that a button class is not strictly needed: a button is just a Text element behaving like a button with a frame giving it the appearance of a button. We will later explain how you can write a button class yourself, to save you some typing.
To quit, click the window's close button. Closing the
Application's last
open window will cause the run
method to return to its caller.
Reacting on input
There are different ways for reacting to presses on the 'Hello,
World' button. The simplest form is to place an onClick
function into the Text object:
ui = require "tek.ui" ui.Application:new { Children = { ui.Window:new { Title = "Hello", Children = { ui.Button:new { Text = "Hello, World!", Width = "auto", onClick = function(self) print "Hello, World!" end } } } } }:run()
But onClick
is just a convenient shortcut for the most trivial
of all cases. To catch more events, you can override handler
functions which react on changes to an element's state variables,
like Pressed
and Selected
, to name but a few. They contain
booleans and are indicative of the Element's state, such as: Is it in
selected state, is it pressed down, is it hovered by the mouse, is it
receiving the input? The
Widget:onPress handler, for example,
can be used to catch not only releases, but also presses on the
element:
ui = require "tek.ui" ui.Application:new { Children = { ui.Window:new { Children = { ui.Text:new { }, ui.Button:new { Text = "Click", Width = "auto", onPress = function(self) ui.Button.onPress(self) self:getPrev():setValue("Text", tostring(self.Pressed)) end } } } } }:run()
When you overwrite a handler, you should forward the call to the original implementation of the same method, as seen in the example.
Setting a value using Object:setValue may invoke a notification handler. In our example, the neighboring element in the group will be notified of an updated text, which will cause it to be repainted.
For regular applications it is normally sufficient to stick to overwriting the available handlers as in the previous example. But the underlying mechanism to register a notification handler can be interesting as well, especially if you plan on writing new GUI classes yourself:
ui = require "tek.ui" app = ui.Application:new() win = ui.Window:new { Title = "Hello", HideOnEscape = true } text = ui.Button:new { Text = "_Hello, World!", Width = "auto" } text:addNotify("Pressed", false, { ui.NOTIFY_SELF, ui.NOTIFY_FUNCTION, function(self) print "Hello, World!" end }) win:addMember(text) app:addMember(win) app:run()
See also Object:addNotify for all the hairy details on notification handlers, and the Area and Widget classes for the most important state variables.
Controlling the layout
By default a tekUI layout is dynamic. This means it will be calculated and, if necessary, recalculated at runtime¹. Screen and font characteristics are taken into account as well as style properties controlling the appearance of individual elements.
Group layouting attributes
Orientation
-"horizontal"
or"vertical"
, the default is"horizontal"
. This attribute informs the group of the primary axis on which elements are to be layouted (left-to-right vs. top-to-bottom). This applies to grids also.Columns
- Number of columns. A number greater than1
turns the group into a grid.Rows
- Number of rows. A number greater than1
turns the group into a grid.Layout
- The name of a layouting class. Default:"default"
, which will try to load thetek.ui.layout.default
class. It is also possible to specify an instance of a layouter instead of just the class name.SameSize
- boolean,"width"
or"height"
. Default is false. If true, all elements in the group will have the same size on their respective axis. If either of the string keywords is given, this applies to only to the width or height.
Common layouting attributes (apply to all elements)
HAlign
- The element's horizontal alignment inside the group which it is part of. Possible values are"left"
,"center"
, and"right"
. The corresponding style property is halign.VAlign
- The element's vertical alignment inside the group which it is part of. Possible values are"top"
,"center"
, and"bottom"
. The corresponding style property is valign.Width
- The width of the element, in pixels, or"auto"
to reserve the minimum size,"free"
for allowing the element to grow to any size, or"fill"
for allowing the element to grow to no more than the maximum width that other elements in the same group have claimed. The corresponding style property is width.Height
- The height of the element, in pixels, or"auto"
to reserve the minimum size,"free"
for allowing the element to grow to any size, or"fill"
for allowing the element to grow to no more than the maximum height that other elements in the same group have claimed. The corresponding style property is Height.MinWidth
- The minimum width of the element, in pixels. The default is0
. The corresponding style property is min-width.MinHeight
- The minimum height of the element, in pixels. The default is0
. The corresponding style property is min-height.MaxWidth
- The maximum width of the element, in pixels, or"none"
for no limit (which is the default). The corresponding style property is max-width.MaxHeight
- The maximum height of the element, in pixels, or"none"
for no limit (which is the default). The corresponding style property is max-height.
Note that the Min
/Max
and Width
/Height
properties
will not override the actual size requirements of an element. An
element will not claim a larger or smaller size than what it is
capable of displaying. Style properties will be used as additional
hints when an element's size is flexible. As most elements are
scalable by nature, the style properties are normally considered.
¹ More layouting options are available, see also the fixed.lua
and layouthook.lua
examples on how to use and implement fixed
and custom layouting strategies.
List of predefined handlers
Name | Base Class | Cause |
Widget:onActivate() | Widget |
change of the Active attribute
|
Window:onChangeStatus() | Window |
change of Status attribute
|
Widget:onClick() | Widget |
caused when Pressed changes to false
|
Widget:onDisable() | Widget |
change of the Disabled attribute
|
Widget:onDblClick() | Widget |
change of the DblClick attribute
|
Input:onEnter() | Input |
change of Enter attribute, pressing enter
|
Widget:onFocus() | Widget |
change of the Focus attribute
|
Window:onHide() | Window | window close button, Escape key |
Widget:onHilite() | Widget |
change of the Hilite attribute
|
Widget:onHold() | Widget |
change of the Hold attribute
|
Widget:onPress() | Widget |
change of the Pressed attribute
|
Widget:onSelect() | Widget |
change of the Selected attribute
|
DirList:onSelectEntry() | DirList | item selected by the user |
Lister:onSelectLine() | Lister |
change of the SelectedLine attribute
|
PopList:onSelectLine() | PopList |
change of the SelectedLine attribute
|
Input:onSetChanged() | Input |
setting the Changed attribute, on text changes
|
Canvas:onSetChild() | Canvas |
change of the Child attribute
|
Element:onSetClass() | Element |
change of the Class attribute
|
Lister:onSetCursor() | Lister |
change of the CursorLine attribute
|
ImageWidget:onSetImage() | ImageWidget |
change of the Image attribute
|
Numeric:onSetMax() | Numeric |
change of the Max attribute
|
ScrollBar:onSetMax() | ScrollBar |
change of the Max attribute
|
Numeric:onSetMin() | Numeric |
change of the Min attribute
|
ScrollBar:onSetMin() | ScrollBar |
change of the Min attribute
|
PageGroup:onSetPageNumber() | PageGroup |
change of the PageNumber attribute
|
ScrollBar:onSetRange() | ScrollBar |
change of the Range attribute
|
Slider:onSetRange() | Slider |
change of the Range attribute
|
Element:onSetStyle() | Element |
change of the Style attribute
|
FloatText:onSetText() | FloatText |
change of Text attribute
|
Input:onSetText() | Input |
change of Text attribute
|
Text:onSetText() | Text |
change of Text attribute
|
Numeric:onSetValue() | Numeric |
change of the Value attribute
|
ScrollBar:onSetValue() | ScrollBar |
change of the Value attribute
|
Ad-hoc setup of classes
To inherit properties and functionality from existing classes and to consequently reuse existing code, it is often desirable to create new classes yourself. There are different scopes in which new classes can be useful:
- Global classes are written as separate source files, located in the system-wide installation path under
tek/ui/class
and set up using a procedure as described in the class setup section.- Application classes are created in the same way, but they are located in
tek/ui/class
relative to the application's local program directory.- Another scope is inside a running application or module. We call this the ad-hoc style, because new classes are often created out of a spontaneous need.
For the ad-hoc style, it is not necessary to create a new source file or module. For example, a Button class can be derived from the Text class whereever you see fit:
local Button = ui.Text:newClass { _NAME = "_button" }
ad-hoc classes may be named arbitrarily, but their names should be prefixed with an underscore to distinguish them from global classes. You can even do without a name, as tekUI will create one for you if necessary, but you will find it difficult to reference such a class in a style sheet.
From this point, the new class can be extended, e.g. for initializations which turn a Text into a Button:
function Button.init(self) self.Class = "button" self.Mode = self.Mode or "button" self.KeyCode = true return ui.Text.init(self) end
As shown in the example, we also passed the call on to our super class, which we expect to perform the missing initializations.
Finally, a new object from our new class can be created:
button = Button:new { Text = "_Hello, World!" }
Also refer to the Class reference and the Class setup section for further information.
The lifecycle of an element
A GUI element is set up in several stages, all of which are initiated by the tekUI framework. Normally, you do not call any of these methods yourself (aside from passing a call on to the same method in your super class):
- 1. Object.init() - gets called when an element is created
- 2. Element:connect() - connects the element with a parent element
- 3. The element's properties are decoded.
- 4. Element:setup() - registers the element with the application
- 5. Area:askMinMax() - queries the element's minimal and maximal dimensions
- 6. The window is opened.
- 7. Area:show() - gets called when the element is about to be shown
- 8. Area:layout() - layouts the element into a rectangle
- 9. Area:draw() - paints the element onto the screen
- 10. Area:hide() - gets called when the element is about to hide
- 11. The window is closed.
- 12. Element:cleanup() - unregisters the element
- 13. Element:disconnect() - disconnects the element from its parent
Drawing an element
In the drawing method, the control flow is roughly as follows:
function ElementClass:draw() if SuperClass.draw(self) then -- your rendering here return true end end
There are rare cases in which a class modifies the drawing context, e.g. by setting a coordinate displacement. Such modifications must be performed in Area:drawBegin() and reverted in Area:drawEnd(), and the control flow looks like this:
function ElementClass:draw() if SuperClass.draw(self) then if self:drawBegin() then -- your rendering here self:drawEnd() end return true end end
Debug library
The debug library used throughout tekUI is
tek.lib.debug. The default debug
level is 10 (ERROR
). To increase verbosity, set level
to
a lower value, either by modifying tek/lib/debug.lua
, or by
setting it after including the module:
db = require "tek.lib.debug" db.level = db.INFO
See also the module's documentation for redirecting the output.
Proxied object model
If you wish to use validation of XML files against tekUI's implementation, of if you plan on extending existing classes or develop your own, you are advised to set the following configurable parameters in tek.class, the base class of all tekUI classes:
local PROXY = true local DEBUG = true
The PROXY
option allows for intercepting read/write accesses to
objects, which will be harnessed by the DEBUG
option for tracking
accesses to uninitialized class members. So whenever a nil
value is read from or written to an object, this causes tek.class
to bail out with an error and a meaningful message.
As a result, all member variables must be initialized during
new()
or init()
– or more specifically, before the class
metatable is attached and an object is becoming fully functional.
This will assist in keeping variables neatly together, and you won't
end up in a fluff of variables of limited scope and significance,
getting initialized at random places. This also means that you cannot
assign a distinct meaning to nil for a class member – you will
have to use false instead, or find another arrangement. (This
convention of not using nil for class variables is found
throughout the whole tekUI framework.)
Once your application is tested and ready for deployment, you can
disable PROXY
, as this will improve performance and reduce memory
consumption.
Class setup
A class is usually set up in a prologue like this:
local Widget = require "tek.ui.class.widget" module("tek.ui.class.button", tek.ui.class.widget) _VERSION = "Button Widget 1.0" local Button = _M Widget:newClass(Button)
The second argument to module
is the super class to derive the
new class from (see also tek.class for
details on how this is supposed to work). By convention, we then put
the module table (the class) into a local variable. The last line is
for compatibility with Lua 5.2, as the second argument to module
doesn't work with Lua 5.2 anymore.
Finally, methods in the newly created class may look like this (note
that, thanks to the Button
variable, the second example provides
an implicit self
):
function Button.new(class, self) ... return Widget.new(class, self) end function Button:method() Widget.method(self) ... end
Also, don't forget to add a _VERSION
variable, as it will be used
by the documentation system – see also the next section.
Class documentation system
Don't stray off too far from the class setup described in the previous section, as it contains valuable informations for tekUI's documentation generator.
Most notably, the second argument to module
should be written out
in full – in the example above, one might be tempted to use
Widget
instead of tek.ui.class.widget
; but then, the path
information would be lost for the source code parser, which tries to
assemble a self-contained class hierarchy from individual class /
child class relations.
Tokens for markup
Aside from the aforementioned module
and _VERSION
keys (see
section Class setup), the source code parser reacts
on the following tokens.
Long lines of dashes signify the beginnings and endings of comment blocks that are subject to processing markup notation, e.g.
---------------------------------------------------------------- -- OVERVIEW:: -- Area - implements margins, layouting and drawing ----------------------------------------------------------------
The other condition that must be met for the following text to appear in the documentation is the recognition of either a definition (as seen in the example) or function marker inside such a comment block. The template for a definition is this:
DEFINITION::
And the function template:
ret1, ret2, ... = function(arg1, arg2, ...): ...
The marker and the following text will then become part of the documentation. (In other words, by avoiding these markers, it is also possible to write comment blocks that do not show up in the documentation.)
Functions inside classes will automatically receive a symbolic name
as their class prefix (from assigning the module table _M
to a
local variable, see Class setup). Hereinafter, they
can be cross-referenced using the following notations:
Class:function() Class.function()
For further information, consult the sources in the
class hierarchy as examples, and the source code
containing the
markup notation reference, which
can be found in tek.class.markup
.
5.6. Stylesheets precedence
TekUI uses a cascading stylesheets (CSS) engine, which borrows from the W3C recommendations. The order of precedence is as follows:
- Hardcoded class defaults
- User agent: tekUI's built-in stylesheets, either
"default"
or"minimal"
. The implied default is"default"
, unless the first word in theTHEME
variable is"minimal"
."minimal"
is hardwired into the ui library, while"default"
is an actual stylesheet file.- User: Names of stylesheet files from the
THEME
environment variable, e.g."stain gradient"
. The default is"desktop"
.- Author: Names of stylesheet files specified in
Application.AuthorStyleSheets
.- Author: Styles specified in
Application.AuthorStyles
.- User important: The
user.css
stylesheet.
To shut off interferences from user styles, use a predefined cascade
by overwriting the THEME
variable inside your application, e.g.:
local ui = require "tek.ui" ui.ThemeName = "mycompany" -- also implies "default" ui.UserStyles = false -- to also disable the user.css file
See also Global Lua runtime arguments.