Table of Contents

User Interface

The existing user interface of Transport Fever 2 can be extended as well as additional interface components can be added via modding.

Style Sheets

The visual style of user interface elements is controlled by style sheets in res/config/style_sheet/. These files are combined to an internal stylesheet by the game. To tweak some values or add new style definitions, add a custom stylesheet in your mod with a unique name.

require "tableutil"
local ssu = require "stylesheetutil"
 
function data()
  local result = { }
 
  local a = ssu.makeAdder(result)          -- helper function
 
  a(
    "ConstructionMenuTab::ButtonsLayout",  -- Selector
    {
      margin = { 0, 0, 30 + 35, 0 },       -- style
      ...
    }
  )
 
  return result
end

To generate the correct stylesheet information, usually a helper function in the stylesheetutil.lua is used. It receives two parameters: A selector and the actual styling.

Selectors

The selectors are similar to CSS selectors but have some smaller differences. The whole selector is a string and several comma seperated alternatives can be used with the same styleset.

Basically there are three ways to select an element:

To select by a hierarchy, you can seperate the levels with a blank character: Parent !child-class. To specifically address individual referenced elements, :: can be used: WarningsButton::Icon.

There are several pseudoclasses that can be used with selectors:

To find out the name or class of an ui element, you can use the ingame tools to pick elements. Then the name of the element that the cursor points on is visible in the console.

Styles

There are different kinds of styles that can be set for an element.

Size, Spacing and Padding

To define dimensions of an ui element, there are several properties:

From the frame that is defined by the size properties, there are offsets to the inside and to the outside:

The gravity is a vector with 2 float values that is used for the positioning of the element. It is a vector of two float numbers, one for the horizontal and one for the vertical positioning. Both can be set to 0.0 for left/top position, 0.5 for center positioning and 1.0 for right/bottom positioning. If set to -1, the element will grow in that dimension to fill up space.

For layout components, there are additional properties that control the spacing between the contained elements.

The scaling property contains a single float value that is used as a scaling factor on the whole element. Further scaling is applied through the global scale factor in the user settings.

Font

Elements with texts have several other properties:

Background



The background of ui elements has several layers: In the back, just above the shadow is a color only layer that is controlled by the backgroundColor 2.

Above the color backgrond, is the layer that is controlled by backgroundImage1 and backgroundColor1 3. backgroundImage1 has three properties:

If horizontal or vertical are set, the referenced image repeats the area between the second and third value until the image is large enough to fill the element. Otherwise it is used as a static image.

There is another background layer 4 that is controlled by backgroundImage2 and backgroundColor2 like the other background image layer.

Border and Shadow

For the border 5, there is a set of three properties available:

The properties for shadows 1 are defined identical to borders. Their names are shadowNinePatch, shadowColor and shadowWidth.

To blur the content behind the window, it is possible to set a blurRadius to some positive float value.

Transitions and Animations

Animations are keyframed changes of color properties. They require their own animation keyframe definition in result.animations:

result.animations.attention = {
  [.0] = {
    color = { 1.0, 1.0, 1.0, 1.0 },
    backgroundColor1 = { 1.0, .0, .0, .0 },
    backgroundColor2 = { 1.0, 1.0, 1.0, 1.0 },
  },
  [.5] = {
    color = { 1.0, .0, .0, 1.0 },
    backgroundColor1 = { 1.0, .0, .0, .5 },
    backgroundColor2 = { 1.0, .0, .0, 1.0 },
  },
  [1.0] = {
    color = { 1.0, 1.0, 1.0, 1.0 },
    backgroundColor1 = { 1.0, .0, .0, .25 },
    backgroundColor2 = { 1.0, .5, .5, 1.0 },
  }
}

This animation then is referenced by its key in the animationName property. The total duration of one animation playback is specified as a float value in animationDuration. It is possible to limit the number of loops with the animationIterationCount and the animation can use a non linear playback with animationTimingFunction. Possible values are:

Sound Effects

Some ui elements may play a sound when they are activated or the mouse is hovered over them. The references to the sounds are defined in the stylesheets too. There are two properties for sounds, soundEffect1 and soundEffect2. Which is used when depends on the ui element. To reference a sound, the get(reference) function from soundeffectsutil.lua is used. The reference is relative to res/audio/effects/. See the sound effect manual for further information regarding sound files.

Examples

Here are some examples that show simple use cases of stylesheet modding.

Hide UI in cockpit camera and camera sequence preview

This example hides the text that usually is shown while playing a keyframe sequence from the camera tool and the cockpit camera. It adds a stylesheet that overrides the styling of various gui components and setting their background and text color to full transparency.

This example is available for download.

UI Scripting

There are several ways of modifying or extending the user interface. Those ways all use the game scripts as a base.

Manipulating UI

The functions used to manipulate the user interface are available as part of the api.gui module. It provides functions to retrieve existing ui elements like api.gui.getById(<id>) as well as constructor functions to create new elements: api.gui.TextInputField.new(<id>). See the API reference for a more in detail description of all functions.

UI Game Script Callbacks

Beside the load callback, there are three different ui related callbacks that are called for different purposes:

GUI Init

Once the user interface is loaded, the guiInit callback is called once. It is a parameterless function:

function data()
  return {       
    guiInit = function()
      -- create and initialize ui elements
    end,
    ...
  }
end

GUI Update

The game regularily updates its interface. For this purpose, there is a guiUpdate callback for game scripts. It is a parameterless function too:

function data()
  return {       
    guiUpdate = function()
      -- update elements if neccessary   
    end,
    ...
  }
end

GUI Events

Beside the interval timed guiUpdate callback, there are some events which can be fetched with the guiHandleEvent callback. These are caused e.g. by button clicks. The callback is a function with three parameters:

function data()
  return {       
    guiHandleEvent = function(id, name, param)
      -- do something
    end,
    ...
  }
end

The properties are:

By default, the following gui events may occur (only on elements with an id or tag) :

Causing Element Event Name Parameters
“cutscene” “play” -
“cutscene” “end” -
“cutscene” “deactivate” -
“voiceover” “start” -
“voiceover” “end” -
“musicPlayer” “trackEnded” -
“voiceover” “end” -
CameraController, e.g. “mainView” “camera.keyScroll” true movement begins
false movement ends
CameraController, e.g. “mainView” “camera.mouseRotateTilt” true movement begins
false movement ends
CameraController, e.g. “mainView” “camera.mousePan” true movement begins
false movement ends
CameraController, e.g. “mainView” “camera.mouseWheel” float value >= 0 for scroll position
CameraController, e.g. “mainView” “hover” entity id or -1 for no entity
“constructionBuilder” “builder.apply” proposal contains the requested changes in abstract form.
data contains the concrete changes or error data.
result contains a list of new built and/or modified construction entities.
“constructionBuilder” “builder.proposalCreate” proposal contains the requested changes in abstract form.
data contains the concrete changes or error data.
“constructionBuilder” “builder.rotate” -
“streetTerminalBuilder” “builder.apply” proposal contains the requested changes in abstract form.
data contains the concrete changes or error data.
result contains a list of new built and/or modified construction entities.
“streetTerminalBuilder” “builder.proposalCreate” proposal contains the requested changes in abstract form.
data contains the concrete changes or error data.
“streetBuilder” “builder.apply” proposal contains the requested changes in abstract form.
data contains the concrete changes or error data.
result contains a list of new built and/or modified construction entities.
“streetBuilder” “builder.proposalCreate” proposal contains the requested changes in abstract form.
data contains the concrete changes or error data.
“streetBuilder” “builder.level” float value for slope level
“streetBuilder” “builder.slope” float value for slope gradient
“trackBuilder” “builder.apply” proposal contains the requested changes in abstract form.
data contains the concrete changes or error data.
result contains a list of new built and/or modified construction entities.
“trackBuilder” “builder.proposalCreate” proposal contains the requested changes in abstract form.
data contains the concrete changes or error data.
“trackBuilder” “builder.level” float value for slope level
“trackBuilder” “builder.slope” float value for slope gradient
“streetTrackModifier” “builder.apply” proposal contains the requested changes in abstract form.
data contains the concrete changes or error data.
result contains a list of new built and/or modified construction entities.
“streetTrackModifier” “builder.proposalCreate” proposal contains the requested changes in abstract form.
data contains the concrete changes or error data.
“bulldozer” “builder.apply” proposal contains the requested changes in abstract form.
data contains the concrete changes or error data.
result contains a list of new built and/or modified construction entities.
“bulldozer” “builder.proposalCreate” proposal contains the requested changes in abstract form.
data contains the concrete changes or error data.
“vehicleManager” “accept” entity id of existing vehicle if update else -1
vehicleConfig list of model references

By default, the following gui component events may occur on elements with id or tag:

Causing Element event name parameters
any Component “destroy” -
any Component “visibilityChange” true when becoming visible
false when becoming invisible
any Button Component “button.click” -
any List Component “list.select” integer value for the selected list index
any TabWidget Component “tabWidget.currentChanged” index integer value for the selected tab
id id of the selected tab element if existing
any ToggleButton Component “toggleButton.toggle” true if activated
false if deactivated
any Window Component “window.close” -

UI Component Callbacks

In addition to the global callbacks described above, some ui components have their own event callbacks, e.g. buttons have an onClick callback. Below is a table showing which callback type is available for which component.

Callback AbstractSlider Button Chart CheckBox ColorChooser ColorChooserButton ColorPicker ComboBox Component DoubleSpinBox GameUI ImageView ProgressBar RendererComponent ScrollArea Slider Slider2 SpinBox TabWidget Table TextInputField TextView Window
onChange
onClick
onClose
onColorChanged
onCurrentChanged
onDestroy
onIndexChanged
onMove
onSelect
onStep
onToggle
onValueChanged
onVisibilityChange

All these ui components inherit some properties from the general Component that supports three general callbacks:

A more in detail explanation of the other callbacks can be found in the API reference.

Examples

Here are some examples that show simple use cases of ui modding.

Clock (guiUpdate)

This example adds a game script that shows a real time clock in the bottom game info bar. It is a simple use case of the guiUpdate callback and the api.gui interface.

local clockstate = nil
 
function data()
  return {       
    guiUpdate = function()
      ...    
    end
  }
end

The local variable clockstate will hold a reference to the ui element that should be regularily updated. Then it is not neccessary to fetch the element reference on every update.

The callback contains several things. At first it is checked if the ui element was created before:

      ...
      if not clockstate then
 
        -- element for the divider
        local line = api.gui.comp.Component.new("VerticalLine")
 
        -- element for the icon
        local icon = api.gui.comp.ImageView.new("ui/clock_small.tga")
 
        -- element for the time
        clockstate = api.gui.comp.TextView.new("gameInfo.time.label")
 
        -- add elements to ui
        local gameInfoLayout = api.gui.util.getById("gameInfo"):getLayout()
        gameInfoLayout:addItem(line) 
        gameInfoLayout:addItem(icon) 
        gameInfoLayout:addItem(clockstate) 
 
      end
      ...

The three elements are created:

Then the existing layout in the bottom section is fetched. The id of the containing element can be gathered by using the debug tool window that provides console output when hovering over ui elements.

After that, the three elements are added to the layout. gameInfoLayout:addItem(line) is a shortened version of gameInfoLayout.addItem(gameInfoLayout, line).

The previous code section could be moved to a guiInit callback function too.

      ...
      local time = os.time()
 
      if clockstate and time then
        clockstate:setText(os.date('%H:%M', time))
      end
      ...

This section updates the actual text. At first it retrieves the current system time in milliseconds with os.time(). Then the text of clockstate is set to a formated version of the time displaying hours and minutes. Like above, a shortened version is used with : to omit the self referencing argument.

This example is available for download.

FPS-Counter (onStep)

This example adds a game script that shows an FPS counter in the bottom game info bar. It is a simple use case of the onStep callback from the TextView component and the api.gui interface.

local stopwatch = nil
 
local function updateText(totaltime, steptime)
  stopwatch:setText(math.floor(1000000/steptime + 0.5) .." FPS")
end
 
function data()
  return {       
    guiInit = function()
      ...    
    end
  }
end

The local variable stopwatch will hold a reference to the ui element that should be regularily updated. Then it is not neccessary to fetch the element reference on every update.

The function updateText is the function that is used for the onStep callback. It drops the first parameter and calculates the current frames per second (FPS) based on the length of the last step.

The callback guiInit contains the setup of the needed ui elements:

    guiInit = function()
 
      -- element for the divider
      local line = api.gui.comp.Component.new("VerticalLine")
 
      -- element for the icon
      local icon = api.gui.comp.ImageView.new("ui/fps.tga")
 
      -- element for the time
      stopwatch = api.gui.comp.TextView.new("")
      stopwatch:onStep(updateText)
 
 
      -- add elements to ui
      local gameInfoLayout = api.gui.util.getById("gameInfo"):getLayout()
      gameInfoLayout:addItem(line) 
      gameInfoLayout:addItem(icon) 
      gameInfoLayout:addItem(stopwatch) 
 
    end,
    ...
end

The three elements are created:

Finally the existing layout in the bottom section is fetched and the three elements are added to the layout.

This example is available for download.