TheoTown features a powerful framework of user interface elements that are used throughout the whole game. The main goal within the past few weeks was to expose this functionality to plugins so that plugins can show fancy information dialog windows, offer tool options and so on. For information about specific commands please have a look at the GUI library documentation.
In order to use these functionalities you need at least version 1.9.30 of the game. Don't forget to set the min version attribute in the plugin upload process accordingly (to 1930) if you are using these features in your plugin. Alternatively you can check for the existence of individual GUI functions during runtime by ensuring that they are not nil.
- Introduction
GUI refers to graphical user interface and generally refers to the windows, buttons and texts you see on the screen. Their primary goal is to offer some sort of interface between the user and the program. It is usually implemented in a hierarchical way that allows to nest objects into each other which offers great flexibility. On the left side you see an example of what you would see on the screen. On the right side there's a potential hierarchical tree of objects for that screen outlined. We have one root object at the top that has every other object as a direct or indirect child below it. All objects share some common functionality like having a location (relative to its parent), size and potential children. The nesting of objects is not necessarily useful for all types of objects. For example there's usually no reason to put something into a label object.
The order of the children within an object matters since they are drawn and updated in that order. Because of that you can for example put a window into the foreground by changing its child index accordingly (higher index = drawn and updated later = on top of siblings that have a lower index). The order will also have an impact on the placement if the parent is a layout object.
Here's a small example of how nesting of objects may look like in your plugin code-wise :Here a panel would be added to the main root GUI object. After that, a button is added to that newly created panel.Code: Select all Reset
function script:buildCityGUI() local root = GUI.getRoot() local panel = root:addPanel{ x = 20, y = 20, width = 100, height = 30 } local button = panel:addButton{ icon = Icon.OK, text = 'OK!' } endLua editor
By using Real-time script editing you can speed up the development process since you usually won't have to restart the whole game to see how changes to the script affect the game. However, you may have to revisit the stage/screen you're building something for in order to see an effect since the event functions won't be called automatically after reloading the script (however, you can implement it to do that manually; it's not done by default because the previous script may not have cleaned up up things like created GUI objects).
- Lifecycle
You can only use the GUI library when it is setup. This is usually the case when the game is in a so called stage state. You can check if it is valid by checking if the GUI table is not nil. Most of the time you don't have to check for this manually since the flow of function calls implies its validity. In general you are safe to build your UI in script:enterStage(). If you want to build UI for the city stage (called GameStage in the game) you should rely on script:buildCityGUI() instead since the city stage (that is when a city is open) can cache the GUI hierarchy between stages for performance reasons. script:buildCityGUI will only be called after the GUI hierarchy for the city was (re-)built.
Technically your GUI objects are only valid if they are part of the currently active GUI hierarchy. However, because of the caching for the city stage they might become valid again which is why you may have to check if the GUI objects that you want to build already exist. You can do that for example by using Ids for your objects to find them in the hierarchy if they already exist or by only building city UI in the script:buildCityGUI event function.
- Dialog
Dialog windows are commonly used to present a set of information for a limited time to the user. In the simplest case a dialog window just features an icon, a title, a text and some buttons to interact with it.
Since dialog windows are composed of other GUI objects you can also do more complicated stuff with them, e.g. showing a list box with a bunch of options instead of a text in it. You can access this functionality by querying the table that is returned by GUI.createDialog{...}. The table contains the following entries:
Code: Select all Reset
function script:buildCityGUI() local dialog = GUI.createDialog{ icon = Icon.SETTINGS, -- Icon of the dialog title = 'Title', -- Title of the dialog text = 'Text', -- Text for the inner area of the dialog width = 180, -- Width of the dialog, 300 by default height = 100, -- Height of the dialog, 160 by default closeable = true, -- Show close button, true by default pause = true, -- Pause game when dialog is open, true by default onUpdate = function() end, -- Called every frame onCancel = function() end, -- For closeable dialogs when close button is hit onClose = function() end, -- When the dialog gets closed -- An array of actions that will be shown as buttons, nil by default actions = { { icon = Icon.CANCEL, -- Icon on the button text = Translation.control_cancel, -- Text on the button autoClose = true -- Close the dialog when this button gets hit, true by default }, { id = '$cmdCancel', -- Id for the button icon = Icon.OK, -- Icon for the button text = Translation.control_ok, -- Text on the button onClick = function() end, -- Will be called when the button gets clicked golden = true, -- Will make the button golden, false by default onInit = function(self, cmd) cmd:addHotkey(Keys.ENTER) end -- Starting 1.11.97 this can be used to add hotkeys to dialog buttons } } } endLua editorIf you want a dialog with a text input you may use the GUI.createRenameDialog{} function. It produces a dialog that's preconfigured for this task and works with simple callback functions.Code: Select all
dialog.title -- The label object that is used for the title of the dialog dialog.text -- The text frame object that is used for the inner text of the dialog dialog.content -- The main content panel; you can add objects to it dialog.controls -- If an actions table was provided (even if empty) the dialog has a horizontal layout object at the bottom where you can add buttons or other GUI objects dialog.close -- A function that will close the dialog if you call it
If you want to show a dialog in which the user can select one or multiple drafts you can use the GUI.createSelectDraftDialog{} function. This type of dialog is for example used in the tree planter tool to select the species to plant.
- GUI objects
GUI objects in general offer various functions to query or change their state. On top of that some objects allow you to provide callback functions that will be called by the object at certain events. The general pattern for creating new GUI objects is to call a function on the parent object and provide it with various arguments defined within a table (this is some sort of named arguments that allow you to specify only what you need). In most cases this looks like that
with parent being a parent GUI object, for example GUI.getRoot(), and XXX indicating what type of object to create, e.g. Label.
Code: Select all
parent:addXXX{...}
While some object types accept special arguments the following are accepted by all of them:None of these parameters is required so that you can usually write way shorter code than that. See the examples mentioned below to get an impression of how the actual code will look like.Code: Select all Reset
parent:addPanel{ id = 'myid', -- GUI objects can have an id to find them later using GUI.get(id) x = 10, -- The x location relative to the parent's inner area's left side; 0 by default; negative values are interpreted as distance to the right side of the inner area of the parent y = 10, -- Same as x but for y location; relative to parent's inner area's top side width = -10, -- The width of the new object; by default the remaining space in the inner area of the parent; negative values are interpreted as distance to the right side of the parent area height = -10, -- Same as width for but for the height of the new object onUpdate = function(self) print('called every frame') end -- A function that will be called every frame, nil by default }Lua editor
The following chapters will introduce various types of GUI objects and their functionality.
- Labels
Labels are one of the most simple GUI object types that are available. They render a provided line of text into their area. You can set an alignment for the text by using :setAlignment(x,y) with 0, 0.5 being the default alignment (aligned to the left and to the center vertically). If there's not enough horizontal space available to draw the text it will be compressed to match the area's width.
Code: Select all Reset
local lbl = parent:addLabel{ text = 'Hello World!' } lbl:setAlignment(0, 0) -- Set alignment to the top left corner lbl:setText('Another text') -- Set another text print(lbl:getText()) -- Print the current content of the label lbl:setColor(0, 0, 100) -- Set the color to blue lbl:setFont(Font.BIG) -- Set a big fontLua editor
- Icons
As simple as labels are icons. For them you provide a frame that will be drawn in the area of the object.
By default, the actual frame will be drawn in the center of the icon object. If the icon object's area is too small to display the whole frame it will be scaled down uniformly to match the given area. You can alter the alignment of the frame within the object by using
Code: Select all Reset
local icon = parent:addIcon{ icon = Icon.OK, width = 26, height = 26 }Lua editorThe parameters of this function range from 0 to 1 meaning left to right respectively. The default alignment therefore is 0.5, 0.5. You can change the icon afterwards by usingCode: Select all
icon:setAlignment(0, 0)
Code: Select all
icon:setIcon(Icon.CANCEL)
- Buttons
When it comes to user interaction you usually use buttons since they are easy to create and the user usually knows how to use them.
If you don't provide an icon the text will be centered on the button. Otherwise it will be aligned to the left side of the button where also the icon will be displayed. If you provide a width smaller than the actual width of the button (e.g. width = 0) the button will automatically resize so that it matches the width that is needed to display its content. This behavior is special since most objects don't alter their own size.
Code: Select all Reset
local button = parent:addButton{ icon = Icon.CANCEL, text = 'Test', width = 0, frameDefault = NinePatch.BLUE_BUTTON, frameDown = NinePatch.BLUE_BUTTON_PRESSED, onClick = function(self) Debug.toast('woah') end }Lua editor
By providing an isPressed callback function you can let the game know whether the button should be drawn as pressed right now (that is when the callback returns the value true). This can for example be used to implement a toggle button:You can skin the button by providing nine patch identifiers for the frameDefault and framePressed attributes. By providing the golden = true attribute you can tell the game to render the button in a golden fashion.Code: Select all Reset
local state = true local toggle = parent:addButton{ text = 'Switch', onClick = function() state = not state end, isPressed = function() return state end }Lua editor
- Canvas
The canvas object type is the most versatile GUI object type that's available. Not only does it allow you to draw the object on your own - if you don't it will be invisible - it also offers facilities to react to user input by providing an onClick callback function or by querying the canvas:getTouchPoint() function on it.
By usingCode: Select all Reset
local canvas = parent:addCanvas{ onDraw = function(self, x, y, w, h) Drawing.setColor(0, 0, 0) Drawing.drawRect(x, y, w, h) Drawing.setColor(255, 255, 255) Drawing.drawText('hello', x, y) Drawing.reset() end, onClick = function(self) Debug.toast('You clicked on me') end }Lua editoryou can disable event catching of the canvas which will effectively allow the user to still interact with GUI/the city that is behind the canvas. Since clicks on the canvas will still be reported to the onClick callback function this could be used to detect if the the user taps outside of a panel that is located within a transparent canvas that's filling the whole screen.Code: Select all
canvas:setTouchThrough(false)
In fact other GUI objects that will be presented here were implemented using canvas objects because of its great flexibility. This includes the icon, slider and panel objects.
- Panel
Panels are basically canvas objects whose onDraw function is already implemented so that it draws the default panel graphics (a nine patch image) of the game. It's equivalent to
Code: Select all Reset
parent:addCanvas{ ..., -- Your other arguments onDraw = function(self, x, y, w, h) Drawing.drawNinePatch(NinePatch.PANEL, x, y, w, h) end }Lua editor
- Slider
Sliders are useful for easy input of numerical values. Sliders are implemented so that they don't hold the numerical value state on their own. Instead, you provide two callback functions getValue and setValue that will be used to query/set the value. This allows you for example to limit the allowed values to integers only by applying a rounding function in setValue. You provide the minimal/maximal value of the slider using the minValue/maxValue attribute respectively.
If you provide a function getText it will be used to transform the current numerical value into a text rather than displaying it as a percentage. The default implementation of getText looks like
Code: Select all Reset
local value = 0.5 local slider = parent:addSlider{ minValue = 0, maxValue = 10, getValue = function() return value end, setValue = function(v) value = v end }Lua editorCode: Select all Reset
getText = function(value) return ''..math.floor(100 * value + 0.5)..'%' endLua editor
- List box
A list box is a scrollable list of GUI objects. The objects are children of the list box object and automatically placed in a vertical way similar to the behavior of the layout object that is presented below. The background of the child objects gets automatically drawn to show a zebra pattern for better readability. By using mainly transparent child objects, e.g. canvases without a onDraw function, you can make that pattern visible.
Due to how this is implemented the child objects may not receive as many touch point event data as regular GUI objects would receive. However, clicks on touch sensitive objects should work as before which means that you can use buttons in a list box.
Code: Select all Reset
local listBox = parent:addListBox{ x = 10, y = 10, width = -10, height = -10 } for i = 0, 10 do local entry = listBox:addCanvas{h=30} local lbl = entry:addLabel{ text = 'This is entry '..i, width = -30 } local cmd = entry:addButton{ x = -35, width = 30, icon = Icon.OK, onClick = function() Debug.toast('Clicked on entry '..i) end } endLua editor
You can disable the border of the listbox by using listBox:setShowBorder(false).
- Text frame
The text frame can be used to display text like a label. However, text frames can display multi-line texts and wrap lines that are too long into the next line. If the text is too long/high to fit into the text frame it will be scrollable similar to a list box.
The text frame is implemented by using a listbox internally. For that reason you can use the textFrame:setShowBorder(true) function on it to enable the listbox's border that is disable by default.
Code: Select all Reset
local textFrame = parent:addTextFrame{ text = [[This is a long text that can even span over multiple lines This will scroll if it is long enough.]] }Lua editor
- Text field
Text fields can be used to let the player enter textual information. It features a single line of text where the user can enter something. You can get and set the text of a text field by using textField:getText() and textField:setText() respectively.
On some platforms the text field will use the native text field functionalities of the system. This is for example the case on Android. For that reason it may be displayed on top of everything else and still react to user input even if hierarchically hidden by another GUI object (e.g. a dialog). Because of that you have to ensure that you hide the text field manually using textField:setVisible(false) when it should not be displayed. Later you can make it visible again using true as parameter for the setVisible function.
Code: Select all Reset
local textField = parent:addTextField{ text = 'Hello World', height = 20 } textField:setText('ok') Debug.toast(textField:getText())Lua editor
It is recommended to use a height between 20 and 30 for the text field to ensure that it is compatible with the native implementations that may be used.
By providing an attribute password = true to the addTextField function you can indicate that the text of the text field is a password. Usually the text will then be displayed in a disguised way.
To focus the textfield for text input you can use textField:setActive(). Note that this has only an effect on the desktop platform as of right now. Newly created textfields will be set to active automatically. Only one textfield can be active at a time.
- Layout
A layout object is an invisible GUI object whose purpose is the positioning of its children objects in a linear way. It can align them either in a horizontal (default) or vertical way (vertical = true). They are for example used by the game for the line of buttons in dialog windows that can be accessed by dialog.actions.
A layout offers the methods :getFirstPart(), :getCenterPart() and :getLastPart(). Depending on which part you append children to the alignment will differ. For example in a dialog actions line you would append buttons that should be on the left side to the first part and buttons that should be placed on the right side to the last part. In this specific case (that is when adding objects) you can omit :getFirstPart() for convenience.
Code: Select all Reset
local layout = parent:addLayout{ height = 30 } layout:getFirstPart():addButton{text = 'A', width = 30} layout:getFirstPart():addButton{text = 'B', width = 30} layout:getCenterPart():addButton{text = 'C', width = 30} layout:getLastPart():addButton{text = 'D', width = 30} layout:getLastPart():addButton{text = 'E', width = 30}Lua editor
When using a layout you usually cannot omit the width/height specification for child objects in the direction of the alignment since the inner area size of the layout doesn't take occupied space into account.
- Menu
Menus are an easy way to offer a set of actions to the user at a certain event, e.g. when the user pressed a button. In that case you would create the menu in the onClick function of that button and provide the button as source of the menu.
If you don't have a source GUI object you can alternatively specify a rectangle using the attributes x, y, width, height. The menu will then be located in a position so that it looks as if that area has spawned the menu.
Code: Select all Reset
GUI.createMenu{ source = parent, -- Spawn the menu from this object, e.g. useful if a button should trigger the menu visually -- A table that contains the set of actions that should be offered in the menu actions = { { icon = Icon.OK, -- An icon for the menu entry text = 'A', -- The text for the menu entry onClick = function() -- A function that will be called when the menu items gets clicked Debug.toast('yo') end }, { icon = Icon.CANCEL, text = 'B', enabled = false -- This entry is disabled and can therefore not be clicked }, {}, -- This is a separator for grouping of actions { icon = Icon.CLOSE, text = 'Close', onClick = ... } } }Lua editor
- Labels
Labels are one of the most simple GUI object types that are available. They render a provided line of text into their area. You can set an alignment for the text by using :setAlignment(x,y) with 0, 0.5 being the default alignment (aligned to the left and to the center vertically). If there's not enough horizontal space available to draw the text it will be compressed to match the area's width.
- Resources
The game features some tables to make access to certain resources like images and fonts of the game easier.
- Icon
The Icon table is a table that contains various images that are used by the game. You can use them e.g. like that:
See the GUI example below for a list of all available keys in the Icon table.
Code: Select all Reset
Drawing.drawImage(Icon.OK, 0, 0) GUI.createDialog{ icon = Icon.CITY, ... } parent:addIcon{ icon = Icon.CANCEL, ... }Lua editor
- Font
The game features 3 different fonts. You can use them like that:
- NinePatch
A nine patch is a set of 9 images that belong together and are used to fill a rectangle of arbitrary size. For that, the corner frames are drawn unscaled while the edge and center frames are scaled so that there are no gaps. A nine patch is usually specified by the first frame of the collection. You can define your own nine patches by loading the 9 frames in the correct order and using the first frame to draw it.
You can use Drawing.drawNinePatch(frame, ...) to draw a nine patch.
See the GUI example below for available nine patches in the game. They can be accessed via the NinePatch table:Code: Select all
Drawing.drawNinePatch(NinePatch.PANEL, x, y, w, h)
- Translation
For convenience you can access localized strings of the game. For examplewould return the translated "ok" string for the current language. The Translation.key expression is basically an alias for TheoTown.translate('key'). See the public translation files for available keys. Your plugin can define its own, custom translations, see here on how to do that.Code: Select all
Translation.control_ok
- Icon
The Icon table is a table that contains various images that are used by the game. You can use them e.g. like that:
- Special cases
This chapter sheds some lights on special places where you may want to integrate some own UI.
- City creation screen
By listening for specific stage names in the script:enterStage function you can detect whether the user e.g. just entered a region or city creation screen. You can then access the bottom line of the screen (where the buttons are) by its id "pageControl" and for example add your own buttons to it. In the example code above I use cmd:setChildIndex(1) to place the child before the existing button "Continue" (which will then have child index 2 instead of 1).
Code: Select all Reset
function script:enterStage(name) if name:startsWith('CreateCityStage') then local line = GUI.get('pageControl') local cmd = line:getLastPart():addButton{ w = 0, text = 'My plugin button', icon = Icon.SETTINGS, onClick = function() Debug.toast('Would open a dialog now ') end } cmd:setChildIndex(1) end endLua editor
As of right now the names for the create region/city stages are "CreateCityStageX;region=Y" with X being 1 or 2 depending on which page the user is on right now and Y being true for regions and false for cities. This approach will also work for other "page" based screens in the game. You can find out their names by Debug.toast'ing the names provided to script:enterStage(name).
- Adding a button to the sidebar
Since the sidebar layout object can be found by its id "sidebarLine" you can append your own buttons to it.
Code: Select all Reset
function script:buildCityGUI() -- Let's append the button to the sidebar line local sidebar = GUI.get('sidebarLine') local size = sidebar:getFirstPart():getChild(2):getWidth() local button = sidebar:getFirstPart():addButton{ width = size, height = size, icon = Icon.OK, frameDefault = Icon.NP_BLUE_BUTTON, frameDown = Icon.NP_BLUE_BUTTON_PRESSED, onClick = function(self) Debug.toast('Clicked!') end } endLua editor
- Altering the tile information dialog window
If you want to alter the information dialog window that's displayed when the player taps on something, e.g. a building, you can do that by implementing the script:finishInformationDialog(...) function.
Code: Select all Reset
function script:finishInformationDialog(x, y, _, dialog) dialog.controls:getLastPart():addButton{ -- Add a button to the controls layout of the dialog icon = Icon.TREE, text = 'Select trees (X)', width = 0, -- Adjust button size to content onClick = function(self) -- Do something, e.g. show another dialog end } endLua editor
- City creation screen
- Examples
This chapter presents some example plugins you can use for reference when starting your own plugins featuring GUI
- Tree Planter Tool
The tree planter tool is a plugin that is integrated into the game. The code of it is open source and can be used for reference for creating your own tools/UI.
- Plugin Creator Tools GUI Example The Plugin Creator Tools plugin (you can find it in the plugin store) features this plugin tool that offers a wide range of GUI objects. Have a look at the implementation of the GUI tool to learn how it works.
- Tree Planter Tool
The tree planter tool is a plugin that is integrated into the game. The code of it is open source and can be used for reference for creating your own tools/UI.