diff --git a/exp_gui/module/control.lua b/exp_gui/module/control.lua new file mode 100644 index 00000000..74463af4 --- /dev/null +++ b/exp_gui/module/control.lua @@ -0,0 +1,215 @@ + +local Storage = require("modules/exp_util/storage") + +local ExpElement = require("modules/exp_gui/prototype") + +--- @alias ExpGui.VisibleCallback fun(player: LuaPlayer, element: LuaGuiElement): boolean + +--- @class ExpGui.player_elements +--- @field top table +--- @field left table +--- @field relative table + +--- @type table +local player_elements = {} +Storage.register(player_elements, function(tbl) + player_elements = tbl +end) + +--- @class ExpGui +local ExpGui = { + element = ExpElement.create, + property_from_args = ExpElement.property_from_args, + property_from_name = ExpElement.property_from_name, + top_elements = {}, --- @type table + left_elements = {}, --- @type table + relative_elements = {}, --- @type table +} + +local mod_gui = require("mod-gui") +ExpGui.get_top_flow = mod_gui.get_button_flow +ExpGui.get_left_flow = mod_gui.get_frame_flow + +--- Get a player from an element or gui event +--- @param input LuaGuiElement | { player_index: uint } +--- @return LuaPlayer +function ExpGui.get_player(input) + return assert(game.get_player(input.player_index)) +end + +--- Toggle the enable state of an element +--- @param element LuaGuiElement +--- @param state boolean? +--- @return boolean +function ExpGui.toggle_enabled_state(element, state) + if not element or not element.valid then return false end + if state == nil then + state = not element.enabled + end + element.enabled = state + return state +end + +--- Toggle the visibility of an element +--- @param element LuaGuiElement +--- @param state boolean? +--- @return boolean +function ExpGui.toggle_visible_state(element, state) + if not element or not element.valid then return false end + if state == nil then + state = not element.visible + end + element.visible = state + return state +end + +--- Destroy an element if it exists and is valid +--- @param element LuaGuiElement? +function ExpGui.destroy_if_valid(element) + if not element or not element.valid then return end + element.destroy() +end + +--- Register a element define to be drawn to the top flow on join +--- @param define ExpElement +--- @param visible ExpGui.VisibleCallback | boolean | nil +function ExpGui.add_top_element(define, visible) + assert(ExpGui.top_elements[define.name] == nil, "Element is already added to the top flow") + ExpGui.top_elements[define] = visible or false +end + +--- Register a element define to be drawn to the left flow on join +--- @param define ExpElement +--- @param visible ExpGui.VisibleCallback | boolean | nil +function ExpGui.add_left_element(define, visible) + assert(ExpGui.left_elements[define.name] == nil, "Element is already added to the left flow") + ExpGui.left_elements[define] = visible or false + +end + +--- Register a element define to be drawn to the relative flow on join +--- @param define ExpElement +--- @param visible ExpGui.VisibleCallback | boolean | nil +function ExpGui.add_relative_element(define, visible) + assert(ExpGui.relative_elements[define.name] == nil, "Element is already added to the relative flow") + ExpGui.relative_elements[define] = visible or false +end + +--- Register a element define to be drawn to the top flow on join +--- @param define ExpElement +--- @param player LuaPlayer +--- @return LuaGuiElement +function ExpGui.get_top_element(define, player) + return player_elements[player.index].top[define.name] +end + +--- Register a element define to be drawn to the left flow on join +--- @param define ExpElement +--- @param player LuaPlayer +--- @return LuaGuiElement +function ExpGui.get_left_element(define, player) + return player_elements[player.index].left[define.name] +end + +--- Register a element define to be drawn to the relative flow on join +--- @param define ExpElement +--- @param player LuaPlayer +--- @return LuaGuiElement +function ExpGui.get_relative_element(define, player) + return player_elements[player.index].relative[define.name] +end + +--- Return all top level elements for a player +---@param player LuaPlayer +---@return ExpGui.player_elements +function ExpGui._get_player_elements(player) + return player_elements[player.index] +end + +--- Ensure all the correct elements are visible and exist +--- @param player LuaPlayer +--- @param element_defines table +--- @param elements table +--- @param parent LuaGuiElement +local function ensure_elements(player, element_defines, elements, parent) + local done = {} + for define, visible in pairs(element_defines) do + local element = elements[define.name] + if not element then + element = define(parent) + elements[define.name] = element + assert(element, "Element define did not return an element: " .. define.name) + end + + if type(visible) == "function" then + visible = visible(player, element) + end + element.visible = visible + done[define.name] = true + end + + for name, element in pairs(elements) do + if not done[name] then + element.destroy() + elements[name] = nil + end + end +end + +--- Ensure all elements have been created +--- @param event EventData.on_player_created | EventData.on_player_joined_game +function ExpGui._ensure_elements(event) + local player = assert(game.get_player(event.player_index)) + local elements = player_elements[event.player_index] + if not elements then + elements = { + top = {}, + left = {}, + relative = {}, + } + player_elements[event.player_index] = elements + end + + ensure_elements(player, ExpGui.top_elements, elements.top, ExpGui.get_top_flow(player)) + ensure_elements(player, ExpGui.left_elements, elements.left, ExpGui.get_left_flow(player)) + ensure_elements(player, ExpGui.relative_elements, elements.relative, player.gui.relative) + + -- Check technically not needed, but makes it easier to use this lib without the core defines + if ExpGui.apply_consistency_checks then + ExpGui.apply_consistency_checks(player, true) + end +end + +--- Rerun the visible check for relative elements +--- @param event EventData.on_gui_opened +local function on_gui_opened(event) + local player = ExpGui.get_player(event) + local original_element = event.element + + for define, visible in pairs(ExpGui.relative_elements) do + local element = ExpGui.get_relative_element(define, player) + + if type(visible) == "function" then + visible = visible(player, element) + end + element.visible = visible + + if visible then + event.element = element + --- @diagnostic disable-next-line invisible + define:_raise_event(event) + end + end + + event.element = original_element +end + +local e = defines.events +local events = { + [e.on_player_created] = ExpGui._ensure_elements, + [e.on_player_joined_game] = ExpGui._ensure_elements, + [e.on_gui_opened] = on_gui_opened, +} + +ExpGui.events = events +return ExpGui diff --git a/exp_gui/module/core_elements.lua b/exp_gui/module/core_elements.lua new file mode 100644 index 00000000..ef1a6434 --- /dev/null +++ b/exp_gui/module/core_elements.lua @@ -0,0 +1,289 @@ + +--- @class ExpGui +local ExpGui = require("modules/exp_gui") +local mod_gui = require("mod-gui") + +local toolbar_button_default_style = mod_gui.button_style +local toolbar_button_active_style = "menu_button_continue" +local toolbar_button_size = 36 + +local elements = {} --- @type table +local buttons_with_left_element = {} --- @type table +local left_elements_with_button = {} --- @type table + +--- Set the style of a toolbar button +--- @param element LuaGuiElement +--- @param state boolean? +--- @return boolean +function ExpGui.set_toolbar_button_style(element, state) + if state == nil then state = element.style.name == toolbar_button_default_style end + element.style = state and toolbar_button_active_style or toolbar_button_default_style + + local style = element.style + style.minimal_width = toolbar_button_size + style.height = toolbar_button_size + if element.type == "sprite-button" then + style.padding = -2 + else + style.font = "default-semibold" + style.padding = 0 + end + + return state +end + +--- Set the visible state of the top flow for a player +--- @param player LuaPlayer +--- @param state boolean? +function ExpGui.set_top_flow_visible(player, state) + local top_flow = assert(ExpGui.get_top_flow(player).parent, player.name) + local show_top_flow = elements.core_button_flow.data[player].show_top_flow --- @type LuaGuiElement + + if state == nil then + state = not top_flow.visible + end + + top_flow.visible = state + show_top_flow.visible = not state +end + +--- Set the visible state of the left element for a player +--- @param define ExpElement +--- @param player LuaPlayer +--- @param state boolean? +function ExpGui.set_left_element_visible(define, player, state) + local element = assert(ExpGui.get_left_element(define, player), "Define is not added to the left flow") + local clear_left_flow = elements.core_button_flow.data[player].clear_left_flow --- @type LuaGuiElement + + -- Update the visible state + if state == nil then state = not element.visible end + element.visible = state + + -- Check if there is a toolbar button linked to this element + local toolbar_button = left_elements_with_button[define.name] + if toolbar_button then + ExpGui.set_toolbar_button_style(ExpGui.get_top_element(toolbar_button, player), state) + end + + -- If visible state is true, then we don't need to check all elements + if state then + clear_left_flow.visible = true + return + end + + -- Check if any left elements are visible + --- @diagnostic disable-next-line invisible + local player_elements = ExpGui._get_player_elements(player) + local flow_name = elements.core_button_flow.name + for name, left_element in pairs(player_elements.left) do + if left_element.visible and name ~= flow_name then + clear_left_flow.visible = true + return + end + end + + -- There are no visible left elements, so can hide the button + clear_left_flow.visible = false +end + +--- @class ExpGui.create_toolbar_button__param: LuaGuiElement.add_param.sprite_button, LuaGuiElement.add_param.button +--- @field name string +--- @field type nil +--- @field left_element ExpElement| nil +--- @field visible ExpGui.VisibleCallback | boolean | nil + +--- Create a toolbar button +--- @param options ExpGui.create_toolbar_button__param +--- @return ExpElement +function ExpGui.create_toolbar_button(options) + -- Extract the custom options from the element.add options + local name = assert(options.name, "Name is required for the element") + options.type = options.sprite ~= nil and "sprite-button" or "button" + + local visible = options.visible + if visible == nil then visible = true end + options.visible = nil + + local auto_toggle = options.auto_toggle + options.auto_toggle = nil + + local left_element = options.left_element + options.left_element = nil + + if options.style == nil then + options.style = toolbar_button_default_style + end + + -- Create the new element define + local toolbar_button = ExpGui.element(name) + :draw(options) + :style{ + minimal_width = toolbar_button_size, + height = toolbar_button_size, + padding = options.sprite ~= nil and -2 or nil, + } + + -- If a left element was given then link the define + if left_element then + left_elements_with_button[left_element.name] = toolbar_button + buttons_with_left_element[toolbar_button.name] = left_element + end + + -- Setup auto toggle, required if there is a left element + if auto_toggle or left_element then + toolbar_button:on_click(function(def, event) + local state = ExpGui.set_toolbar_button_style(event.element) + if left_element then + local player = ExpGui.get_player(event) + ExpGui.set_left_element_visible(left_element, player, state) + end + end) + end + + -- Add the define to the top flow and return + ExpGui.add_top_element(toolbar_button, visible) + return toolbar_button +end + +--- Update the consistency of the core elements and registered elements +--- @param player LuaPlayer +--- @param skip_ensure boolean? +function ExpGui.apply_consistency_checks(player, skip_ensure) + if not skip_ensure then + --- @diagnostic disable-next-line invisible + ExpGui._ensure_elements{ player_index = player.index } + end + + -- Get the core buttons for the player + local core_button_data = elements.core_button_flow.data[player] + local hide_top_flow = ExpGui.get_top_element(elements.hide_top_flow, player) + local show_top_flow = core_button_data.show_top_flow --- @type LuaGuiElement + local clear_left_flow = core_button_data.clear_left_flow --- @type LuaGuiElement + + -- Check if any top elements are visible, this includes ones not controlled by this module + local has_top_elements = false + local top_flow = ExpGui.get_top_flow(player) + for _, element in pairs(top_flow.children) do + if element.visible and element ~= hide_top_flow then + has_top_elements = true + break + end + end + + -- The show button is only visible when the flow isn't visible but does have visible children + show_top_flow.visible = has_top_elements and not top_flow.visible or false + + --- @diagnostic disable-next-line invisible + local player_elements = ExpGui._get_player_elements(player) + local left_elements, top_elements = player_elements.left, player_elements.top + + --- Update the styles of toolbar buttons with left elements + for name, top_element in pairs(top_elements) do + local left_element = buttons_with_left_element[name] + if left_element then + local element = assert(left_elements[left_element.name], left_element.name) + ExpGui.set_toolbar_button_style(top_element, element.visible) + end + end + + -- Check if any left elements are visible + local flow_name = elements.core_button_flow.name + for name, left_element in pairs(left_elements) do + if left_element.visible and name ~= flow_name then + clear_left_flow.visible = true + return + end + end + + -- There are no visible left elements, so can hide the button + clear_left_flow.visible = false +end + +--- Hides the top flow when clicked +elements.hide_top_flow = ExpGui.element("hide_top_flow") + :draw{ + type = "sprite-button", + sprite = "utility/preset", + style = "tool_button", + tooltip = { "exp-gui.hide-top-flow" }, + } + :style{ + padding = -2, + width = 18, + height = 36, + } + :on_click(function(def, event) + local player = ExpGui.get_player(event) + ExpGui.set_top_flow_visible(player, false) + end) + +--- Shows the top flow when clicked +elements.show_top_flow = ExpGui.element("show_top_flow") + :draw{ + type = "sprite-button", + sprite = "utility/preset", + style = "tool_button", + tooltip = { "exp-gui.show-top-flow" }, + } + :style{ + padding = -2, + width = 18, + height = 20, + } + :on_click(function(def, event) + local player = ExpGui.get_player(event) + ExpGui.set_top_flow_visible(player, true) + end) + +--- Hides all left elements when clicked +elements.clear_left_flow = ExpGui.element("clear_left_flow") + :draw{ + type = "sprite-button", + sprite = "utility/close_black", + style = "tool_button", + tooltip = { "exp-gui.clear-left-flow" }, + } + :style{ + padding = -3, + width = 18, + height = 20, + } + :on_click(function(def, event) + local player = ExpGui.get_player(event) + event.element.visible = false + + --- @diagnostic disable-next-line invisible + local player_elements = ExpGui._get_player_elements(player) + local flow_name = elements.core_button_flow.name + for name, left_element in pairs(player_elements.left) do + if name ~= flow_name then + left_element.visible = false + local toolbar_button = left_elements_with_button[name] + if toolbar_button then + ExpGui.set_toolbar_button_style(ExpGui.get_top_element(toolbar_button, player), false) + end + end + end + end) + +--- Contains the two buttons on the left flow +elements.core_button_flow = ExpGui.element("core_button_flow") + :draw(function(def, parent) + local flow = parent.add{ + type = "flow", + direction = "vertical", + } + + local player = ExpGui.get_player(parent) + def.data[player] = { + show_top_flow = elements.show_top_flow(flow), + clear_left_flow = elements.clear_left_flow(flow), + } + + return flow + end) + +ExpGui.add_top_element(elements.hide_top_flow, true) +ExpGui.add_left_element(elements.core_button_flow, true) + +return elements diff --git a/exp_gui/module/data.lua b/exp_gui/module/data.lua index 113cff58..b5ceac5c 100644 --- a/exp_gui/module/data.lua +++ b/exp_gui/module/data.lua @@ -56,7 +56,7 @@ local GuiData = { -- This class has no prototype methods -- Do add keys to _raw without also referencing scope_data ---- @class ExpGui.GuiData: table +--- @class ExpGui.GuiData: ExpGui.GuiData_Internal --- @field element_data table> --- @field player_data table --- @field force_data table diff --git a/exp_gui/module/locale/en.cfg b/exp_gui/module/locale/en.cfg index e69de29b..29321c42 100644 --- a/exp_gui/module/locale/en.cfg +++ b/exp_gui/module/locale/en.cfg @@ -0,0 +1,5 @@ + +[exp-gui] +hide-top-flow=Hide Toolbar. +show-top-flow=Show Toolbar. +clear-left-flow=Hide all open windows. \ No newline at end of file diff --git a/exp_gui/module/module.json b/exp_gui/module/module.json index 1fdadc0d..7cd7b787 100644 --- a/exp_gui/module/module.json +++ b/exp_gui/module/module.json @@ -4,9 +4,11 @@ "data.lua", "iter.lua", "prototype.lua", - "module_exports.lua" + "control.lua" ], "require": [ + "core_elements.lua", + "test.lua" ], "dependencies": { "clusterio": "*", diff --git a/exp_gui/module/module_exports.lua b/exp_gui/module/module_exports.lua index 47f3377d..a8322b0e 100644 --- a/exp_gui/module/module_exports.lua +++ b/exp_gui/module/module_exports.lua @@ -1,186 +1 @@ - -local Storage = require("modules/exp_util/storage") - -local ExpElement = require("./prototype") - ---- @alias ExpGui.VisibleCallback fun(player: LuaPlayer, element: LuaGuiElement): boolean - ---- @class ExpGui.player_elements ---- @field top table ---- @field left table ---- @field relative table - ---- @type table -local player_elements = {} -Storage.register(player_elements, function(tbl) - player_elements = tbl -end) - ---- @class ExpGui -local ExpGui = { - element = ExpElement, - top_elements = {}, --- @type table - left_elements = {}, --- @type table - relative_elements = {}, --- @type table -} - -local mod_gui = require("mod-gui") -ExpGui.get_top_flow = mod_gui.get_button_flow -ExpGui.get_left_flow = mod_gui.get_frame_flow - ---- Get a player from an element or gui event ---- @param input LuaGuiElement | { player_index: uint } ---- @return LuaPlayer -function ExpGui.get_player(input) - return assert(game.get_player(input.player_index)) -end - ---- Toggle the enable state of an element ---- @param element LuaGuiElement ---- @param state boolean? -function ExpGui.toggle_enabled_state(element, state) - if not element or not element.valid then return end - if state == nil then - state = not element.enabled - end - element.enabled = state -end - ---- Toggle the visibility of an element ---- @param element LuaGuiElement ---- @param state boolean? -function ExpGui.toggle_visible_state(element, state) - if not element or not element.valid then return end - if state == nil then - state = not element.visible - end - element.visible = state -end - ---- Destroy an element if it exists and is valid ---- @param element LuaGuiElement? -function ExpGui.destroy_if_valid(element) - if not element or not element.valid then return end - element.destroy() -end - ---- Register a element define to be drawn to the top flow on join ---- @param define ExpElement ---- @param visible ExpGui.VisibleCallback | boolean | nil -function ExpGui.add_top_element(define, visible) - assert(ExpGui.top_elements[define.name] == nil, "Element is already added to the top flow") - ExpGui.top_elements[define] = visible or false -end - ---- Register a element define to be drawn to the left flow on join ---- @param define ExpElement ---- @param visible ExpGui.VisibleCallback | boolean | nil -function ExpGui.add_left_element(define, visible) - assert(ExpGui.left_elements[define.name] == nil, "Element is already added to the left flow") - ExpGui.left_elements[define] = visible or false - -end - ---- Register a element define to be drawn to the relative flow on join ---- @param define ExpElement ---- @param visible ExpGui.VisibleCallback | boolean | nil -function ExpGui.add_relative_element(define, visible) - assert(ExpGui.relative_elements[define.name] == nil, "Element is already added to the relative flow") - ExpGui.relative_elements[define] = visible or false -end - ---- Register a element define to be drawn to the top flow on join ---- @param define ExpElement ---- @param player LuaPlayer ---- @return LuaGuiElement -function ExpGui.get_top_element(define, player) - return player_elements[player.index].top[define.name] -end - ---- Register a element define to be drawn to the left flow on join ---- @param define ExpElement ---- @param player LuaPlayer ---- @return LuaGuiElement -function ExpGui.get_left_element(define, player) - return player_elements[player.index].left[define.name] -end - ---- Register a element define to be drawn to the relative flow on join ---- @param define ExpElement ---- @param player LuaPlayer ---- @return LuaGuiElement -function ExpGui.get_relative_element(define, player) - return player_elements[player.index].relative[define.name] -end - ---- Ensure all the correct elements are visible and exist ---- @param player LuaPlayer ---- @param element_defines table ---- @param elements LuaGuiElement[] ---- @param parent LuaGuiElement -local function ensure_elements(player, element_defines, elements, parent) - local done = {} - for define, visible in pairs(element_defines) do - local element = elements[define.name] - if not element then - element = define(parent) - end - - if type(visible) == "function" then - visible = visible(player, element) - end - element.visible = visible - done[define.name] = true - end - - for name, element in pairs(elements) do - if not done[name] then - element.destroy() - elements[name] = nil - end - end -end - ---- Ensure all elements have been created ---- @param event EventData.on_player_created | EventData.on_player_joined_game -function ExpGui._ensure_elements(event) - local player = assert(game.get_player(event.player_index)) - local elements = player_elements[event.player_index] - ensure_elements(player, ExpGui.top_elements, elements.top, player.gui.top) - ensure_elements(player, ExpGui.left_elements, elements.left, player.gui.left) - ensure_elements(player, ExpGui.relative_elements, elements.relative, player.gui.relative) -end - ---- Rerun the visible check for relative elements ---- @param event EventData.on_gui_opened -local function on_gui_opened(event) - local player = ExpGui.get_player(event) - local original_element = event.element - - for define, visible in pairs(ExpGui.relative_elements) do - local element = ExpGui.get_relative_element(define, player) - - if type(visible) == "function" then - visible = visible(player, element) - end - element.visible = visible - - if visible then - event.element = element - --- @diagnostic disable-next-line invisible - define:_raise_event(event) - end - end - - event.element = original_element -end - -local e = defines.events -local events = { - [e.on_player_created] = ExpGui._ensure_elements, - [e.on_player_joined_game] = ExpGui._ensure_elements, - [e.on_gui_opened] = on_gui_opened, -} - -ExpGui.events = events -return ExpGui +return require("modules/exp_gui/control") diff --git a/exp_gui/module/prototype.lua b/exp_gui/module/prototype.lua index de238010..083dc372 100644 --- a/exp_gui/module/prototype.lua +++ b/exp_gui/module/prototype.lua @@ -1,8 +1,8 @@ local ExpUtil = require("modules/exp_util") -local GuiData = require("./data") -local GuiIter = require("./iter") +local GuiData = require("modules/exp_gui/data") +local GuiIter = require("modules/exp_gui/iter") --- @class ExpGui_ExpElement local ExpElement = { @@ -54,6 +54,12 @@ ExpElement._metatable = { __class = "ExpGui", } +--- Used to signal that the property should be the same as the define name +--- @return function +function ExpElement.property_from_name() + return ExpElement.property_from_name +end + --- Used to signal that a property should be taken from the arguments --- @param arg_number number? --- @return [function, number?] @@ -64,11 +70,13 @@ end --- Extract the from args properties from a definition --- @param definition table --- @return string[] -local function extract_from_args(definition) +function ExpElement._prototype:_extract_signals(definition) local from_args = {} for k, v in pairs(definition) do if v == ExpElement.property_from_args then from_args[#from_args + 1] = k + elseif v == ExpElement.property_from_name then + definition[k] = self.name elseif type(v) == "table" and v[1] == ExpElement.property_from_args then from_args[v[2] or (#from_args + 1)] = k end @@ -81,18 +89,20 @@ end --- @return ExpElement function ExpElement.create(name) ExpUtil.assert_not_runtime() - assert(ExpElement._elements[name] == nil, "ExpElement already defined with name: " .. name) + local module_name = ExpUtil.get_module_name(2) + local element_name = module_name .. "/" .. name + assert(ExpElement._elements[element_name] == nil, "ExpElement already defined with name: " .. name) local instance = { - name = name, - data = GuiData.create(name), + name = element_name, + data = GuiData.create(element_name), _events = {}, _debug = { defined_at = ExpUtil.safe_file_path(2), }, } - ExpElement._elements[name] = instance + ExpElement._elements[element_name] = instance return setmetatable(instance, ExpElement._metatable) end @@ -180,7 +190,7 @@ function ExpElement._prototype:draw(definition) end assert(type(definition) == "table", "Definition is not a table or function") - local from_args = extract_from_args(definition) + local from_args = self:_extract_signals(definition) self._debug.draw_definition = definition if #from_args == 0 then @@ -216,7 +226,7 @@ local function definition_factory(prop_name, debug_def, debug_args) end assert(type(definition) == "table", "Definition is not a table or function") - local from_args = extract_from_args(definition) + local from_args = self:_extract_signals(definition) self._debug[debug_def] = definition if #from_args == 0 then diff --git a/exp_gui/module/test.lua b/exp_gui/module/test.lua new file mode 100644 index 00000000..e23ef9da --- /dev/null +++ b/exp_gui/module/test.lua @@ -0,0 +1,16 @@ + +local ExpGui = require("modules/exp_gui") + +local frame = ExpGui.element("test") + :draw{ + type = "frame", + caption = "Hello, World", + } + +ExpGui.add_left_element(frame, true) + +ExpGui.create_toolbar_button{ + name = "test-button", + left_element = frame, + caption = "Test", +}