Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
ahicks92 committed Nov 29, 2024
1 parent 3b665d1 commit 86d8f23
Show file tree
Hide file tree
Showing 5 changed files with 335 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,6 @@
}
],
"Lua.workspace.userThirdParty": [
"c:\\Users\\Austin\\AppData\\Roaming\\Code\\User\\workspaceStorage\\5232b5213d87a49c56164541939376a4\\justarandomgeek.factoriomod-debug\\sumneko-3rd"
"c:\\Users\\Austin\\AppData\\Roaming\\Code\\User\\workspaceStorage\\63b7677c3b16e179985a791a1818f333\\justarandomgeek.factoriomod-debug\\sumneko-3rd"
]
}
2 changes: 2 additions & 0 deletions locale/en/ui-grid.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[fa]
ui-grid-empty=Empty grid
14 changes: 14 additions & 0 deletions math-helpers.lua
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,18 @@ function mod.mod1(index, length)
return ((index - 1) % length) + 1
end

---@param x number
---@param low number
---@param high number
---@return number
function mod.clamp(x, low, high)
if x < low then
return low
elseif x > high then
return high
else
return x
end
end

return mod
86 changes: 86 additions & 0 deletions scripts/ui/grid.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
--[[
A simple grid: you give it a callback to get cell contents, dimensions, and
row/column names, and it handles figuring out all the keyboard stuff for you.
Designed to be mounted in a TabList (ui/tab-list.lua). Callbacks should return
tablist responses to force closure.
Grid positions are 1-based, to match lua tables.
Your state will have a field `grid` injected into it; this field should be
treated as private. This allows passing you the full tab context directly, as
if you had implemented all of this logic yourself.
]]
local Math2 = require("math-helpers")

local mod = {}

---@alias fa.ui.GridStringOrResponse LocalisedString | fa.ui.TabEventResponse
---@alias fa.ui.GridNames (LocalisedString[])|(fun(number): fa.ui.GridStringOrResponse)

---@class fa.ui.GridCallbacks
---@field title LocalisedString
---@field row_names fa.ui.GridNames
---@field col_names fa.ui.GridNames
---@field get_dims fun(self, fa.ui.TabContext): (number| fa.ui.TabEventResponse, number?)
---@field read_cell fun(self, fa.ui.TabContext, number, number): fa.ui.GridStringOrResponse

---@class fa.ui.GridState
---@field x number
---@field y number

---@class fa.ui.GridContext: fa.ui.TabContext
---@field state { grid: fa.ui.GridState }

---@class fa.ui.Grid: fa.ui.TabCallbacks
---@field callbacks fa.ui.GridCallbacks
local GridImpl = {}

---@param context fa.ui.GridContext
---@return fa.ui.TabEventResponse?
function GridImpl:_force_inbounds(context)
local size_x, size_y = self.callbacks.get_dims(context)

-- Forced closure.
if type(size_x) ~= "number" then return size_x end
assert(size_y)

if size_x == 0 or size_y == 0 then return end

local state = context.state.grid
state.x = Math2.clamp(state.x, 1, size_x)
state.y = Math2.clamp(state.y, 1, size_y)
end

function printout_if_lstring(msg, pindex)
if type(msg) == "table" and msg.kind then return msg end
printout(msg, pindex)
end
---@param context fa.ui.GridContext
function GridImpl:_move(context, dx, dy)
local state = context.state.grid
local x, y = state.x, state.y
local old_x, old_y = x, y

state.x = x + dx
state.y = y + dy
local forced_resp = self:_force_inbounds(context)
if forced_resp then return forced_resp end

local moved = state.x ~= old_x or state.y ~= old_y
return printout_if_lstring(self:_read_cell_string(context), context.pindex)
end

---@return fa.ui.TabEventResponse|LocalisedString
function GridImpl:_read_cell_string(context)
local forced_resp = self:_force_inbounds(context)
if forced_resp then return forced_resp end
local size_x, size_y = self.callbacks:get_dims(context)
if type(size_x) ~= "number" then return size_x end
assert(size_y)

if size_x == 0 or size_y == 0 then return { "fa.ui-grid-empty" } end
return self.callbacks:read_cell(context.state.grid.x, context.state.grid.y)
end

return mod
232 changes: 232 additions & 0 deletions scripts/ui/tab-list.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
--[[
Holds a list of named tabs, and lets you select between them. This is a lesser
form of #263, which can be converted to the full form as a special case, by
creating a layer that just calls the methods. That's a good idea anyway:
ultimately no one wants to write complex UI by individually handling keys.
For now this does not support dynamicism. That is to say that tabs must be
declared at the top level of a file. You don't get multiple instances per
player. For example you can't have two open belt analyzers. As is the theme,
this can also be extended later once we are at the point of being able to have a
proper framework. In particular, layers can be used to provide the missing UI
stack abstractions that would allow multiple arbitrary sets of UIs to all be
open at once.
For now this isn't super documented. If you are looking for some overarching
framework you won't find it. That comes later. This just gets us off the
ground.
This is the top level of the UI hierarchy and also handles the state management.
Each tab will receive a context with a field to use for persistent state.
Whatever it stores there persists. It can reset this state by responding to
`on_tab_focused` and related events. Again, not super documented for now: this
is just better than status quo.
To clarify how input gets here, this is currently hardcoded in the menu logic in
control.lua. That is, by checking global.players[pindex].menu_name, and setting
it for the open tab list.
]]
local StorageManager = require("scripts.storage-manager")
local Math2 = require("math-helpers")

local mod = {}

---@class fa.ui.TabContext
---@field pindex number
---@field player LuaPlayer
---@field state table Goes into storage.
---@field parameters table Whatever was passed to :open()

---@enum fa.ui.TabEventResponseKind
mod.EVENT_RESPONSES = {
-- Return this special value to close a tab list in response to finding e.g.
-- an invalid entity. `on_tab_list_closed` will stil be called. State for
-- all tabs in this list is also dropped.
CLOSE_INVALID = {},

-- Return this value to ask the handler to clear this tab's state, resetting
-- it to the empty table.
CLEAR_STATE = {},
}

-- Can include other fields.
---@alias fa.ui.TabEventResponse { kind: fa.ui.TabEventResponseKind }

---@alias fa.ui.SimpleTabHandler fun(fa.ui.TabContext): fa.ui.TabEventResponse

---@class fa.ui.TabCallbacks
---@field title LocalisedString? If set, read out when the tab changes to this tab.
---@field on_tab_focused fa.ui.SimpleTabHandler?
---@field on_tab_unfocused fa.ui.SimpleTabHandler?
---@field on_tab_list_opened fa.ui.SimpleTabHandler?
---@field on_tab_list_closed fa.ui.SimpleTabHandler?
---@field on_up fa.ui.SimpleTabHandler?
---@field on_down fa.ui.SimpleTabHandler?
---@field on_left fa.ui.SimpleTabHandler?
---@field on_right fa.ui.SimpleTabHandler?

---@class fa.ui.TabDescriptor
---@field name string
---@field callbacks fa.ui.TabCallbacks

---@class fa.ui.TabListDeclaration
---@field menu_name string
---@field tabs fa.ui.TabDescriptor[]

---@class fa.ui.TabListStorageState
---@field active_tab number
---@field tab_states table<string, table>
---@field parameters any
---@field currently_open boolean

---@type table<number, table<string, fa.ui.TabListStorageState>>
local tablist_storage = StorageManager.declare_storage_module("tab_list", {})

---@class fa.ui.TabList
---@field descriptors table<string, fa.ui.TabDescriptor>
---@field menu_name string
---@field tab_order string[]
local TabList = {}
local TabList_meta = { __index = TabList }
mod.TabList = TabList

-- Returns whatever the callback returns, including special responses. Does
-- nothing if this tablist is not open, which can happen when calling a bunch of
-- events back to back if the tab list has to close in the middle of a sequence
-- of actions.
function TabList:_do_callback(pindex, target_tab_index, cb_name, ...)
local tl = tablist_storage[pindex][self.menu_name]
local tabname = self.tab_order[target_tab_index]
local tabstate = tl.tab_states[tabname]
assert(tabstate, "this gets set up on self:open()")
if not tabstate.currently_open then return end

local callbacks = self.descriptors[tabname].callbacks
local callback = callbacks[cb_name]

-- The tab might not want to do anything.
if not callback then return end

local player = game.get_player(pindex)
assert(player, "Somehow got input from a dead player")

---@type fa.ui.TabContext
local context = {
pindex = pindex,
player = player,
state = tabstate,
parameters = tl.parameters,
}

local result = callback(context, ...)
if result and type(result) == "table" and result.kind then
local k = result.kind
if k == mod.EVENT_RESPONSES.CLEAR_STATE then
tl.tab_states[tabname] = {}
elseif k == mod.EVENT_RESPONSES.CLOSE_INVALID then
tl.tab_states = {}
-- TODO: closing logic. We don't have opening logic yet.
end
end

return result
end

-- Returns a method which will call the event handler for the given name, so
-- that we may avoid rewriting the same body over and over.
local function build_simple_method(evt_name)
return function(self, pindex)
local tl = tablist_storage[pindex][self.menu_name]
self:_do_callback(pindex, tl.active_tab, evt_name)
end
end

---@type fun(self)
TabList.on_up = build_simple_method("on_up")
---@type fun(self)
TabList.on_down = build_simple_method("on_down")
---@type fun(self)
TabList.on_left = build_simple_method("on_left")
---@type fun(self)
TabList.on_right = build_simple_method("on_right")

---@param pindex number
---@param direction 1|-1
function TabList:_cycle(self, pindex, direction)
local tl = tablist_storage[pindex][self.menu_name]
local old_index = tl.active_tab

local new_index = Math2.mod1(tl.active_tab + direction, #self.tab_order)
local wrapped = new_index ~= tl.active_tab + direction

-- Need to play sounds here.

local tabname = self.tab_order[tl.active_tab]
local desc = self.tabs[tabname]
local title = desc.callbacks.title
if title then printout(title, pindex) end

-- Call the event on the old tab, then change to the new tab, then call the
-- focused event on the new tab. But only do these if the tab actually
-- changed, so that we don't duplicate work in the case of one tab.
if old_index ~= new_index then
self:_do_callback(pindex, old_index, "on_tab_unfocused")
self:_do_callback(pindex, new_index, "on_tab_focused")
end
tl.active_tab = new_index
end

function TabList:next_tab(self, pindex)
self:_cycle(pindex, 1)
end

function TabList:previous_tab(self, pindex)
self:_cycle(pindex, -1)
end

function TabList:open(pindex, parameters)
storage.players[pindex].menu_name = self.menu_name
local tabstate = tablist_storage[pindex][self.menu_name]

-- Tabs wishing to clear their state have the infrastructure to handle that
-- via responding in on_tab_list_opened/closed.
if not tabstate then
tabstate = {
parameters = parameters,
active_tab = 1,
tab_order = {},
tab_states = {},
currently_open = true,
}
tablist_storage[pindex][self.menu_name] = tabstate

for _, desc in pairs(self.descriptors) do
table.insert(tabstate.tab_order, desc.name)
tabstate.tab_states[desc.name] = {}
end
end

-- Parameters always gets updated.
tabstate.parameters = parameters
-- And it is now open.
tabstate.currently_open = true

-- Tell all the tabs that they have opened, but abort if any of them close.

for i = 1, #tabstate.tab_order do
self:_do_callback(pindex, i, "on_tab_list_opened")
end
end

function TabList:close(pindex)
storage.players[pindex].menu_name = nil
storage.players[pindex].in_menu = false

for i = 1, #self.tab_order do
self:_do_callback(pindex, i, "on_tab_list_closed")
end

tablist_storage[pindex][self.menu_name].currently_open = false
end

return mod

0 comments on commit 86d8f23

Please sign in to comment.