Skip to content

Instantly share code, notes, and snippets.

@emptyrivers
Last active March 9, 2018 09:42
Show Gist options
  • Save emptyrivers/99cbceb3fce5a2c6f8e3518789797820 to your computer and use it in GitHub Desktop.
Save emptyrivers/99cbceb3fce5a2c6f8e3518789797820 to your computer and use it in GitHub Desktop.
GUI_layer
--[[
Abstraction layer for factorio GUI api, intended to extend the normal
functionality and simplify the process of handling GUI events.
How it works:
at the top of control.lua, write
local GUI = require "GUI"
edit the require path as necessary
call GUI:Init() during on_init, and GUI:Load during on_load
During on_player_created, call GUI:New(event.player_index) to create the
abstraction for that player (call GUI:Delete(event.player_index) on_player_removed).
You are then able to add more sophisticated gui elements.
to add gui elements, do like so:
--where `element` is any entry within our model
newElement = element:Add(widget)
widget should be a table, with the following data:
widget = {
-- prototype is what gets sent to the underlying factorio api.
-- Don't worry too much about the name attribute, since this will be handled separately.
-- Otherwise, follow the documentation http://lua-api.factorio.com/latest/LuaGuiElement.html#LuaGuiElement.add
prototype = <table>,
-- choose a unique string. GUI:Add() will modify the value you set here to ensure
-- that the element's name is truly unique for that player
-- note: unlike factorio, this abstraction **requires** that you give a name in order to function properly
name = <string>,
-- the following attributes are optional:
indestructible = <bool>, -- use to simulate top,left, and center's indestructibility
unique = <bool>, -- if true, then GUI:Add() assumes that you will not attempt to add a widget with the same name again.
-- supports OnAdd, OnDestroy, OnClear, which are called when the underlying element is
-- .add()ed, .destroy()ed, and .clear()ed, respectively
-- also supports event responses, keyed by event id.
-- note that these functions are saved, so they must not have any upvalues associated with them.
methods = <table>,
-- misc. attributes. It is recommended that you avoid name collisions.
-- If you set an attribute which collides with something in the abstraction *or*
-- the underlying element, then you will be unable to read or edit those values.
attributes = <table>,
}
Delete an element using
element:Destroy()
Clear it using:
element:Clear()
A shortcut for changing the visibility of an element is available
(equivalent to setting element.style.visible):
element:Hide()
element:Show()
element:Toggle()
All attributes from the underlying factorio api are available to you, as if
you were working with the base API.
If you use the OnClear/OnDestroy methods, note that the OnDestroy methods of
any descendants will be called as well, before the underlying element is actually destroyed.
]]
local GUI = {}
-- requires
local mod_gui = require "mod-gui"
require "util"
-- metatables
local elementMt = {
__index = function(self,key)
if type(key) == "number" then -- event ids are numbers
-- error("A gui event occured for a gui element with no response")
return -- uncomment the last line if you wish
end
return GUI[key] or self.__element[key]
end,
__newindex = function(self,key,value)
self.__element[key] = value
end,
}
local namePoolMt = {
__index = function(self,key)
self[key] = 1
return 1
end
}
-- Init/load/config scripts
function GUI:Init()
global.namePool = {}
global.models = {}
return global.models, global.namePool
end
function GUI:Load()
for _,model in pairs(global.models) do
self:setmetatable(model)
end
return global.models, global.namePool
end
function GUI:OnConfigurationChanged()
--dummy function, fill in with the edits to gui models which best suits your purposes
end
-- methods
function GUI:setmetatable(model)
for _, child in pairs(model.__flatmap) do
setmetatable(child, elementMt)
end
setmetatable(global.namePool[model.__element.player.index], namePoolMt)
return setmetatable(model, elementMt)
end
function GUI:New(playerID)
local player = game.players[playerID]
if not player then
error("Attempt to create a GUI model for a non-existant playerID: "..(playerID or 'nil'))
end
local model = {
__element = player.gui,
__flatmap = {},
gui = model,
indestructible = true,
}
for id, method in pairs{top = 'get_button_flow',center = false,left = 'get_frame_flow'} do
model[id] = {
__element = method and mod_gui[method](player) or player.gui[id],
shown = true,
gui = model,
parent = model,
indestructible = true,
}
model.__flatmap[id] = model[id]
end
global.namePool[player.index] = {}
global.models[player.index] = model
return self:setmetatable(model)
end
function GUI:Delete(playerID)
local index = game.players[playerID].index -- there are more valid playerIDs then necessary, so get the uint version
global.namePool[index] = nil
global.models[index] = nil
end
-- helper function for Add
function AcquireName(name, playerID)
-- we could also release the name on GUI:PreDestroy(), i suppose. But that would require keeping a much larger data structure.
-- In practice, this will never cause collisions, since lua uses doubles for numbers, and it would overflow at 2^54
local pool = global.namePool[game.players[playerID].index]
local id = pool[name]
pool[name] = pool[name] + 1
return ("GUI_%s:%s:%s"):format(name,playerID,id),id
end
function GUI:Add(widget)
widget.prototype.name = not widget.unique and AcquireName(widget.name, self.player_index) or widget.name
local newElement = {
prototypeName = widget.name,
__element = self.add(widget.prototype),
gui = self.gui,
parent = self,
}
widget.prototype.name = nil
if widget.methods then
for eventID, method in pairs(widget.methods) do
newElement[eventID] = method
end
end
if widget.attributes then
local attributes = table.deepcopy(attributes)
for attributeID, attribute in pairs(attributes) do
newElement[attributeID] = attribute
end
end
--rawset because the parent of this new element already has a __newindex method, and it would try to write to the LuaGuiElement, causing an error
rawset(self,newElement.__element.name, setmetatable(newElement, elementMt))
self.gui.__flatmap[newElement.name] = self[newElement.name]
if newElement.OnAdd then
newElement:OnAdd()
end
return newElement
end
function GUI:PreDestroy(...)
if self.OnDestroy then
self:OnDestroy(...)
end
self.gui.__flatmap[self.name] = nil
for _, child_name in pairs(self.children_names) do
self[child_name]:PreDestroy(...)
end
end
function GUI:Destroy(...)
if self.indestructible then
error("Attempt to destroy indestructible element: "..self.name)
end
self:PreDestroy(...)
self.parent[self.name] = nil
self.__element.destroy()
end
function GUI:Clear(...)
if self.OnClear then
self:OnClear(...)
end
for _,child_name in pairs(self.children_names) do
self[child_name]:PreDestroy(...)
self[child_name] = nil
end
self.__element.clear()
end
function GUI:Hide()
self.__element.style.visible = false
return true
end
function GUI:Show()
self.__element.style.visible = true
return true
end
function GUI:Toggle()
return self.__element.style.visible and self:Hide() or self:Show()
end
--script handler
script.on_event(
{
defines.events.on_gui_checked_state_changed,
defines.events.on_gui_click,
defines.events.on_gui_elem_changed,
defines.events.on_gui_selection_state_changed,
defines.events.on_gui_text_changed,
},
function(event)
local model = models[event.player_index]
if not model then
-- error("A gui event occured for a player with a non-existent model")
return
end
local element = model.__flatmap[event.element.name]
if element then
local response = element[event.name]
if response then
return response(element, event)
end
end
end
)
return GUI
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment