Files
Cooldude2606 7ab721b4b6 Refactor some of the Guis from the legacy plugin (#399)
* Fix bugs in core and add default args to Gui defs

* Refactor production Gui

* Refactor landfill blueprint button

* Fix more bugs in core

* Consistent naming of new guis

* Refactor module inserter gui

* Refactor surveillance gui

* Add shorthand for data from arguments

* Make element names consistent

* Add types

* Change how table rows work

* Refactor player stats gui

* Refactor quick actions gui

* Refactor research milestones gui

* Refactor player bonus gui

* Refactor science production gui

* Refactor autofill gui

* Cleanup use of aligned flow

* Rename "Gui.element" to "Gui.define"

* Rename Gui types

* Rename property_from_arg

* Add guide for making guis

* Add full reference document

* Add condensed reference

* Apply style guide to refactored guis

* Bug fixes
2025-08-29 14:30:30 +01:00

8.8 KiB
Raw Permalink Blame History

Design motivation

This document outlines why I created this framework, and the reasoning behind some of the opinionated decisions that shaped its design.

The motivation came from my experience with existing libraries, which often enforced a strict separation between element definitions, event handling, and GUI-related data. In many cases, these libraries focused solely on element creation, leaving developers to manually manage event filtering and data scoping themselves.

I found that approach cumbersome and unintuitive. I believed there was a better way, one that embraced a different kind of encapsulation, making the conceptual model easier to understand and work with. And so I created a framework with four distinct and independent parts that all come together with a sense of locality not seen in our libraries.

Additionally, the guide places greater emphasis on naming conventions and calling patterns, rather than just listing what each function does. These conventions are key to how the framework is expected to be used and are intended to make development feel more cohesive and intuitive.

At the heart of the framework are four core concepts that bring everything together:

ExpElement

ExpElement serves as the prototype for all element definitions. It's intentionally designed as a wrapper around LuaGuiElement.add and associated event handlers. It takes in definition tables and functions, and returns a function that can be used to create a LuaGuiElement.

This focused purpose makes it easier to reason about. It also reduces boilerplate, allowing you to concentrate on functionality rather than repetitive setup.

You can optionally add methods to the definition, such as add_row or refresh. While these could technically be local functions, including them directly in the definition makes it immediately clear which data they interact with or modify. This enhances both readability and maintainability.

For example, the following two snippets are conceptually equivalent:

Elements.my_label = Gui.define("my_label")
    :draw{
        type = "label",
        caption = "Hello, World!",
    }
    :style{
        font_color = { r = 1, g = 0, b = 0 },
        width = Gui.from_argument(1),
    }
    :element_data{
        foo = "bar"
    }
    :on_click(function(def, player, element, event)
        element.caption = "Clicked!"
    end)

function Elements.my_label.reset(my_label)
    my_label.caption = "Hello, World!"
end
local my_label_data = GuiData.create("my_label")
function Elements.my_label(parent, width)
    -- :draw
    local element = parent.add{
        type = "label",
        caption = "Hello, World!",
    }

    -- :style
    local style = element.style
    style.font_color = { r = 1, g = 0, b = 0 }
    style.width = width

    -- :element_data
    my_label_data[element] = {
        foo = "bar"
    }

    -- event handlers
    local tags = element.tags or {}
    local event_tags = tags.event_tags or {}
    event_tags[#event_tags + 1] = "my_label"
    element.tags = tags

    return element
end

local function my_label_reset(my_label)
    my_label.caption = "Hello, World!"
end

local function on_gui_click(event)
    local element = event.element
    if is_my_label(element) then -- pseudo function to check event_tags
        element.caption = "Clicked!"
    end
end

In the example, I use table-style definitions, which are the most common approach for simple elements and are encouraged wherever possible. Internally, these tables are converted into draw functions, which can also be passed directly if needed.

You could, of course, write everything into a single "create" function, or even place all logic inside a :draw method, but maintaining a separation between these responsibilities serves as a form of clear signposting. This improves readability and makes the structure of your code easier to follow at a glance.

Elements.my_label = Gui.define("my_label")
    :draw(function(def, parent, width)
        return parent.add{
            type = "label",
            caption = "Hello, World!",
        }
    end)
    :style(function(def, element, parent, width)
        return {
            font_color = { r = 1, g = 0, b = 0 },
            width = width,
        }
    end)
    :element_data(function(def, element, parent, width)
        return {
            foo = "bar"
        }
    end)
    :on_click(function()
        print("Clicked!")
    end)

GuiData

Building on the goal of keeping GUI data close to where its used and displayed, I introduced GuiData, which is integrated as ExpElement.data. Like the other components, its purpose is focused and singular, and it can even be used standalone if it's the only part of the framework you find useful.

In simple terms, GuiData creates a table in storage with a custom __index metamethod that enables automatic scoping of data. It also cleans up data when the associated key is destroyed, helping to reduce unnecessary memory usage.

One common use case, explained earlier in this guide, is storing references to other elements. This approach removes the tight coupling between event handlers and the GUI structure by giving handlers direct access to what they need.

Additionally, it encourages you to make assumptions explicit by requiring references as arguments. While this pattern can take some getting used to, it makes dependencies much easier to identify and reason about.

While this example exposes some of the internal mechanics, it should help you understand the convenience and clarity that scoped data access provides.

local data = GuiData.create("my_data")

-- data[element] = "foo"
storage.gui_data.scopes["my_data"].element_data[element.player_index][element.index] = "foo"

-- data[player] = "bar"
storage.gui_data.scopes["my_data"].player_data[player.index] = "bar"

-- data[force] = "baz"
storage.gui_data.scopes["my_data"].force_data[force.index] = "baz"

GuiIter

With scoped data easily accessible, it became straightforward to track elements belonging to a specific player, especially for updates or state changes. However, this pattern became so common (and often cluttered GuiData) that I created a dedicated iterator: GuiIter.

As with the other modules, GuiIter can be used independently if you like what it offers, or through its integration with ExpElement.

Whenever an element is created, or at any point, really, it can be registered with the iterator for future access. Retrieval is then handled by applying a filter across all tracked elements, returning them one by one. Dont worry, the underlying data structure is designed for efficient lookup and automatic cleanup.

This can be incredibly powerful. It gives you direct access to GUI elements without having to manually navigate from player.gui, and the filtering makes it simple to, for example, target only elements belonging to online players in a specific force.

Below is an example of how GuiIter can be used as a standalone utility:

local function teammate_counter(player)
    local frame = player.gui.left.add{ type = "frame" }
    local label = frame.add{ type = "label", caption = tostring(#player.force.players) }
    GuiIter.add_element("teammate_counter", label)
end

local function on_player_changed_force(event)
    local old_force = event.old_force
    local old_force_count = tostring(#old_force.players)
    for player, label in GuiIter.get_online_elements("teammate_counter", old_force) do
        label.caption = caption
    end

    local new_force = game.get_player(event.player_index).force
    local new_force_count = tostring(#new_force.players)
    for player, label in GuiIter.get_online_elements("teammate_counter", new_force) do
        label.caption = caption
    end
end

Toolbar

While ExpElement ties individual components together into self-contained units, the Toolbar acts as a singleton that manages them all. From an implementation standpoint, its split into two parts: one that handles drawing elements when a player joins, and an optional settings menu named "Toolbox".

The element-drawing functionality is the final piece of the puzzle for eliminating boilerplate and letting you focus on functionality. You simply register an element at a given location, and it gets drawn automatically on player join, it really is that straightforward.

The optional settings menu provides a standardised way to manage button behaviour, while also giving players control over which buttons are visible. This was born out of necessity: as the number of GUI modules grew, having all of them visible by default became overwhelming. The settings menu solves that by letting players hide modules they dont need.

toolbox