GUI Compendium

The Lua scripting language allows you to give your plugin more advanced features.

Moderator: Plugin Moderators

User avatar
Lobby
Developer
Reactions:
Posts: 3590
Joined: Sun Oct 26, 2008 12:34
Plugins: Show
Version: Beta

Platform

GUI Compendium

#1

Post by Lobby »

image.png
Cover image by Bearbear76.

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.



  1. 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.
    image.png
    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 :lua: :

    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!' } end
    Interactive Lua editor
    Run
    Here a panel would be added to the main root GUI object. After that, a button is added to that newly created panel.

    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).



  2. 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.



  3. Dialog
    image.png
    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.

    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 } } } end
    Interactive Lua editor
    Run
    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

    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 call the dialog if you call it


  4. GUI objects
    image.png
    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

    Code: Select all

    parent:addXXX{...}
    with parent being a parent GUI object, for example GUI.getRoot(), and XXX indicating what type of object to create, e.g. Label.
    While some object types accept special arguments the following are accepted by all of them:

    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 }
    Interactive Lua editor
    Run
    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.

    The following chapters will introduce various types of GUI objects and their functionality.



    1. Labels
      image.png
      image.png (20.48 KiB) Viewed 300 times
      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 font
      Interactive Lua editor
      Run


    2. Icons
      image.png
      image.png (5.62 KiB) Viewed 300 times
      As simple as labels are icons. For them you provide a frame that will be drawn in the area of the object.

      Code: Select all    Reset

      local icon = parent:addIcon{ icon = Icon.OK, width = 26, height = 26 }
      Interactive Lua editor
      Run
      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

      icon:setAlignment(0, 0)
      The 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 using

      Code: Select all

      icon:setIcon(Icon.CANCEL)


    3. Buttons
      image.png
      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.

      Code: Select all    Reset

      local button = parent:addButton{ icon = Icon.CANCEL, text = 'Test', width = 0, frameDefault = NinePatch.BLUE_BUTTON, framePressed = NinePatch.BLUE_BUTTON_PRESSED, onClick = function(self) Debug.toast('woah') end }
      Interactive Lua editor
      Run
      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.

      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:

      Code: Select all    Reset

      local state = true local toggle = parent:addButton{ text = 'Switch', onClick = function() state = not state end, isPressed = function() return state end }
      Interactive Lua editor
      Run
      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.



    4. Canvas
      image.png
      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.

      Code: 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 }
      Interactive Lua editor
      Run
      By using

      Code: Select all

      canvas:setTouchThrough(false)
      you 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.

      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.



    5. Panel
      image.png
      image.png (12.52 KiB) Viewed 301 times
      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 }
      Interactive Lua editor
      Run


    6. Slider
      image.png
      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.

      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 }
      Interactive Lua editor
      Run
      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

      getText = function(value) return ''..math.floor(100 * value + 0.5)..'%' end
      Interactive Lua editor
      Run


    7. List box
      image.png
      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.

      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 } end
      Interactive Lua editor
      Run
      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.



    8. Text frame
      image.png
      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.

      Code: Select all    Reset

      local textFrame = parent:addTextFrame{ text = [[This is a long text that can even span over multiple lines :P This will scroll if it is long enough.]] }
      Interactive Lua editor
      Run
    9. Text field
      image.png
      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.

      Code: Select all    Reset

      local textField = parent:addTextField{ text = 'Hello World', height = 20 } textField:setText('ok') Debug.toast(textField:getText())
      Interactive Lua editor
      Run
      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.

      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.



    10. Layout
      image.png
      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.

      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}
      Interactive Lua editor
      Run
      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.

      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.



    11. Menu
      image.png
      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.

      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 = ... } } }
      Interactive Lua editor
      Run
      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.


  5. Resources
    The game features some tables to make access to certain resources like images and fonts of the game easier.


    1. Icon
      image.png
      The Icon table is a table that contains various images that are used by the game. You can use them e.g. like that:

      Code: Select all    Reset

      Drawing.drawImage(Icon.OK, 0, 0) GUI.createDialog{ icon = Icon.CITY, ... } parent:addIcon{ icon = Icon.CANCEL, ... }
      Interactive Lua editor
      Run
      See the GUI example below for a list of all available keys in the Icon table.



    2. Font
      image.png
      The game features 3 different fonts. You can use them like that:

      Code: Select all    Reset

      Drawing.drawText('Hello', 0, 0, Font.SMALL) label:setFont(Font.BIG)
      Interactive Lua editor
      Run


    3. NinePatch
      image.png
      image.png (14.46 KiB) Viewed 298 times
      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)


    4. Translation
      For convenience you can access localized strings of the game. For example

      Code: Select all

      Translation.control_ok
      would 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.


  6. Special cases
    This chapter sheds some lights on special places where you may want to integrate some own UI.



    1. City creation screen
      image.png

      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 end
      Interactive Lua editor
      Run
      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).

      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).



    2. Adding a button to the sidebar
      image.png
      image.png (16.18 KiB) Viewed 240 times
      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 } end
      Interactive Lua editor
      Run


    3. Altering the tile information dialog window
      image.png
      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 } end
      Interactive Lua editor
      Run


  7. Examples
    This chapter presents some example plugins you can use for reference when starting your own plugins featuring GUI :mine



    1. Tree Planter Tool
      image.png
      image.png (19.09 KiB) Viewed 298 times
      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.



    2. Plugin Creator Tools GUI Example
      image.png
      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.
=^._.^= ∫

LobbyFan
TheoTown Councillor
Reactions:
Posts: 5
Joined: Mon Feb 01, 2016 10:15
Plugins: Show

Re: GUI Compendium

#2

Post by LobbyFan »

Hast du well done

Post Reply Previous topicNext topic

Return to “Lua Scripting”