diff --git a/exp_gui/module/data.lua b/exp_gui/module/data.lua index 94424240..cc7bdb26 100644 --- a/exp_gui/module/data.lua +++ b/exp_gui/module/data.lua @@ -1,37 +1,60 @@ +--[[-- ExpGui - GuiData +Provides a method of storing data for elements, players, and forces under a given scope. +This is not limited to GUI element definitions but this is the most common use case. +]] local ExpUtil = require("modules/exp_util") local Storage = require("modules/exp_util/storage") --- @type table +local registered_scopes = {} + +--- @type table local script_data = {} Storage.register(script_data, function(tbl) script_data = tbl + for scope, data in pairs(tbl) do + local proxy = registered_scopes[scope] + if proxy then + proxy.element_data = data[1] + proxy.player_data = data[2] + proxy.force_data = data[3] + end + end end) --- @class ExpGui_GuiData local GuiData = { - _gui_data = script_data, - _registered = {}, --- @type table + _data = script_data, + _scopes = registered_scopes, } --- @alias DataKey LuaGuiElement | LuaPlayer | LuaForce ---- @class ExpGui.GuiData: table ---- @field _init ExpGui.GuiDataInit ---- @field element_data table> ---- @field player_data table ---- @field force_data table - ---- @class ExpGui.GuiDataInit +--- @class ExpGui.GuiData._init --- @field element any --- @field player any --- @field force any +--- @class ExpGui.GuiData: table +--- @field _scope string +--- @field _init ExpGui.GuiData._init +--- @field element_data table> +--- @field player_data table +--- @field force_data table +-- This class has no prototype methods + +GuiData._metatable = { + __class = "GuiData", +} + +Storage.register_metatable(GuiData._metatable.__class, GuiData._metatable) + --- Return the index for a given key --- @param self ExpGui.GuiData --- @param key DataKey --- @return any -function GuiData.__index(self, key) +function GuiData._metatable.__index(self, key) assert(type(key) == "userdata", "Index type '" .. ExpUtil.get_class_name(key) .. "' given to GuiData. Must be of type userdata.") local rtn, init local object_name = key.object_name @@ -61,7 +84,7 @@ end --- @param self ExpGui.GuiData --- @param key DataKey --- @param value unknown -function GuiData.__newindex(self, key, value) +function GuiData._metatable.__newindex(self, key, value) assert(type(key) == "userdata", "Index type '" .. ExpUtil.get_class_name(key) .. "' given to GuiData. Must be of type userdata.") local object_name = key.object_name if object_name == "LuaGuiElement" then @@ -80,43 +103,44 @@ function GuiData.__newindex(self, key, value) end end -GuiData._metatable = { - __index = GuiData.__index, - __newindex = GuiData.__newindex, - __class = "GuiData", -} - -Storage.register_metatable(GuiData._metatable.__class, GuiData._metatable) - ---- Register the starting values for element data ---- @param define_name string ---- @param init_element any ---- @param init_player any ---- @param init_force any -function GuiData.register(define_name, init_element, init_player, init_force) - assert(GuiData._registered[define_name] == nil, "Define already has data registered") - GuiData._registered[define_name] = { - element = init_element, - player = init_player, - force = init_force, - } +--- Sallow copy the keys from the provided table into itself +--- @param self ExpGui.GuiData +--- @param data table +function GuiData._metatable.__call(self, data) + for k, v in pairs(data) do + self[k] = v + end end ---- Create the data for an element definition ---- @param define_name string +--- Create the data object for a given scope +--- @param scope string --- @return ExpGui.GuiData -function GuiData.create(define_name) - local init = assert(GuiData._registered[define_name], "Define does not have any registered data") +function GuiData.create(scope) + assert(GuiData._scopes[scope] == nil, "Scope already exists with name: " .. scope) - local data = { - _init = init, + local instance = { + _init = {}, + _scope = scope, element_data = {}, player_data = {}, force_data = {}, } - script_data[define_name] = data - return setmetatable(data, GuiData._metatable) + script_data[scope] = { + instance.element_data, + instance.player_data, + instance.force_data, + } + + GuiData._scopes[scope] = instance + return setmetatable(instance, GuiData._metatable) +end + +--- Get the link to an existing data scope +--- @param scope string +--- @return ExpGui.GuiData +function GuiData.get(scope) + return GuiData._scopes[scope] end return GuiData diff --git a/exp_gui/module/iter.lua b/exp_gui/module/iter.lua index 3fba2f38..d2f58e36 100644 --- a/exp_gui/module/iter.lua +++ b/exp_gui/module/iter.lua @@ -1,7 +1,13 @@ +--[[-- ExpGui - GuiData +Provides a method of storing elements created for a player and provide a global iterator for them. +]] local ExpUtil = require("modules/exp_util") local Storage = require("modules/exp_util/storage") +--- @alias ExpGui_GuiIter.FilterType LuaPlayer | LuaForce | LuaPlayer[] | nil +--- @alias ExpGui_GuiIter.ReturnType ExpGui_GuiIter.ReturnType + --- @type table>> local script_data = {} Storage.register(script_data, function(tbl) @@ -29,17 +35,17 @@ local function next_valid_element(elements, prev_index) end --- Get the next valid player with elements ---- @param define_elements table> +--- @param scope_elements table> --- @param players LuaPlayer[] --- @param prev_index uint? --- @param online boolean? --- @return uint?, LuaPlayer?, table? -local function next_valid_player(define_elements, players, prev_index, online) +local function next_valid_player(scope_elements, players, prev_index, online) local index, player = nil, nil while true do index, player = next(players, prev_index) while player and not player.valid do - define_elements[player.index] = nil + scope_elements[player.index] = nil index, player = next(players, index) end @@ -48,7 +54,7 @@ local function next_valid_player(define_elements, players, prev_index, online) end if online == nil or player.connected == online then - local player_elements = define_elements[player.index] + local player_elements = scope_elements[player.index] if player_elements and #player_elements > 0 then return index, player, player_elements end @@ -57,16 +63,16 @@ local function next_valid_player(define_elements, players, prev_index, online) end --- Iterate over all valid elements for a player ---- @param define_name string +--- @param scope string --- @param player LuaPlayer ---- @return fun(): LuaPlayer?, LuaGuiElement? -function GuiIter.player_elements(define_name, player) +--- @return ExpGui_GuiIter.ReturnType +function GuiIter.player_elements(scope, player) if not player.valid then return nop end - local define_elements = script_data[define_name] - if not define_elements then return nop end + local scope_elements = script_data[scope] + if not scope_elements then return nop end - local player_elements = define_elements[player.index] + local player_elements = scope_elements[player.index] if not player_elements then return nop end local element_index, element = nil, nil @@ -78,13 +84,13 @@ function GuiIter.player_elements(define_name, player) end --- Iterate over all valid elements for a player ---- @param define_name string +--- @param scope string --- @param players LuaPlayer[] --- @param online boolean? ---- @return fun(): LuaPlayer?, LuaGuiElement? -function GuiIter.filtered_elements(define_name, players, online) - local define_elements = script_data[define_name] - if not define_elements then return nop end +--- @return ExpGui_GuiIter.ReturnType +function GuiIter.filtered_elements(scope, players, online) + local scope_elements = script_data[scope] + if not scope_elements then return nop end local index, player, player_elements = nil, nil, nil local element_index, element = nil, nil @@ -92,7 +98,7 @@ function GuiIter.filtered_elements(define_name, players, online) while true do -- Get the next valid player elements if needed if element_index == nil then - index, player, player_elements = next_valid_player(define_elements, players, index, online) + index, player, player_elements = next_valid_player(scope_elements, players, index, online) if index == nil then return nil, nil end --- @cast player_elements -nil end @@ -107,11 +113,11 @@ function GuiIter.filtered_elements(define_name, players, online) end --- Iterate over all valid elements ---- @param define_name string ---- @return fun(): LuaPlayer?, LuaGuiElement? -function GuiIter.all_elements(define_name) - local define_elements = script_data[define_name] - if not define_elements then return nop end +--- @param scope string +--- @return ExpGui_GuiIter.ReturnType +function GuiIter.all_elements(scope) + local scope_elements = script_data[scope] + if not scope_elements then return nop end local player_index, player_elements, player = nil, nil, nil local element_index, element = nil, nil @@ -119,14 +125,14 @@ function GuiIter.all_elements(define_name) while true do if element_index == nil then -- Get the next player - player_index, player_elements = next(define_elements, player_index) + player_index, player_elements = next(scope_elements, player_index) if player_index == nil then return nil, nil end player = game.get_player(player_index) -- Ensure next player is valid while player and not player.valid do - define_elements[player_index] = nil - player_index, player_elements = next(define_elements, player_index) + scope_elements[player_index] = nil + player_index, player_elements = next(scope_elements, player_index) if player_index == nil then return nil, nil end player = game.get_player(player_index) end @@ -142,65 +148,63 @@ function GuiIter.all_elements(define_name) end end ---- @alias FilterType LuaPlayer | LuaForce | LuaPlayer[] | nil - --- Iterate over all valid gui elements for all players ---- @param define_name string ---- @param filter FilterType ---- @return fun(): LuaPlayer?, LuaGuiElement? -function GuiIter.get_elements(define_name, filter) +--- @param scope string +--- @param filter ExpGui_GuiIter.FilterType +--- @return ExpGui_GuiIter.ReturnType +function GuiIter.get_tracked_elements(scope, filter) local class_name = ExpUtil.get_class_name(filter) if class_name == "nil" then --- @cast filter nil - return GuiIter.all_elements(define_name) + return GuiIter.all_elements(scope) elseif class_name == "LuaPlayer" then --- @cast filter LuaPlayer - return GuiIter.player_elements(define_name, filter) + return GuiIter.player_elements(scope, filter) elseif class_name == "LuaForce" then --- @cast filter LuaForce - return GuiIter.filtered_elements(define_name, filter.players) + return GuiIter.filtered_elements(scope, filter.players) elseif type(filter) == "table" and ExpUtil.get_class_name(filter[1]) == "LuaPlayer" then --- @cast filter LuaPlayer[] - return GuiIter.filtered_elements(define_name, filter) + return GuiIter.filtered_elements(scope, filter) else error("Unknown filter type: " .. class_name) end end --- Iterate over all valid gui elements for all online players ---- @param define_name string ---- @param filter FilterType ---- @return fun(): LuaPlayer?, LuaGuiElement? -function GuiIter.get_online_elements(define_name, filter) +--- @param scope string +--- @param filter ExpGui_GuiIter.FilterType +--- @return ExpGui_GuiIter.ReturnType +function GuiIter.get_online_elements(scope, filter) local class_name = ExpUtil.get_class_name(filter) if class_name == "nil" then --- @cast filter nil - return GuiIter.filtered_elements(define_name, game.connected_players) + return GuiIter.filtered_elements(scope, game.connected_players) elseif class_name == "LuaPlayer" then --- @cast filter LuaPlayer if not filter.connected then return nop end - return GuiIter.player_elements(define_name, filter) + return GuiIter.player_elements(scope, filter) elseif class_name == "LuaForce" then --- @cast filter LuaForce - return GuiIter.filtered_elements(define_name, filter.connected_players) + return GuiIter.filtered_elements(scope, filter.connected_players) elseif type(filter) == "table" and ExpUtil.get_class_name(filter[1]) == "LuaPlayer" then --- @cast filter LuaPlayer[] - return GuiIter.filtered_elements(define_name, filter, true) + return GuiIter.filtered_elements(scope, filter, true) else error("Unknown filter type: " .. class_name) end end --- Add a new element to the global iter ---- @param define_name string +--- @param scope string --- @param element LuaGuiElement -function GuiIter.add_element(define_name, element) +function GuiIter.add_element(scope, element) if not element.valid then return end - local define_elements = script_data[define_name] - if not define_elements then - define_elements = {} - script_data[define_name] = define_elements + local scope_elements = script_data[scope] + if not scope_elements then + scope_elements = {} + script_data[scope] = scope_elements end local player_elements = script_data[element.player_index] @@ -212,4 +216,16 @@ function GuiIter.add_element(define_name, element) player_elements[element.index] = element end +--- Remove an element from the global iter +--- @param scope string +--- @param player_index uint +--- @param element_index uint +function GuiIter.remove_element(scope, player_index, element_index) + local scope_elements = script_data[scope] + if not scope_elements then return end + local player_elements = script_data[player_index] + if not player_elements then return end + player_elements[element_index] = nil +end + return GuiIter diff --git a/exp_gui/module/prototype.lua b/exp_gui/module/prototype.lua new file mode 100644 index 00000000..63d48142 --- /dev/null +++ b/exp_gui/module/prototype.lua @@ -0,0 +1,386 @@ + +local ExpUtil = require("modules/exp_util") + +local GuiData = require("./data") +local GuiIter = require("./iter") + +--- @class ExpGui_ExpElement +local ExpElement = { + _elements = {} +} + +--- @alias ExpElement.DrawCallback fun(def: ExpElement, parent: LuaGuiElement, ...): LuaGuiElement?, function? +--- @alias ExpElement.StyleCallback fun(def: ExpElement, element: LuaGuiElement?, parent: LuaGuiElement, ...): table? +--- @alias ExpElement.DataCallback fun(def: ExpElement, element: LuaGuiElement?, parent: LuaGuiElement, ...): table? +--- @alias ExpElement.OnEventAdder fun(self: ExpElement, handler: fun(event: E)): ExpElement + +--- @class ExpElement._debug +--- @field defined_at string +--- @field draw_src table? +--- @field style_src table? + +--- @class ExpElement +--- @field name string +--- @field scope string +--- @field data ExpGui.GuiData +--- @field _debug ExpElement._debug +--- @field _draw ExpElement.DrawCallback? +--- @field _style ExpElement.StyleCallback? +--- @field _element_data ExpElement.DataCallback? +--- @field _player_data ExpElement.DataCallback? +--- @field _force_data ExpElement.DataCallback? +--- @field _events table +ExpElement._prototype = { + _track_elements = false, + _tag_elements = false, +} + +ExpElement._metatable = { + __call = nil, -- ExpElement._prototype.create + __index = ExpElement._prototype, + __class = "ExpGui", +} + +--- Register a new instance of a prototype +--- @param name string +--- @return ExpElement +function ExpElement.create(name) + ExpUtil.assert_not_runtime() + assert(ExpElement._elements[name] == nil, "ExpElement already defined with name: " .. name) + local scope = ExpUtil.get_module_name(2) .. "::" .. name + + local instance = { + name = name, + scope = scope, + data = GuiData.create(scope), + _events = {}, + _debug = { + defined_at = ExpUtil.safe_file_path(2), + }, + } + + ExpElement._elements[name] = instance + return setmetatable(instance, ExpElement._metatable) +end + +--- Create a new instance of this element definition +--- @param parent LuaGuiElement +--- @param ... any +--- @return LuaGuiElement? +function ExpElement._prototype:create(parent, ...) + assert(self._draw, "Element does not have a draw definition") + local element, status = self:_draw(parent, ...) + local player = assert(game.get_player(parent.player_index)) + + if self._style then + local style = self:_style(element, parent, ...) + if style then + assert(element, "Cannot set style when no element was returned by draw definition") + local element_style = element.style + for k, v in pairs(style) do + element_style[k] = v + end + end + end + + if self._element_data then + local data = self:_element_data(element, parent, ...) + if data then + assert(element, "Cannot set element data when no element was returned by draw definition") + self.data[element] = data + end + end + + if self._player_data then + local data = self:_player_data(element, parent, ...) + if data then + self.data[player] = data + end + end + + if self._force_data then + local data = self:_force_data(element, parent, ...) + if data then + self.data[player.force] = data + end + end + + if not element then return end + + if self._track_elements and status ~= ExpElement._prototype.track_element and status ~= ExpElement._prototype.untrack_element then + self:track_element(element) + end + + if self._tag_elements and status ~= ExpElement._prototype.tag_element and status ~= ExpElement._prototype.untag_element then + self:tag_element(element) + end + + return element +end + +--- Enable tracking of all created elements +--- @return ExpElement +function ExpElement._prototype:track_all_elements() + self._track_elements = true + return self +end + +--- Set the draw definition +--- @param definition table | ExpElement.DrawCallback +--- @return ExpElement +function ExpElement._prototype:draw(definition) + if type(definition) == "table" then + self._debug.draw_src = definition + self._draw = function(_, parent) + return parent.add(definition) + end + else + self._draw = definition + end + + return self +end + +--- Set the style definition +--- @param definition table | ExpElement.StyleCallback +--- @return ExpElement +function ExpElement._prototype:style(definition) + if type(definition) == "table" then + self._debug.style_src = definition + self._style = function(_, parent) + return parent.add(definition) + end + else + self._style = definition + end + + return self +end + +--- Set the default element data +--- @param definition table | ExpElement.DataCallback +--- @return ExpElement +function ExpElement._prototype:element_data(definition) + if type(definition) == "table" then + --- @diagnostic disable-next-line invisible + self.data._init.element = definition + else + self._element_data = definition + end + + return self +end + +--- Set the default player data +--- @param definition table | ExpElement.DataCallback +--- @return ExpElement +function ExpElement._prototype:player_data(definition) + if type(definition) == "table" then + --- @diagnostic disable-next-line invisible + self.data._init.player = definition + else + self._player_data = definition + end + + return self +end + +--- Set the default force data +--- @param definition table | ExpElement.DataCallback +--- @return ExpElement +function ExpElement._prototype:force_data(definition) + if type(definition) == "table" then + --- @diagnostic disable-next-line invisible + self.data._init.force = definition + else + self._force_data = definition + end + + return self +end + +--- Iterate the tracked elements of all players +--- @param filter ExpGui_GuiIter.FilterType +--- @return ExpGui_GuiIter.ReturnType +function ExpElement._prototype:tracked_elements(filter) + return GuiIter.get_tracked_elements(self.scope, filter) +end + +--- Iterate the tracked elements of all online players +--- @param filter ExpGui_GuiIter.FilterType +--- @return ExpGui_GuiIter.ReturnType +function ExpElement._prototype:online_elements(filter) + return GuiIter.get_online_elements(self.scope, filter) +end + +--- Track an arbitrary element, tracked elements can be iterated +--- @param element LuaGuiElement +--- @return LuaGuiElement +--- @return function +function ExpElement._prototype:track_element(element) + GuiIter.add_element(self.scope, element) + return element, ExpElement._prototype.track_element +end + +--- Untrack an arbitrary element, untracked elements can't be iterated +--- @param element LuaGuiElement +--- @return LuaGuiElement +--- @return function +function ExpElement._prototype:untrack_element(element) + GuiIter.remove_element(self.scope, element.player_index, element.index) + return element, ExpElement._prototype.untrack_element +end + +--- Tag an arbitrary element, tagged elements call event handlers +--- @param element LuaGuiElement +--- @return LuaGuiElement +--- @return function +function ExpElement._prototype:tag_element(element) + local element_tags = element.tags + if not element_tags then + element_tags = {} + end + + local event_tags = element_tags["ExpGui"] + if not event_tags then + event_tags = {} + element_tags["ExpGui"] = event_tags + end + --- @cast event_tags string[] + + if not table.contains(event_tags, self.scope) then + event_tags[#event_tags + 1] = self.scope + end + + element.tags = element_tags + return element, ExpElement._prototype.tag_element +end + +--- Untag an arbitrary element, untagged elements do not call event handlers +--- @param element LuaGuiElement +--- @return LuaGuiElement +--- @return function +function ExpElement._prototype:untag_element(element) + local element_tags = element.tags + if not element_tags then + element_tags = {} + end + + local event_tags = element_tags["ExpGui"] + if not event_tags then + event_tags = {} + element_tags["ExpGui"] = event_tags + end + --- @cast event_tags string[] + + table.remove_element(event_tags, self.scope) + element.tags = element_tags + return element, ExpElement._prototype.untag_element +end + +local e = defines.events +local events = { +} + +--- Create a function to add event handlers to an element definition +--- @param event_name any +--- @return ExpElement.OnEventAdder +local function event_factory(event_name) + --- @param event EventData.on_gui_click + events[event_name] = function(event) + local element = event.element + if not element or not element.valid then return end + + local event_tags = element.tags and element.tags["ExpGui"] + if not event_tags then return end + --- @cast event_tags string[] + + for _, define_name in ipairs(event_tags) do + local define = ExpElement._elements[define_name] + if define then + define:_raise_event(event) + end + end + end + + return function(self, handler) + self._tag_elements = true + local handlers = self._events[event_name] + if not handlers then + handlers = {} + self._events[event_name] = handlers + end + handlers[#handlers + 1] = handler + return self + end +end + +--- Raise all handlers for an event on this definition +--- @param event EventData +function ExpElement._prototype:_raise_event(event) + local handlers = self._events[event.name] + if not handlers then return end + for _, handler in ipairs(handlers) do + handler(event) + end +end + +--- Called when LuaGuiElement checked state is changed (related to checkboxes and radio buttons). +--- @type ExpElement.OnEventAdder +ExpElement._prototype.on_checked_state_changed = event_factory(e.on_gui_checked_state_changed) + +--- Called when LuaGuiElement is clicked. +--- @type ExpElement.OnEventAdder +ExpElement._prototype.on_click = event_factory(e.on_gui_click) + +--- Called when the player closes the GUI they have open. +--- @type ExpElement.OnEventAdder +ExpElement._prototype.on_closed = event_factory(e.on_gui_closed) + +--- Called when a LuaGuiElement is confirmed, for example by pressing Enter in a textfield. +--- @type ExpElement.OnEventAdder +ExpElement._prototype.on_confirmed = event_factory(e.on_gui_confirmed) + +--- Called when LuaGuiElement element value is changed (related to choose element buttons). +--- @type ExpElement.OnEventAdder +ExpElement._prototype.on_elem_changed = event_factory(e.on_gui_elem_changed) + +--- Called when LuaGuiElement is hovered by the mouse. +--- @type ExpElement.OnEventAdder +ExpElement._prototype.on_hover = event_factory(e.on_gui_hover) + +--- Called when the player's cursor leaves a LuaGuiElement that was previously hovered. +--- @type ExpElement.OnEventAdder +ExpElement._prototype.on_leave = event_factory(e.on_gui_leave) + +--- Called when LuaGuiElement element location is changed (related to frames in player.gui.screen). +--- @type ExpElement.OnEventAdder +ExpElement._prototype.on_location_changed = event_factory(e.on_gui_location_changed) + +--- Called when the player opens a GUI. +--- @type ExpElement.OnEventAdder +ExpElement._prototype.on_opened = event_factory(e.on_gui_opened) + +--- Called when LuaGuiElement selected tab is changed (related to tabbed-panes). +--- @type ExpElement.OnEventAdder +ExpElement._prototype.on_selected_tab_changed = event_factory(e.on_gui_selected_tab_changed) + +--- Called when LuaGuiElement selection state is changed (related to drop-downs and listboxes). +--- @type ExpElement.OnEventAdder +ExpElement._prototype.on_selection_state_changed = event_factory(e.on_gui_selection_state_changed) + +--- Called when LuaGuiElement switch state is changed (related to switches). +--- @type ExpElement.OnEventAdder +ExpElement._prototype.on_switch_state_changed = event_factory(e.on_gui_switch_state_changed) + +--- Called when LuaGuiElement text is changed by the player. +--- @type ExpElement.OnEventAdder +ExpElement._prototype.on_text_changed = event_factory(e.on_gui_text_changed) + +--- Called when LuaGuiElement slider value is changed (related to the slider element). +--- @type ExpElement.OnEventAdder +ExpElement._prototype.on_value_changed = event_factory(e.on_gui_value_changed) + +ExpElement._metatable.__call = ExpElement._prototype.create +ExpElement.events = events --- @package +return ExpElement