Skip to content

Configurable Widgets

Introduction

VCMI has capabilities to change some UI elements in your mods beyond only replacing one image with another. Not all UI elements are possible to modify currently, but development team is expanding them.

Elements possible to modify are located in config/widgets.

Tutorial

Let's take extendedLobby mod from vcmi-extras as an example for VCMI-1.4. Example sources.

You can take all assets from this tutorial from sources.

This submod offers extended options while player creates new game. For random map tab it defines UI to select RMG template, select map size bigger than XL, configure teams and roads. For options tab it introduces UI for chess timers.

In this tutorial we will recreate options tab to support chess timers UI.

Creating mod structure

To start making mod, create following folders structure;

extendedLobby/
|- content/
|  |- sprites/
|  |- config/
|  |  |- widgets/
|- mod.json

File mod.json is generic and could look like this:

{
    "name" : "Configurable UI tutorial mod",
    "description" : "See tutorial here https://github.com/vcmi/vcmi/wiki/Configurable-UI-widgets",
    "version" : "0.1",
    "modType" : "Interface",
    "compatibility":
    {
        "min" : "1.4.0"
    },
}

After that you can copy extendedLobby/ folder tomods/` folder and your mod will immediately appear in launcher but it does nothing for now.

Making layout for timer

Let's copy config/widgets/optionsTab.json file from VCMI folder to content/config/widgets/ folder from our mod. It defines UI for options tab as it designed in original game, we will keep everything related to player settings and will modify only timer area.

It's important, that when you are modifying optionsTab.json, game restart is not needed! When you updated file, it's enough to go back to main menu and then open lobby and options again. However, when you add new assets (images), you have to restart game to make possible find them.

It looks like on image below and has 3 elements: label with "Player Turn Duration", label with timer duration ("Unlimited" on picture) and slider to control timer.

Снимок экрана 2023-08-30 в 14 53 49

So we need to modify turn duration label and add combo box with timer types

Снимок экрана 2023-08-30 в 14 53 02

Open optionsTab.json and scroll it until you see comment timer. Three elements after this comment are related to timer.

Let's find first element, which is label

{
    "items"
    [
        ...
        // timer
        {
            "type": "label",
            "font": "small",
            "alignment": "center",
            "color": "yellow",
            "text": "core.genrltxt.521",
            "position": {"x": 222, "y": 544}
        },
        ...
    ],
    ...
}

And modify it a bit

{
    "name": "labelTimer", //add name, only for convenience
    "type": "label",
    "font": "small",
    "alignment": "center",
    "color": "yellow",
    "text": "vcmi.optionsTab.widgets.labelTimer", //replace text
    "position": {"x": 104, "y": 542} //move to the left
},

But we also need proper background image for this label. Add image widget BEFORE labelTimer widget:

{
    "type": "picture",
    "image": "RmgTTBk",
    "position": {"x": 54, "y": 532}
},
{
    "name": "labelTimer", //add name, only for convenience
    ...
},

In order to make it work, add file RmgTTBk.bmp to content/sprites/

Elements named labelTurnDurationValue and sliderTurnDuration we will keep without change - they are needed to configure classic timer.

Adding combo box

Now, let's add combo box.

Copy image DrDoCoBk.bmp to content/sprites/. Button objects use animated images to show different button states. For normal, pressed, blocked and highlighted. Our combo box inherits this behavior, so let's convert image to animation. In order to do it, we need to create file DrDoCoBk.json in same folder content/sprites/ with following content:

{
    "sequences" :
    [
        {
            "group" : 0,
            "frames" :
            [
                "DrDoCoBk.bmp"
            ]
        }
    ]
}

Thus we created file with animation, containing single frame which can be used for combo box.

Let's add one more element after //timer comment:

...
//timer
{
    "name": "timerModeSwitch",  //this is important to name it timerModeSwitch, because VCMI binds behavior to element called this way
    "type": "comboBox",
    "image": "DrDoCoBk",
    "position": {"x": 158, "y": 532},
    "imageOrder": [0, 0, 0, 0],
    "dropDown": {}, //here will be defined elements to be shown in drop down list
},

imageOrder helps VCMI to understand, which frame from animation to use in normal, pressed, blocked and highlighted states. In our case they will be same and we use 0 frame from DrDoCoBk animation.

We also want to have label on the top of this combo box showing which element is selected. You need to add items array, where additional elements can be specified, label in our case:

...
//timer
{
    "name": "timerModeSwitch",
    "type": "comboBox",
    "image": "DrDoCoBk",
    "position": {"x": 158, "y": 532},
    "imageOrder": [0, 0, 0, 0],
    "dropDown": {}, //here will be defined elements to be shown in drop down list
    "items":
    [
        {
            "name": "timer",
            "type": "label",
            "font": "small",
            "alignment": "left",
            "color": "yellow",
            "text": "vcmi.optionsTab.widgets.timerModeSwitch.classic" //default value for timer label
        }
    ]
},

With that we already have desired layout with all elements shown by default, but we also need to add elements with timer modes into drop-down list:

Снимок экрана 2023-08-30 в 15 34 47

First of all, add images to content/sprites/ folder: List2Bk.bmp for drop-down background and List10Sl.bmp for element highlighting.

Now specify items inside dropDown field

"dropDown":
{
    "items":
    [
        {
            "name": "background",
            "type": "picture",
            "image": "List2Bk",
            "position": {"x": 0, "y": -52} //negative value because our drop-down shall open in the top direction
        },

        {
            "name": "slider", //let's add slider if we have more elements in future
            "type": "slider",
            "position": {"x": 212, "y": -52},
            "size": 52,
            "style": "blue",
            "itemsVisible": 2, //we show only two elements
            "itemsTotal": 0,
            "selected": 0,
            "orientation": "vertical",
            "callback": "sliderMove" //callback predefined for drop-down menu to control which elements to show
        },

        //now list elements
        { //classic timer
            "type": "item", //this is special type for drop-down elements
            "position": {"x": 0, "y": -52},
            "items": //each element may have several elements
            [
                {
                    "type": "label", 
                    "name": "labelName",
                    "font": "small",
                    "alignment": "left",
                    "color": "white",
                    "position": {"x": 4, "y": 0},
                    "text": "vcmi.optionsTab.widgets.timerModeSwitch.classic"
                },
                {
                    "type": "picture",
                    "name": "hoverImage", //"hoverImage" is a key word, helping VCMI to understand which element to show when cursor hovers element
                    "visible": false, //invisible by default
                    "image": "List10Sl"
                }
            ]
        },

        { //chess timer
            "type": "item",
            "position": {"x": 0, "y": -27},
            "items":
            [
                {
                    "type": "label",
                    "name": "labelName",
                    "font": "small",
                    "alignment": "left",
                    "color": "white",
                    "position": {"x": 4, "y": 0},
                    "text": "vcmi.optionsTab.widgets.timerModeSwitch.chess"
                },
                {
                    "type": "picture",
                    "name": "hoverImage",
                    "visible": false,
                    "image": "List10Sl"
                }
            ]
        },
    ]
},

Now we can press drop-down menu and even select elements.

Switching timer modes

After view part is done, let's make behavioural part. Let's hide elements, related to classic timer when chess timer is selected and show them back if classic selected.

To do that, find "variables" part inside optionsTab.json and add there "timers" array, containing 2 elements:

"variables":
{
    "timers":
    [
        { //variables used if first element is chosen
            "text": "vcmi.optionsTab.widgets.timerModeSwitch.classic",
            "showWidgets": ["labelTurnDurationValue", "sliderTurnDuration"],
            "hideWidgets": [],
        },

        { //variables used if second element is chosen
            "text": "vcmi.optionsTab.widgets.timerModeSwitch.chess",
            "showWidgets": [],
            "hideWidgets": ["labelTurnDurationValue", "sliderTurnDuration"],
        },
    ],
    "timerPresets" :
    [
        ...
    ]
}

Now we show and hide elements, but visually you still can some "artifacts":

Снимок экрана 2023-08-30 в 15 51 22

It's because options tab background image we use has those elements drawn. Let's hide them with overlay image timchebk.bmp. It should be drawn before all other timer elements:

...
// timer
{
    "name": "timerBackground",
    "type": "picture",
    "image": "timchebk",
    "position": {"x": 0, "y": 530}
},

{
    "name": "timerModeSwitch",
    ...
    },
...

This background must be visible for chess timer and hidden for classic timer. Just put its name "timerBackground" into "hideWidgets" and "showWidgets" for corresponding elements.

It works and can switch elements, the only missing part is chess timer configuration.

Chess timer configuration

We should add text input fields, to specify different timers. We will use background for them timerField.bmp, copy it to content/sprites/ folder of your mod.

There are 4 different timers: base, turn, battle and creature. Read about them here: https://github.com/vcmi/vcmi/issues/1364 We can add editors for them into items list, their format will be following:

{
    "name": "chessFieldBase",
    "type": "textInput",
    "background": "timerField", 
    "alignment": "center",
    "text": "00:00", //default text
    "rect": {"x": 54, "y": 557, "w": 84, "h": 25},
    "offset": {"x": 0, "y": 0},
    "callback": "parseAndSetTimer_base", //callback to specify base timer value from string
    "help": "vcmi.optionsTab.widgets.chessFieldBase.help"
},

Add three remaining elements for different timers by yourself. You can play with all settings, except callback. There are 4 predefined callbacks to setup timers: - parseAndSetTimer_base - parseAndSetTimer_turn - parseAndSetTimer_battle - parseAndSetTimer_creature

And what we want to do is to hide/show those fields when classic/chess times is selected. Just add names of those elements into corresponding variables "showWidgets", `"hideWidgets".

We are done! You can find more information about configurable UI elements in documentation section.

Documentation

Types

All fields have format "key": value There are different basic types, which can be used as value.

Primitive types

Read JSON documentation for primitive types description: https://www.json.org/json-en.html

Text

Load predefined text which can be localised, examples: "vcmi.otherOptions.availableCreaturesAsDwellingLabel" "core.genrltxt.738"

Position

Point with two coordinates, example: { "x": 43, "y": -28 }

Rect

Rectangle ares, example: { "x": 28, "y": 220, "w": 108, "h": 50 }

Text alignment

Defines text alignment, can be one of values: "center", "left", "right"

Color

Predefined colors: "yellow", "white", "gold", "green", "orange", "bright-yellow"

To have custom color make an array of four elements in RGBA notation: [255, 128, 0, 255]

Font

Predefined fonts: "big", "medium", "small", "tiny", "calisto"

Hint text

Hint text is a pair of strings, one is usually shown in status bar when cursor hovers element, another hint while right button pressed. Each of elements is a Text

{
  "hover": "Text",
  "help": "Text
}

If one string specified, it will be applied for both hover and help.

"text"

Shortcut

String value defines shortcut. Some examples of shortcuts: "globalAccept", "globalCancel", "globalReturn","globalFullscreen", "globalOptions", "globalBackspace", "globalMoveFocus"

Full list is TBD

[VCMI-1.4] Player color

One of predefined values: "red", "blue", "tan", "green", "orange", "purple", "teal", "pink"

Configurable objects

Configurable object has following structure:

{
    "items": [],
    "variables": {}, //optional
    "customTypes": {}, //optional
    "library": {} //optional
}

items - array of widgets to be created. Widgets are created in sequentially in same order as they described.

variables - variables, which can be used by object. Meaningful variable names are predefined for each object

customTypes - description of custom widgets, which can be used for this object, see Custom widgets

library - same as above, but custom widgets are described in separate json, this parameter should contain path to library json is specified

Basic widgets

Label

"type": "label"

"name": "string" optional, object name

"font": font

"alignment": alignment,

"color": color,

"text": text,

"position": position

[VCMI-1.4] Multi-line label

"type": "multiLineLabel"

"name": "string" optional, object name

"font": font

"alignment": alignment,

"color": color,

"text": text,

"position": position

"rect": rect //text area

"adoptHeight": bool //if true, text area height will be adopted automatically based on content

Label group

"type": "labelGroup"

"name": "string" optional, object name

"font": font

"alignment": alignment,

"color": color,

"items": [] array of elements

Label group item

"position": position

"text": text,

TextBox

"type": "textBox"

"name": "string" optional, object name

"font": font

"alignment": alignment,

"color": color,

"text": text,

"rect": rect

Picture

"type": "picture"

"name": "string" optional, object name

"position": position

"image": string, specify filename

"visible": bool, optional

"playerColored", bool, optional, if true will be colorised to current player

Image

Use to show single frame from animation

"type": "image"

"name": "string" optional, object name

"position": position

"image": string, specify filename, animation only (def, json)

"group": integer optional, specify animation group

"frame": integer optional, specify animation frame

Texture

Filling area with texture

"type": "texture"

"name": "string" optional, object name

"image": string, specify filename

"rect": rect

TransparentFilledRectangle

"type": "transparentFilledRectangle"

"name": "string" optional, object name

"color": color fill color of rectangle (supports transparency)

"colorLine": color optional, 1px border color

"rect": rect

Animation

"type": "animation"

"name": "string" optional, object name

"position": position

"image": string, specify filename, animation only (def, json)

"repeat": bool, play only once or repeat animation

"group": integer optional, specify animation group

"alpha": integer optional, specify alpha opacity

"callback": string optional, callback to be called after animation complete

"frames": [] optional, array of frame ranges to show

Frame range

"start": integer, first frame

"end": integer, last frame

[VCMI-1.4] Text input

"type": "textInput"

"name": "string" optional, object name

"rect": rect

`"backgroundOffset": position

"background": string, specify filename

"font": font

"alignment": alignment,

"color": color,

"text": string optional, default text. Translations are not supported

"position": position

"help": hint

"callback": string optional, callback to be called on text changed. Input text is passed to callback function as an argument.

Button

"type": "button"

"name": "string" optional, object name

"position": position

"image": string, specify filename, animation only (def, json)

"help": hint

"imageOrder": [] array of 4 integers, each is responsible for frame to be shown in different states: normal, pressed, blocked and highlighted

"borderColor": color, optional

"hotkey": shortcut, optional

"callback": string optional, callback to be called on press. No arguments are passed to callback function.

"items": [] array of widgets to be shown as overlay (caption label, for example)

Toggle button

"type": "toggleButton"

"name": "string" optional, object name

"position": position

"image": string, specify filename, animation only (def, json)

"help": hint

"imageOrder": [] array of 4 integers, each is responsible for frame to be shown in different states: normal, pressed, blocked and highlighted

"callback": string optional, callback to be called on selection. Toggle identifier is passed to callback function as an argument.

"selected": bool, optional, is selected by default

"items": [] array of widgets to be shown as overlay (caption label, for example)

Toggle group

Group of toggle buttons, when one is selected, other will be de-selected

"type": "toggleGroup"

"name": "string" optional, object name

"position": position

"callback": string optional, callback to be called when one of toggles is selected

"items": [] array of toggle buttons

Slider

"type": "slider"

"name": "string" optional, object name

"position": position

"size": integer size in pixels

"style": string, can be "brown" or "blue"

"itemsVisible": integer, how many items are visible

"itemsTotal": integer, how many items in total

"selected": integer, current state for slider

"orientation" string, can be "horizontal" or "vertical"

"callback": string callback to be called on state change. Slider position is passed to callback function as an argument.

"scrollBounds": rect, optional

"panningStep": integer, optional

Combo box

"type": "comboBox"

"name": "string" optional, object name

"position": position

"image": string, specify filename, animation only (def, json)

"help": hint

"imageOrder": [] array of 4 integers, each is responsible for frame to be shown in different states: normal, pressed, blocked and highlighted

"borderColor": color, optional

"hotkey": shortcut, optional

"items": [] array of widgets to be shown as overlay, for example, label

"dropDown" : {} description of drop down menu widget

Used only as special object for combo box

"items": [] array of widgets to be built. Usually contains background picture, slider and item elements.

Item elements

"type": "item"

"name": "string" optional, object name

"position": position

"items": [] array of overlay widgets with certain types and names: - "name": "hoverImage", "type": picture - image to be shown when cursor hovers elements - "name": "labelName", "type": label - element caption

Callbacks - sliderMove connect to slider callback to correctly navigate over elements

Layout

"type": "layout"

"name": "string" optional, object name

High-level widgets

Custom widgets

For developers

While designing a new element, you can make it configurable to reuse all functionality described above. It will provide flexibility to further changes as well as modding capabilities.

Class should inherit InterfaceObjectConfigurable.

#include "gui/InterfaceObjectConfigurable.h" //assuming we are in client folder

class MyYesNoDialog: public InterfaceObjectConfigurable
{
}

InterfaceObjectConfigurable doesn't have default constructor, but has possibility to specify arguments to be passed to CIntObject.

To make new object work, it's sufficient to define constructor, which receives const reference to JsonNode.

MyYesNoDialog::MyYesNoDialog(const JsonNode & config):
    InterfaceObjectConfigurable(), //you can pass arguments same as for CIntObject
{
    //register custom builders
    REGISTER_BUILDER("MyItem", &MyYesNoDialog::buildMyItem);

    //add callbacks which can be used by widgets
    addCallback("okPressed", std::bind(&MyYesNoDialog::onOk, this, std::placeholders::_1));
    addCallback("cancelPressed", std::bind(&MyYesNoDialog::onCancel, this, std::placeholders::_1));

    build(config); //after this point all widgets are built and accessible

    //access widgets by name
    if(auto w = widget<CButton>("cancel"))
    {
        //now you can do something with button
    }
}

Callbacks

Custom widgets

You can build custom widgets, related to your UI element specifically. Like in example above, there is Item widget, which can be also used on JSON config.

REGISTER_BUILDER("myItem", &MyYesNoDialog::buildMyItem);

You have to define function, which takes JsonNode as an argument and return pointer to built widget

std::shared_ptr<MyYesNoDialog::Item> MyYesNoDialog::buildMyItem(const JsonNode & config)
{
    auto position = readPosition(config["position"]);
    return std::make_shared<MyYesNoDialog::Item>(*this, position); //define Item object as you want
}

After that, if your JSON file has items with type "MyItem", the new Item element will be constructed.

{
    "items":
    [
        {
            "type": "myItem",
            "position": {"x": 100, "y": 50}
        }
    ]
}

Variables

After calling build(config) variables defined in config JSON file become available. You can interpret them and use in callbacks or in element code

build(config);

if(variables["colorfulText"].Bool())
{
    if(auto w = widget<CLabel>("text"))
    {
        w->setColor(getMyPlayerColor()); //for reference only, getMyPlayerColor is not defined
    }
}