* 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
8.8 KiB
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 it’s 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. Don’t 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, it’s 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 don’t need.
