GUI Scripting: Syntax, Semantics, & Usage

From The DarkMod Wiki
Jump to navigationJump to search

This page is part of a series. See GUI Scripting Language for overview.


This is a comprehensive listing of "rules of thumb" to successfully author a GUI. Much of this is also discussed elsewhere, but gathered here for convenience. Aspects that need attention because they are most likely to cause problems are in bold.

CAUTION: Some of these "rules" are based on behavioral and usage observations, in some cases generalizations made from scant experimental evidence. So future refinement may be in order.

See also GUI Scripting: Names & Case Sensitivity, about when keywords and other names of things in the GUI are case-sensitive or insensitive, and related naming conventions and styles. Case issues can cause parser fails!

Nesting & Layout

A GUI always start with a windowDef, at the top-level (outermost) level of nesting. The top-level's "rect" property will define the logical co-ordinate system within it, and is almost always:

rect 0, 0, 640, 480

Within that, nested child guiDefs have rect coordinates that are relative to their immediate parent's origin. You will need to think about what that means for the child's absolute boundaries. Most layouts strive to have a given child's absolute boundaries not exceed it's immediate parent's absolute boundaries. But sometimes exceeding is necessary to achieve desired margins or word wraps.

A given GUI can define multiple, potential conflicting layouts, as long as only 1 unconflicted layout is "active" at time. By "active" here, it is meant it has "visible 1" set throughout its nest of guiDefs. (A given guiDef may not be perceptually visible if it's a transparent overlay without text content). This can be managed by having a script toggle some boolean gui:: parameters that are used to initialize various "visible" properties.

In a tree of active guiDefs, rendering occurs in file order, which is depth-first. For a simple hierarchy, this means that a child will render atop its parent. As another example, if two active guiDefs resolve to the same absolute screen coordinates, the one listed later will be on top.


Registers and Non-Registers - Types, Defaults, Scope of Access

  • This "GUI Scripting" series treats "Properties" as a category including "Registers" and non-registers, as opposed to applying only to non-registers.
  • Property names and meanings are predefined.
  • A property has a predefined type, either float (including int), boolean, string, vect4, or (rarely) vect2. This type must be respected during initialization and use. (Internally, boolean properties are largely one of 12 specific bits in a "flags" variable. The usual C conventions pertain for conversions between booleans and ints.)
  • Properties and their values appear in a property list, by convention at the beginning of the body of a guiDef.
  • By convention, each property (name and value) is on its own line. NO TRAILING ";"
  • However, a group of properties may be on a single line, separated by spaces (not semicolons), as part of a #define.
  • By convention, the "rect" register is first, and the "visible" register is second; thus these are not defaulted.
  • Within a particular guiDef, if a property is not listed, it is still present, with a defined default value. Default values are generally:
    • "" for string
    • 0 for float, which includes int
    • 0 (false) for boolean
    • 1,1,1,1 for 4-vector. If color = opaque white
    • 0,0 for 2-vector. Such as for parameters origin, shear
  • Exceptions have these defaults:
    • visible 1 // = true
    • forceaspectwidth 640
    • forceaspectheight 480
    • matscalex 1.0
    • matscaley 1.0
    • rect 0,0,640,480
    • textscale 0.35
    • backcolor 0,0,0,0 // black but transparent
    • background NULL
    • name // gets guiDef <name>, unless explicitly overwritten
  • Properties can be listed in any order, but if duplicated, a later value overwrites the previous value.
  • Properties that are Registers are true variables, that can be read or written throughout the GUI (taking scope into account).
  • Properties that are non-Registers are more like tags. They can only be initialized, not read or written anywhere else. Specifically not in event handlers.
  • The scope to access a register by its unprefixed name is the current guiDef only, including its event handlers, but not higher or lower levels of guiDef nesting.
  • Within a given GUI, one guiDef can access another’s register with a "<guiDef-name>::" prefix.


  • Property initialization can be done from:
    • a literal value
    • a GUI:: Parameter (set by an associated .script function, entity spawnarg, or globally by the SDK)
    • for non-strings, mathematical combinations of those, using common C-language operators and parenthetical nesting.
  • Property initialization may be problematic from other local registers, those accessed by a "guiDef-name::" prefix, or user variables. If you need to have a register reflect the value of one of those sources, do it in a "set" command.
  • When initializing a string property, the value MUST be in double-quotes. Examples:
text "Heading"
font "fonts/stone"
background	"gui::BriefingVideoMaterial" // background string bound to and initialized by a script function of associated entity.
text	"gui::gui_parm1" // "gui_parm" prefix used so that text can be initialized from an associated entity’s spawnarg.
  • The C convention is followed of "\" being an "escape" character. The common usage is to include "\n" in a "text" property string to force a new line. Use "\\" to mean just "\".
  • Exception, included for completeness: following C conventions, a single literal character may be in single-quotes. This includes an escaped character like '\n'.
  • String properties support TDM's internationalization using the "#str_n" form.

  • When initializing a float (or variant such as int or bool) with a literal, the number is NOT quoted:
visible 0  // Boolean
visible		1 // tabs used as separator
textalign 2 // enum represented by int
textscale 1.5 // Decimal notation is supported, scientific notation is not.
  • However, if the value comes from a GUI:: Parameter (or one or more of them in a math expression), the source names are in double quotes:
visible "gui::LootIconVisible"
visible 1*"gui::GridItem0_ItemStackable"
visible "gui::Inventory_GroupVisible"*"gui::Inventory_ItemVisible"
visible		("gui::WeaponNameVisible")
visible		("gui::video_aspectratio" == 3)
visible	( ("gui::map_loading" >= 0.05) &&  ("gui::map_loading" < 0.10) )
visible		"gui::HUD_LightgemVisible" // This is a global GUI:: Parameter set by the SDK and mirroring a CVar
  • A string value can be converted to a float when needed. For example, in mainmenu_settings.gui, multiple languages are supported by adjusting button centers and widths accordingly. Here's one of the buttons being positioned:
set "gui::audio_pos" "#str_02240"; // = "310" in english.lang, "320" in german.lang
  • When initializing a vector, use comma-separated numbers, with or without white space:
forecolor 1,1,1,1 // RGBA of text
forecolor 0, 0, 0, 0.66 // with spaces too
matcolor 1, 1, 1, "gui::HUD_Opacity" // alpha channel set by global GUI:: parameter in Settings
#define S1 "gui::iconSize"
rect 5*S1, (80 - 7 * "gui::Inventory_ItemNameMultiline")*S1, 110*S1, 22*S1
shear 0.5,0 // vect2

Updating of Initial Value

  • A Property's initialization value, if based on literal(s) or an entity spawnarg (with "gui::gui_parm..." naming), is simply set once when the GUI is first parsed. But if changable GUI:: parameters are involved, then the value is potentially re-evaluated for changes on each frame. Example:
text "gui::Inventory_ItemName"

Scope of Influence

  • In general, the values of properties affect only the layout and behavior of the area defined by the guiDef’s rectangle. But there are exceptions.
  • For instance, "rect" is a relative location. So to calculate the absolute screen location, the parent’s guiDef’s absolute location must be known, calculated from its "rect". And so on recursively up the hierarchy.
  • "Bordersize" draws pixels outside the "rect" dimensions.
  • Properties "forceaspectx" and "forceaspecty" in some cases need to be set in the parent guiDef to take effect in a child guiDef.

For More


User Variables

  • You invent the name (unique within its guiDef) of a user variable. It should start with a letter and be composed of alphanumeric characters or underline. Avoid names of properties or other keywords.
  • Correct syntax for "float" (or "definefloat") user variable is:
float		winAlpha 0
float		isDown;
  • Traditional best-practice guidance is:
    • A float user variable is ALWAYS initialized to zero. Attempting to use any other number (before TDM 2.11) is often ignored.
    • It must be ended in either ";" or " 0". Ignore any documentation suggesting otherwise.
    • This is because, although initialization with a non-zero value was intended with Doom 3, buggy implementation made that unreliable.
  • Code refactoring in TDM 2.11 may now make non-zero initialization reliable. Too early to tell.
  • The "definevect" user variable seems to be avoided, so maybe you should too.
  • User variables are true variables, like registers.
  • The scope to access a user variable by its unprefixed name is the current guiDef only, including its event handlers, but not higher or lower levels of guiDef nesting.
  • Within a given GUI, one guiDef can access another’s user variable with a "<guiDef-name>::" prefix.
  • The primary use case is as a Boolean, to control actions that should only be done once, or for processing across multiple event handlers.
  • As an interesting non-Boolean example, consider the standard picture to help the user adjust the gamma level in Settings. A user variable "winAlpha" is used to adjust the transparency of background and text. The relevant properties can access the variable, even though the variable is listed later than them.
 windowDef GammaStandardPicture {
   backcolor 0,0,0,"winAlpha"
   forecolor 1,1,1,"winAlpha"
   float winAlpha 0
   // Not shown: event handlers setting actual values of winAlpha

For More

See GUI Scripting: User Variables

Event Handlers

This section could use expansion. For more now, see:

Mouse and Keyboard Events

The handling of events generally is quite complicated. Each guiDef type has its own event handler. Just a few simplified generalizations:

  • For a non-desktop guiDef to participate in mouse and keyboard event handling, it must be both active ("visible 1") and with "noevents 0".
  • At any given time, at most one guiDef can have the keyboard "focus". Focus can be set by LMB click or tabbing.
  • Independent of that, at most one guiDef can have the mouse cursor considered "over" it (i.e., within its boundaries and considered on top). When a mouse cursor moves from being "over" one guiDef to another, the onMouseExit handler (if present) is called on the former, and the onMouseEnter handler (if present) is called on the latter. If a handler is not present, the event is unhandled, not propagated to its parent (I think).

onTime Events

  • By design, the value of "noevents" does not affect onTime events.
  • The "notime" property controls whether the window's local timer is running:
notime 1

Evidently each guiDef has its own timer [although how that happens is not evident to me from the code, where it looks like all the guiDefs of a GUI would share a timer. I'm missing something.] Time begins at zero, and may be reset to zero with the "resetTime" command. The timer is nominally updated once per frame, reflecting the number of milliseconds between updates. On update, all guiDefs are inspected - independently of value of property "visible" - to see if any particular onTime event handler needs to fire.

GUIs associated with the main menu system usually start with "onTime 10" instead of "onTime 0", to allow engine startup needed to handle "set 'cmd' log..." commands. More generally, small time offsets are used to ensure order of triggering handlers. Thus, imagine you have originally 50 "onTime 0" handlers, but you want some of them evaluated after the others; time offsets solve this.

By convention, a given guiDef's onTime handlers are listed in increasing time order.

Within Event Handler Bodies

Set Command

In the second parameter, a "$" in front indicates that it's not the literal string that is the value. For instance, here, leftCapital:text is not set to literal string "gui::left_capital", but rather to its value:

set "leftCapital::text" "$gui::left_capital";

Problems within Event Handler Bodies

It's so easy to forget the ";" after each command.

Just-Changed Value Not Immediately Known to "If" Expression

If a user variable or register property is changed within an event handler (with "set" or "transition"), that change will not be detected by an "if" further down in the same instance of the handler. Once the handler is done, the change becomes effective. This unusual behavior unfortunately impedes using the complex "if" logic that is normal in other languages.

In TDM 2.11+, an expression can also occur, wrapped in parentheses, on the "right-hand" side of a "set" commands. Like an expression in an if-condition, it will likewise get "early evaluation" when the handler starts, and not detect changes that precede it within the event handler body.

For more about this, see GUI Scripting: Evaluating Expressions & Variables.

No Conversion from Vector to String

In a set command, trying to cast a vector to a string will fail:

set "Debug::text" "$InventoryItemIcon::rect"; // FAILS

Limited Access to Vector Components

Unlike Quake 4, Doom 3 & TDM has no support for the use of suffixes "_x", "_y", "_z", "_w" for individual vector components.

Starting with 2.11, vector components can be read on the "right-hand side" (i.e., source parameter) of the "set" command, provided you enter "expression mode" by using enclosing parentheses:

set backcolor (backcolor[0], backcolor[1], backcolor[2], 1 - backcolor[3]);

This capability does not extend to the "transition" command, nor the "left-hand side" (i.e., sink or target parameter) of "set".

Unknown Identifiers

An alphanumeric token that is unknown (such as a misspelled property or non-register used as if a register) will sometimes quietly be taken as a float with value 0, messing up your logic in an event handler. That’s the best case scenario.