525 lines
16 KiB
Lua
525 lines
16 KiB
Lua
--- From flib 0.10.1 by Raiguard
|
||
|
||
local mod_name = script.mod_name
|
||
local gui_event_defines = {}
|
||
|
||
local event_id_to_string_mapping = {}
|
||
for name, id in pairs(defines.events) do
|
||
if string.find(name, "^on_gui") then
|
||
gui_event_defines[name] = id
|
||
event_id_to_string_mapping[id] = string.gsub(name, "^on_gui", "on")
|
||
end
|
||
end
|
||
|
||
--- GUI structuring tools and event handling.
|
||
local flib_gui = {}
|
||
|
||
--- Provide a callback to be run for GUI events.
|
||
---
|
||
--- # Examples
|
||
---
|
||
--- ```lua
|
||
--- gui.hook_events(function(e)
|
||
--- local msg = gui.read_action(e)
|
||
--- if msg then
|
||
--- -- read the action to determine what to do
|
||
--- end
|
||
--- end)
|
||
--- ```
|
||
--- @param callback function
|
||
function flib_gui.hook_events(callback)
|
||
local on_event = script.on_event
|
||
for _, id in pairs(gui_event_defines) do
|
||
on_event(id, callback)
|
||
end
|
||
end
|
||
|
||
--- Retrieve the action message from a GUI element's tags.
|
||
---
|
||
--- # Examples
|
||
---
|
||
--- ```lua
|
||
--- event.on_gui_click(function(e)
|
||
--- local action = gui.read_action(e)
|
||
--- if action then
|
||
--- -- do stuff
|
||
--- end
|
||
--- end)
|
||
--- ```
|
||
--- @param event_data EventData
|
||
--- @return any? action The element's action for this GUI event.
|
||
function flib_gui.read_action(event_data)
|
||
local elem = event_data.element
|
||
if not elem or not elem.valid then
|
||
return
|
||
end
|
||
|
||
local mod_tags = elem.tags[mod_name]
|
||
if not mod_tags then
|
||
return
|
||
end
|
||
|
||
local elem_actions = mod_tags.flib
|
||
if not elem_actions then
|
||
return
|
||
end
|
||
|
||
local event_name = event_id_to_string_mapping[event_data.name]
|
||
local msg = elem_actions[event_name]
|
||
|
||
return msg
|
||
end
|
||
|
||
--- Navigate a structure to build a GUI
|
||
--- @param parent LuaGuiElement
|
||
--- @param structure GuiBuildStructure
|
||
--- @param refs table
|
||
local function recursive_build(parent, structure, refs)
|
||
-- If the structure has no type, just ignore it
|
||
-- This is to make it possible to pass unit types `{}` to represent "no element" without breaking things
|
||
if not structure.type then
|
||
return
|
||
end
|
||
|
||
-- Prepare tags
|
||
local original_tags = structure.tags
|
||
local tags = original_tags or {}
|
||
local actions = structure.actions
|
||
local tags_flib = tags.flib
|
||
tags.flib = actions
|
||
structure.tags = { [mod_name] = tags }
|
||
|
||
-- Make the game not convert these into a property tree for no reason
|
||
structure.actions = nil
|
||
-- Substructures can be defined in special tables or as the array portion of this structure
|
||
local substructures
|
||
local substructures_len = #structure
|
||
if substructures_len > 0 then
|
||
if structure.children or structure.tabs then
|
||
error("Children or tab-and-content pairs must ALL be in the array portion, or a subtable. Not both at once!")
|
||
end
|
||
substructures = {}
|
||
for i = 1, substructures_len do
|
||
substructures[i] = structure[i]
|
||
structure[i] = nil
|
||
end
|
||
else
|
||
substructures = structure.children or structure.tabs
|
||
structure.children = nil
|
||
structure.tabs = nil
|
||
end
|
||
|
||
-- Create element
|
||
local elem = parent.add(structure)
|
||
|
||
-- Restore structure
|
||
structure.tags = original_tags
|
||
structure.actions = actions
|
||
tags.flib = tags_flib
|
||
|
||
local style_mods = structure.style_mods
|
||
if style_mods then
|
||
for k, v in pairs(style_mods) do
|
||
elem.style[k] = v
|
||
end
|
||
end
|
||
|
||
local elem_mods = structure.elem_mods
|
||
if elem_mods then
|
||
for k, v in pairs(elem_mods) do
|
||
elem[k] = v
|
||
end
|
||
end
|
||
|
||
local ref = structure.ref
|
||
if ref then
|
||
-- Recursively create tables as needed
|
||
local prev = refs
|
||
local ref_length = #ref
|
||
for i = 1, ref_length - 1 do
|
||
local current_key = ref[i]
|
||
local current = prev[current_key]
|
||
if not current then
|
||
current = {}
|
||
prev[current_key] = current
|
||
end
|
||
prev = current
|
||
end
|
||
prev[ref[ref_length]] = elem
|
||
end
|
||
|
||
-- Substructures
|
||
if substructures then
|
||
if structure.type == "tabbed-pane" then
|
||
local add_tab = elem.add_tab
|
||
for i = 1, #substructures do
|
||
local tab_and_content = substructures[i]
|
||
if not (tab_and_content.tab and tab_and_content.content) then
|
||
error("TabAndContent must have `tab` and `content` fields")
|
||
end
|
||
local tab = recursive_build(elem, tab_and_content.tab, refs)
|
||
local content = recursive_build(elem, tab_and_content.content, refs)
|
||
add_tab(tab, content)
|
||
end
|
||
else
|
||
for i = 1, #substructures do
|
||
recursive_build(elem, substructures[i], refs)
|
||
end
|
||
end
|
||
end
|
||
|
||
return elem
|
||
end
|
||
|
||
--- Build a GUI based on the given structure(s).
|
||
--- @param parent LuaGuiElement The parent GUI element where the new GUI will be located.
|
||
--- @param structures GuiBuildStructure[] The GUIs to build.
|
||
--- @return table refs `LuaGuiElement` references and subtables, built based on the values of `ref` throughout the `GuiBuildStructure`.
|
||
function flib_gui.build(parent, structures)
|
||
local refs = {}
|
||
for i = 1, #structures do
|
||
recursive_build(parent, structures[i], refs)
|
||
end
|
||
return refs
|
||
end
|
||
|
||
--- Build a single element based on a GuiStructure.
|
||
---
|
||
--- This is to allow use of `style_mods`, `actions` and `tags` without needing to use `gui.build()` for a single element.
|
||
---
|
||
--- Unlike `gui.build()`, the element will be automatically returned from the function without needing to use `ref`. If
|
||
--- you need to obtain references to children of this element, use `gui.build()` instead.
|
||
--- @param parent LuaGuiElement The parent GUI element where this new element will be located.
|
||
--- @param structure GuiBuildStructure The element to build.
|
||
--- @return LuaGuiElement elem A reference to the element that was created.
|
||
function flib_gui.add(parent, structure)
|
||
-- Just in case they had a ref in the structure already, extract it
|
||
local previous_ref = structure.ref
|
||
-- Put in a known ref that we can use later
|
||
structure.ref = { "FLIB_ADD_ROOT" }
|
||
-- Build the element
|
||
local refs = {}
|
||
recursive_build(parent, structure, refs)
|
||
-- Restore the previous ref
|
||
structure.ref = previous_ref
|
||
-- Return the element
|
||
return refs.FLIB_ADD_ROOT
|
||
end
|
||
|
||
--- @param elem LuaGuiElement
|
||
--- @param updates GuiUpdateStructure
|
||
local function recursive_update(elem, updates)
|
||
if updates.cb then
|
||
updates.cb(elem)
|
||
end
|
||
|
||
if updates.style then
|
||
elem.style = updates.style
|
||
end
|
||
|
||
if updates.style_mods then
|
||
for key, value in pairs(updates.style_mods) do
|
||
elem.style[key] = value
|
||
end
|
||
end
|
||
|
||
if updates.elem_mods then
|
||
for key, value in pairs(updates.elem_mods) do
|
||
elem[key] = value
|
||
end
|
||
end
|
||
|
||
if updates.tags then
|
||
flib_gui.update_tags(elem, updates.tags)
|
||
end
|
||
|
||
-- TODO: This could be a lot better
|
||
if updates.actions then
|
||
for event_name, payload in pairs(updates.actions) do
|
||
flib_gui.set_action(elem, event_name, payload)
|
||
end
|
||
end
|
||
|
||
local substructures
|
||
local substructures_len = #updates
|
||
if substructures_len > 0 then
|
||
if updates.children or updates.tabs then
|
||
error("Children or tab-and-content pairs must ALL be in the array portion, or a subtable. Not both at once!")
|
||
end
|
||
substructures = {}
|
||
for i = 1, substructures_len do
|
||
substructures[i] = updates[i]
|
||
updates[i] = nil
|
||
end
|
||
else
|
||
substructures = updates.children or updates.tabs
|
||
updates.children = nil
|
||
updates.tabs = nil
|
||
end
|
||
local subelements
|
||
if elem.type == "tabbed-pane" then
|
||
subelements = elem.tabs
|
||
else
|
||
subelements = elem.children
|
||
end
|
||
|
||
if substructures then
|
||
for i, substructure in pairs(substructures) do
|
||
if substructure.tab or substructure.content then
|
||
local elem_tab_and_content = subelements[i]
|
||
if elem_tab_and_content then
|
||
local tab = elem_tab_and_content.tab
|
||
local tab_updates = substructures.tab
|
||
if tab and tab_updates then
|
||
recursive_update(tab, tab_updates)
|
||
end
|
||
local content = elem_tab_and_content.content
|
||
local content_updates = substructures.content
|
||
if content and content_updates then
|
||
recursive_update(content, content_updates)
|
||
end
|
||
end
|
||
elseif subelements[i] then
|
||
recursive_update(subelements[i], substructure)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
--- Update an existing GUI based on a given structure.
|
||
--- @param elem LuaGuiElement The element to update.
|
||
--- @param updates GuiUpdateStructure The updates to perform.
|
||
function flib_gui.update(elem, updates)
|
||
recursive_update(elem, updates)
|
||
end
|
||
|
||
--- Retrieve a GUI element's tags.
|
||
---
|
||
--- These tags are automatically written to and read from a subtable keyed by mod name, preventing conflicts.
|
||
---
|
||
--- If no tags exist, this function will return an empty table.
|
||
--- @param elem LuaGuiElement
|
||
--- @return table
|
||
function flib_gui.get_tags(elem)
|
||
return elem.tags[mod_name] or {}
|
||
end
|
||
|
||
--- Set (override) a GUI element's tags.
|
||
---
|
||
--- These tags are automatically written to and read from a subtable keyed by mod name, preventing conflicts.
|
||
--- @param elem LuaGuiElement
|
||
--- @param tags table
|
||
function flib_gui.set_tags(elem, tags)
|
||
local elem_tags = elem.tags
|
||
elem_tags[mod_name] = tags
|
||
elem.tags = elem_tags
|
||
end
|
||
|
||
--- Delete a GUI element's tags.
|
||
--- These tags are automatically written to and read from a subtable keyed by mod name, preventing conflicts.
|
||
---
|
||
--- @param elem LuaGuiElement
|
||
function flib_gui.delete_tags(elem)
|
||
local elem_tags = elem.tags
|
||
elem_tags[mod_name] = nil
|
||
elem.tags = elem_tags
|
||
end
|
||
|
||
--- Perform a shallow merge on a GUI element's tags.
|
||
---
|
||
--- These tags are automatically written to and read from a subtable keyed by mod name, preventing conflicts.
|
||
---
|
||
--- Only the top level will be updated. If deep updating is needed, use `gui.get_tags` and `table.deep_merge`, then
|
||
--- `gui.set_tags`.
|
||
--- @param elem LuaGuiElement
|
||
--- @param updates table
|
||
function flib_gui.update_tags(elem, updates)
|
||
local elem_tags = elem.tags
|
||
local existing = elem_tags[mod_name]
|
||
|
||
if not existing then
|
||
existing = {}
|
||
elem_tags[mod_name] = existing
|
||
end
|
||
|
||
for k, v in pairs(updates) do
|
||
existing[k] = v
|
||
end
|
||
|
||
elem.tags = elem_tags
|
||
end
|
||
|
||
--- Set (overwrite) the specified action message for this GUI element.
|
||
--- @param elem LuaGuiElement
|
||
--- @param event_name string The GUI event name for this action, with the `_gui` portion omitted (i.e. `on_click`).
|
||
--- @param msg any? The action message, or `nil` to clear the action.
|
||
function flib_gui.set_action(elem, event_name, msg)
|
||
local elem_tags = elem.tags
|
||
local existing = elem_tags[mod_name]
|
||
|
||
if not existing then
|
||
existing = {}
|
||
elem_tags[mod_name] = existing
|
||
end
|
||
|
||
local actions = existing.flib
|
||
if not actions then
|
||
actions = {}
|
||
existing.flib = actions
|
||
end
|
||
|
||
actions[event_name] = msg or nil
|
||
|
||
elem.tags = elem_tags
|
||
end
|
||
|
||
--- Retrieve the specified action message for this GUI element.
|
||
--- @param elem LuaGuiElement
|
||
--- @param event_name string The GUI event name to get the action message for, with the `_gui` portion omitted (i.e. `on_click`).
|
||
--- @return any|nil msg The action message, if there is one.
|
||
function flib_gui.get_action(elem, event_name)
|
||
local elem_tags = elem.tags
|
||
local existing = elem_tags[mod_name]
|
||
|
||
if not existing then
|
||
return
|
||
end
|
||
|
||
local actions = existing.flib
|
||
if not actions then
|
||
return
|
||
end
|
||
|
||
return actions[event_name]
|
||
end
|
||
|
||
--- A series of nested tables used to build a GUI.
|
||
---
|
||
--- This is an extension of `LuaGuiElement`, providing new features and options.
|
||
---
|
||
--- This inherits all required properties from its base `LuaGuiElement`, i.e. if the `type` field is
|
||
--- `sprite-button`, the `GuiBuildStructure` must contain all the fields that a `sprite-button` `LuaGuiElement`
|
||
--- requires.
|
||
---
|
||
--- There are a number of new fields that can be applied to a `GuiBuildStructure` depending on the type.
|
||
---
|
||
--- # Example
|
||
---
|
||
--- ```lua
|
||
--- gui.build(player.gui.screen, {
|
||
--- {
|
||
--- type = "frame",
|
||
--- direction = "vertical",
|
||
--- ref = {"window"},
|
||
--- actions = {
|
||
--- on_closed = {gui = "demo", action = "close"}
|
||
--- },
|
||
--- -- Titlebar
|
||
--- {type = "flow", ref = {"titlebar", "flow"},
|
||
--- {type = "label", style = "frame_title", caption = "Menu", ignored_by_interaction = true},
|
||
--- {type = "empty-widget", style = "flib_titlebar_drag_handle", ignored_by_interaction = true},
|
||
--- {
|
||
--- type = "sprite-button",
|
||
--- style = "frame_action_button",
|
||
--- sprite = "utility/close_white",
|
||
--- hovered_sprite = "utility/close_black",
|
||
--- clicked_sprite = "utility/close_black",
|
||
--- ref = {"titlebar", "close_button"},
|
||
--- actions = {
|
||
--- on_click = {gui = "demo", action = "close"}
|
||
--- }
|
||
--- }
|
||
--- },
|
||
--- -- Content
|
||
--- {type = "frame", style = "inside_deep_frame_for_tabs",
|
||
--- {type = "tabbed-pane",
|
||
--- {
|
||
--- tab = {type = "tab", caption = "1"},
|
||
--- content = {type = "table", style = "slot_table", column_count = 10, ref = {"tables", 1}}
|
||
--- },
|
||
--- {
|
||
--- tab = {type = "tab", caption = "2"},
|
||
--- content = {type = "table", style = "slot_table", column_count = 10, ref = {"tables", 2}}
|
||
--- }
|
||
--- }
|
||
--- }
|
||
--- }
|
||
--- })
|
||
--- ```
|
||
--- @class GuiBuildStructure
|
||
--- @field style_mods? table<string, any>
|
||
--- @field elem_mods? table<string, any>
|
||
--- @field tags? table
|
||
--- @field actions? GuiElementActions
|
||
--- @field ref? string[]
|
||
--- @field children? GuiBuildStructure[]
|
||
--- @field tabs? TabAndContent[]
|
||
|
||
---- A series of nested tables used to update a GUI.
|
||
---
|
||
--- # Examples
|
||
---
|
||
--- ```lua
|
||
--- gui.update(
|
||
--- my_frame,
|
||
--- {
|
||
--- elem_mods = {caption = "Hello there!"},
|
||
--- tags = {subject = "General Kenobi"},
|
||
--- actions = {on_click = "everybody_say_hey"},
|
||
--- {
|
||
--- {
|
||
--- {tab = {elem_mods = {badge_text = "69"}}, content = {...}},
|
||
--- {content = {...}}
|
||
--- }
|
||
--- }
|
||
--- }
|
||
--- )
|
||
--- ```
|
||
--- @class GuiUpdateStructure
|
||
--- @field cb? function A callback to run on this GUI element. The callback will be passed a `LuaGuiElement` as its first parameter.
|
||
--- @field style? string The new style that the element should use.
|
||
--- @field style_mods? table A key -> value dictionary defining modifications to make to the element's style. Available properties are listed in `LuaStyle`.
|
||
--- @field elem_mods? table A key –> value dictionary defining modifications to make to the element. Available properties are listed in LuaGuiElement.
|
||
--- @field tags? table Tags that should be added to the element. This is identical to calling `gui.update_tags` on the element.
|
||
--- @field actions? table Actions that should be added to the element. The format is identical to `actions` in a `GuiBuildStructure`. This is identical to calling `set_action` for each action on this element.
|
||
--- @field children? GuiUpdateStructure[] `GuiUpdateStructure`s to apply to the children of this `LuaGuiElement`. This may alternatively be defined in the array portion of the parent structure to improve readability.
|
||
--- @field tabs? TabAndContent[] `TabAndContent`s to apply to the tabs of this `LuaGuiElement`. This may alternatively be defined in the array portion of the parent structure to improve readability.
|
||
|
||
--- A mapping of GUI event name -> action message.
|
||
---
|
||
--- Each key is a GUI event name (`on_gui_click`, `on_gui_elem_changed`, etc.) with the `_gui` part removed. For example, `on_gui_click` will become `on_click`.
|
||
---
|
||
--- Each value is a custom set of data that `gui.read_action` will return when that GUI event is fired and passes
|
||
--- this GUI element. This data may be of any type, as long as it is truthy.
|
||
---
|
||
--- Actions are kept under a `flib` subtable in the element's mod-specific tags subtable, retrievable with
|
||
--- `gui.get_tags`. Because of this, there is no chance of accidental mod action overlaps, so feel free to use
|
||
--- generic actions such as "close" or "open".
|
||
---
|
||
--- A common format for a mod with multiple GUIs might be to give each GUI a name, and write the actions as shown below.
|
||
---
|
||
--- # Example
|
||
---
|
||
--- ```lua
|
||
--- gui.build(player.gui.screen, {
|
||
--- {
|
||
--- type = "frame",
|
||
--- caption = "My frame",
|
||
--- actions = {
|
||
--- on_click = {gui = "my_gui", action = "handle_click"},
|
||
--- on_closed = {gui = "my_gui", action = "close"}
|
||
--- }
|
||
--- }
|
||
--- })
|
||
--- ```
|
||
--- @class GuiElementActions
|
||
|
||
--- A table representing a tab <-> content pair.
|
||
---
|
||
--- When used in `gui.build`, both fields are required. When used in `gui.update`, both fields are optional.
|
||
--- @class TabAndContent
|
||
--- @field tab GuiBuildStructure|GuiUpdateStructure
|
||
--- @field content GuiBuildStructure|GuiUpdateStructure
|
||
|
||
return flib_gui
|