Sandbox/GUI/Getting Started

From The Battle for Wesnoth Wiki

So, it looks like I can't exclude this page/section from the search engine as I had hoped. I wanted to put this in a sandbox so that it wouldn't be published without some proper review. I guess for now

DON'T TRUST ANYTHING YOU READ HERE

Introduction

This guide is designed to help you get a simple Wesnoth GUI, implemented in lua, up and running while describing the basic building blocks along the way. Some would find creating a GUI in part or in full using WML simpler to follow, and those alternatives are available, but we're using lua.

Credits

Most of the basic framework for these examples were lifted from LotI. It's a great place to find well written examples, but some of it is complex enough to be a little overwhelming when getting started.

Getting Started

For example purposes, we'll create a directory in our campaign directory called lua containing a file called gui_tutorial.lua, and add a command in the prestart event for a scenario which will create a right-click menu option to invoke our new GUI. Of course there are other methods, but this one will get us started. After all, we really just want to get something up on the screen ASAP, right?

A most basic GUI

In our prestart event, we create a simple menu item, which executes a single line of lua which calls the lua function most_basic_gui() which is found in gui_tutorial.lua:

[set_menu_item]
   id=most_basic_gui
   description="Our first GUI"
   [command]
       [lua]
           code=<<
                   wesnoth.require("~add-ons/<OUR_CAMPAIGN>/lua/gui_tutorial.lua").most_basic_gui()
                >>
       [/lua]
    [/command]
[/set_menu_item]

And create gui_tutorial.lua:

local function most_basic_gui()
        local dialogDefinition = {
                wml.tag.tooltip { id = "tooltip_large" },  -- required
                wml.tag.helptip { id = "helptip_large" },  -- required
                wml.tag.grid {   -- our most basic gui
                        wml.tag.row {  -- must start with a row
                                wml.tag.column {  -- a row needs a column
                                        wml.tag.image {  -- we can put stuff in a column
                                                label = "units/trolls/grunt.png"
                                        }
                                }
                        }
                }
        }
        gui.show_dialog(dialogDefinition)
end
return { most_basic_gui = most_basic_gui}

In the above file, we have just the single function to create our GUI, followed by a return command which makes our function most_basic_gui() available to the wesnoth.require command as most_basic_gui(). In this case, we'll just refer to our function using the same name as the function, though we don't have to use the same name.

Our function creates a table containing the definition of a "dialog", and then passes that to gui.show_dialog(). It should be noted that gui.show_dialog does not provide synchronization, so if your GUI make changes to the game state, you'll need to jump through undocumented hoops or you'll break replays and multiplayer, but that's not something we need to care about at this point, so just be aware that you may need to deal with it in the future.

Our dialog definition at this point includes three parts. The first two are a tooltip and a helptip, which are required and probably do something, but nothing we care about here, we just have to have them. The third part is a grid, which is basically a table, like in HTML or MySQL, with rows and columns. A grid always contains at least one row. A row contains at least one column. Inside a column is exactly one item, a widget, in this case we'll use an image.

Note: if you should try out the above, you'll have to hit escape to close the gui.

Adding a little flavor

You may have noticed our GUI is missing a header and a way to close it when we're done admiring our work. Here we add a new first row to our grid, while demonstrating a couple formatting options: a border to put some distance between our grid cell its neighbor, and the "use_markup = true" option to enable Pango support in our text.

Our third row adds an OK button at the bottom. You can associate actions with buttons to do all kinds of things, but here we just exploit the default behaviour to close the GUI.

Of course, we'll also have to modify our return to support the new function, and update our menu item(s) accordingly.

local function most_basic_gui2()
        local dialogDefinition = {
                wml.tag.tooltip { id = "tooltip_large" },
                wml.tag.helptip { id = "helptip_large" },
                wml.tag.grid {
                        wml.tag.row {  -- A header 
                                wml.tag.column { 
                                        border = "bottom",
                                        border_size = 10,
                                        wml.tag.label {
                                                use_markup = true,
                                                label = "<span size='large'>" .. _"Here there be " .. 
                                                    "<span color='yellow'>" ..
                                                    _"MONSTERS!" .. "</span></span>"
                                        }
                                }
                        },
                        wml.tag.row {  -- The body of our GUI
                                wml.tag.column {
                                        wml.tag.image { 
                                                label = "units/trolls/grunt.png"
                                        }
                                }
                        },
                        wml.tag.row {
                                wml.tag.column {  -- An "OK" button, with no action assigned for now,
                                                  -- but it will close the gui
                                        wml.tag.button { 
                                                id = "ok", 
                                                label = _"OK"
                                        },
                                }
                        }
                }
        }
        gui.show_dialog(dialogDefinition)
end
return { most_basic_gui = most_basic_gui, most_basic_gui2 = most_basic_gui2 }

And a bit more

And now a minor, but important change. We want to add a new text field to the body of our GUI. Obviously, we want to add a new column, but this is more difficult than when we added new rows in the previous example. The problem is that all rows (at a given level) in a grid must contain the same number of columns. We can't have two columns in the row that constitutes the body of our GUI, but only one in the header and OK button. To solve this problem, we replace our image tag with a new grid, which can use as many columns as we like (as long as they are the same within each row of our new grid).

local function most_basic_gui3()
        local dialogDefinition = {
                wml.tag.tooltip { id = "tooltip_large" },
                wml.tag.helptip { id = "helptip_large" },
                wml.tag.grid {
                        wml.tag.row {  -- A header 
                                wml.tag.column {
                                        border = "bottom",
                                        border_size = 10,
                                        wml.tag.label {
                                                use_markup = true,
                                                label = "<span size='large'>" .. _"Here there be " .. 
                                                    "<span color='yellow'>" ..
                                                    _"MONSTERS!" .. "</span></span>"
                                        }
                                }
                        },
                        wml.tag.row {  -- The body of our GUI
                                wml.tag.column {
                                        wml.tag.grid {
                                                wml.tag.row {
                                                        wml.tag.column {
                                                                wml.tag.image {
                                                                        label = "units/trolls/grunt.png"
                                                                }
                                                        },
                                                        wml.tag.column {
                                                                wml.tag.label {
                                                                        label = "A troll"
                                                                }
                                                        }
                                                }
                                        }
                                }
                        },
                        wml.tag.row {
                                wml.tag.column {  -- An "OK" button, with no action assigned for now,
                                                  -- but it will close the gui
                                        wml.tag.button {
                                                id = "ok",
                                                label = _"OK"
                                        },
                                }
                        }
                }
        }
        gui.show_dialog(dialogDefinition)
end

Adding a listbox

Now we'd like to add some more monsters. Obviously, we could just add more rows, but what if we won't know until runtime how many and which ones? We could break up the definition of our dialog and add new rows dynamically, it's just a table after all, but fortunately we have a widget which handles this for us, a listbox. A listbox is kind of like an array, a collection of similar objects where the number of items can vary. We will tell the GUI where we want the box, and what each entry in the box will look like, and then at runtime we can add entries to the box.

local function gui_with_listbox()
        local monsters = {
                { image = "units/trolls/grunt.png", string = "A troll" },
                { image = "units/monsters/cuttlefish.png", string = "A cuttlefish" },
                { image = "units/monsters/yeti.png", string = "A yeti" }
        }

        local listbox_id = "monsters"

        local listboxItem = wml.tag.grid {
                wml.tag.row {
                        wml.tag.column {
                                wml.tag.image {
                                        id = "monster_image"
                                }
                        },
                        wml.tag.column {
                                wml.tag.label {
                                        id = "monster_label"
                                }
                        }
                }
        }

        local listboxDefinition = wml.tag.listbox { id = listbox_id,
               wml.tag.list_definition {
                        wml.tag.row {
                                wml.tag.column {
                                        wml.tag.toggle_panel {
                                                listboxItem
                                        }
                                }
                        }
                }
        }

        local dialogDefinition = {
                wml.tag.tooltip { id = "tooltip_large" },
                wml.tag.helptip { id = "helptip_large" },
                wml.tag.grid {
                        wml.tag.row {  -- A header 
                                wml.tag.column {
                                        border = "bottom",
                                        border_size = 10,
                                        wml.tag.label {
                                                use_markup = true,
                                                label = "<span size='large'>" .. _"Here there be " ..
                                                        "<span color='yellow'>" ..
                                                        _"MONSTERS!" .. "</span></span>"
                                        }
                                }
                        },
                        wml.tag.row {  -- The body of our GUI
                                  wml.tag.column {
                                          listboxDefinition
                                }
                        },
                        wml.tag.row {
                                wml.tag.column {  -- An "OK" button, with no action assigned for now, 
                                                  -- but it will close the gui
                                        wml.tag.button {
                                                id = "ok",
                                                label = _"OK"
                                        },
                                }
                        }
                }
        }
 
        local function preshow(dialog)  -- Prepare the GUI before display
                local listbox = dialog[listbox_id]
                for i, monster in ipairs(monsters) do
                        listbox[i].monster_image.label = monster.image
                        listbox[i].monster_label.label = monster.string
                end
        end

        gui.show_dialog(dialogDefinition,preshow)
end

We begin by creating a simple table which represents the data which will be presented in our listbox. In most cases, we probably wouldn't create that monster table here, but we need it for our example.

The variable listbox_id gives us an identifier for our listbox so we can reference it when we need to. We don't really need to use a variable here, it's just convenient.

We define the structure for the elements in our listbox, stored in listboxItem. Note that we've replaced the actual data with identifiers (e.g. 'units/trolls/grunt.png' becomes 'id = "monster_image"), since each element may have different data.

We define the listbox itself, specifying the identifier, and the definition of our listbox element (which looks a lot like a grid). The cell inside the column contains a variable which is just the listbox element definition we created above; it's not necessary to do this, we could have used one big table, but it's easier to read this way (IMO).

We place the hardcoded data in our GUI body with our new listbox definition, again using a variable to make it easier to read.

We create a function, often called preshow(), which will be called for us as part of the drawing the GUI (see the new optional argument in gui.show_dialog() -- there's also an optional postshow(), but we don't need it here). This is where we pull the data from our example table into the listbox. We create a listbox variable associated with the listbox, identified by the listbox_id we assigned earlier, inside the dialog which gui.show_dialog() passes to preshow(). Then we iterate through our data table, using each entry from that table to populate a listbox item.

Let's look at that last action a little more closely using an example.

listbox[i].monster_image.label = monster.image

Which we can read as "In listbox element i of our listbox, for the variable monster_image which we defined in our listboxItem, use the data from the corresponding index i in our example data table". If you were to step through all of the variable substitutions, you'd see that for i=1, our dialogDefinition is basically the same thing as it was in the earlier examples, just supplied from a table instead of hardcoded.

You will probably notice that our data does not line up nicely in the GUI. We will fix that later.

Tree View

You may have noticed that clicking on a listbox item in our listbox caused it to be highlighted with a box around the contents. This is because the items in a listbox are selectable. This will be useful when we want to add actions, but it looks a bit odd if we just want to present a list of data.

Also, most of the data had to be formatted the same for each item in the listbox. We could, perhaps, include custom markup in our labels, but the actual layout, like the number of columns per row, had to be the same for every item.

Another way to present a list of data is the tree view. Since a tree view supports multiple data types, we may need to create multiple definitions for the elements in our list. These are known as nodes. It will also be necessary to explicitly define which node to use for each element when we populate our tree view.

local function basic_tree_view()
        local monsters = {
                { image = "units/trolls/grunt.png", label = "A troll", name = _"Bob", type = "Trolls" },
                { image = "units/trolls/whelp.png", label = "A troll whelp", name = "Junior", type = "Trolls" },
                { image = "units/trolls/shaman.png", label = "A troll shaman", name = _"Alice", type = "Trolls" },
                { image = "units/monsters/cuttlefish.png", label = "A cuttlefish", type = "Seamonsters" },
                { image = "units/monsters/yeti.png", label = "A yeti", type = "Coolers" }
        }

        local tree_view = wml.tag.tree_view {
                id = "monsters_tv",
                wml.tag.node {
                        id = "trolls_node",
                        wml.tag.node_definition {
                                wml.tag.row {
                                        wml.tag.column {
                                                wml.tag.label {
                                                        id = "monster_name",
                                                        linked_group = "monster_name"
                                                }
                                        },
                                        wml.tag.column {
                                                wml.tag.image {
                                                        id = "monster_image",
                                                        linked_group = "monster_image"
                                                }
                                        },
                                        wml.tag.column {
                                                wml.tag.label {
                                                        id = "monster_label",
                                                        linked_group = "monster_label"
                                                }
                                        }
                                }
                        }
                },
                wml.tag.node {
                        id = "nottrolls_node",
                        wml.tag.node_definition {
                                wml.tag.row {
                                        wml.tag.column {
                                                wml.tag.image {
                                                        id = "monster_image",
                                                        linked_group = "monster_image"
                                                }
                                        },
                                        wml.tag.column {
                                                wml.tag.label {
                                                        id = "monster_label",
                                                        linked_group = "monster_label"
                                                }
                                        }
                                }
                        }
                }
        }
        local dialogDefinition = {
                wml.tag.tooltip { id = "tooltip_large" },
                wml.tag.helptip { id = "helptip_large" },
                wml.tag.linked_group {
                        id = "monster_name",
                        fixed_width = true
                },
                wml.tag.linked_group {
                        id = "monster_image",
                        fixed_width = true
                },
                wml.tag.linked_group {
                        id = "monster_label",
                        fixed_width = true
                },
                wml.tag.grid {
                        wml.tag.row {  -- A header 
                                wml.tag.column {
                                        border = "bottom",
                                        border_size = 10,
                                        wml.tag.label {
                                                use_markup = true,
                                                label = "<span size='large'>" .. _"Here there be " ..
                                                        "<span color='yellow'>" ..
                                                        _"MONSTERS!" .. "</span></span>"
                                        }
                                }
                        },
                        wml.tag.row {  -- The body of our GUI
                                wml.tag.column {
                                        tree_view
                                }
                        },
                        wml.tag.row {
                                wml.tag.column {  -- An "OK" button, with no action assigned for now, 
                                                  -- but it will close the gui
                                        wml.tag.button {
                                                id = "ok",
                                                label = _"OK"
                                        },
                                }
                        }
                }
        }
       local function preshow(dialog)  -- Prepare the GUI before display
                for i, monster in ipairs(monsters) do
                        if monster.type == "Trolls" then
                                dialog.monsters_tv:add_item_of_type("trolls_node")
                                dialog.monsters_tv[i].monster_name.label = monster.name
                        else
                                dialog.monsters_tv:add_item_of_type("nottrolls_node")
                        end
                                dialog.monsters_tv[i].monster_image.label = monster.image
                                dialog.monsters_tv[i].monster_label.label = monster.label
 
                end
        end
        gui.show_dialog(dialogDefinition,preshow)
end

We have expanded our list of monsters to include three types of trolls. We have also given each troll a name. This is of course a rather simplistic example, we could have simply given each monster that is not a troll an empty name, but it as presented here our approach serves the purpose of demonstrating how we deal with multiple data types. Our new tree view contains a node for each type of data we will present. In preview, we explicitly define each element in our list using add_item_of_type(), and populate the item accordingly.

You may also note the addition of a few linked_group lines. We'll cover linked groups in more detail later, but as used here they instruct the GUI that each column using the same node type needs to be aligned with the others. For example, all of our trolls line up nicely.

Multi_page

Like a tree_view, a multi_page is a heterogeneous container which is dynamically populated, however, while a multi_page contains multiple pages, only one is displayed at any one time. Its purpose is perhaps best described by a couple of examples.

function basic_multipage()
        local monsters = {
                { image = "units/trolls/grunt.png", label = "A troll", name = _"Bob", type = "Trolls" },
                { image = "units/trolls/whelp.png", label = "A troll whelp", name = "Junior", type = "Trolls" },
                { image = "units/trolls/shaman.png", label = "A troll shaman", name = _"Alice", type = "Trolls" },
                { image = "units/monsters/cuttlefish.png", label = "A cuttlefish", type = "Seamonsters" },
                { image = "units/monsters/yeti.png", label = "A yeti", type = "Coolers" }
        }

        local multi_page = wml.tag.multi_page {
                id = "monsters_mp",
                wml.tag.page_definition {
                        id = "trolls_page",
                        wml.tag.row {
                                wml.tag.column {
                                        wml.tag.label {
                                                id = "monster_name"
                                        }
                                },
                                wml.tag.column {
                                        wml.tag.image {
                                                id = "monster_image"
                                        }
                                },
                                wml.tag.column {
                                        wml.tag.label {
                                                id = "monster_label"
                                        }
                                }
                        }
                },
                wml.tag.page_definition {
                        id = "nottrolls_page",
                        wml.tag.row {
                                wml.tag.column {
                                        wml.tag.image {
                                                id = "monster_image",
                                                linked_group = "monster_image"
                                        }
                                },
                                wml.tag.column {
                                        wml.tag.label {
                                                id = "monster_label",
                                                linked_group = "monster_label"
                                        }
                                }
                        }
                }
        }

       local dialogDefinition = {
                wml.tag.tooltip { id = "tooltip_large" },
                wml.tag.helptip { id = "helptip_large" },
                wml.tag.grid {
                        wml.tag.row {  -- A header 
                                wml.tag.column {
                                        border = "bottom",
                                        border_size = 10,
                                        wml.tag.label {
                                                use_markup = true,
                                                label = "<span size='large'>" .. _"Here there be " ..
                                                        "<span color='yellow'>" ..
                                                        _"MONSTERS!" .. "</span></span>"
                                        }
                                }
                        },
                        wml.tag.row {  -- The body of our GUI
                                wml.tag.column {
                                        multi_page
                                }
                        },
                        wml.tag.row {
                                wml.tag.column {  -- An "OK" button
                                        wml.tag.button {
                                                id = "ok",
                                                label = _"OK"
                                        },
                                }
                        }
                }
        }

        local function preshow(dialog)  -- Prepare the GUI before display
                for i, monster in ipairs(monsters) do
                        if monster.type == "Trolls" then
                                dialog.monsters_mp:add_item_of_type("trolls_page")
                                dialog.monsters_mp[i].monster_name.label = monster.name
                        else
                                dialog.monsters_mp:add_item_of_type("nottrolls_page")
                        end
                        dialog.monsters_mp[i].monster_image.label = monster.image
                        dialog.monsters_mp[i].monster_label.label = monster.label
                end
                --dialog.monsters_mp.selected_index = 5
        end
        gui.show_dialog(dialogDefinition,preshow)
end

As you can see, our basic multi_page GUI looks a lot like our basic tree_view GUI. The difference is what is displayed. While both the tree_view and the multi_page containers contain five items, with the multi_page, only the first one is displayed. More specifically, only the page associated with the selected_index, which defaults to 1, is displayed. If we were to uncomment the last line in preshow(), we would still see only one item, but it would be the fifth one we allocated. It's nice to know all are pages are in there somewhere, but this example is pretty useless. What we need is a way to select which page we want to see from within our GUI.