Files
2026-07-06 14:01:11 -04:00

263 lines
7.6 KiB
Lua

local Cryo = require(script.Parent.Parent.Cryo)
--[[
StateUtils provides utilities to read and write standard route data.
Routes have the following general structure:
{
index = <required integer: current active route in list>,
routes = [
{
routeName = <required string: navigation name for route>,
key = <unique string: identifier for this route>,
params = <optional dictionary: params for screen>,
action = <optional dictionary: sub-action to be run by child routers>,
},
...
]
}
This structure is independent of the notion of stack, tab, drawer, or any
other kind of navigation. It simply represents a list of pages and their
parameters. Different kinds of routers can treat the data in their own way.
]]
local StateUtils = {}
StateUtils.__index = StateUtils
-- Get the route matching the given key. Returns nil if no match is found.
function StateUtils.get(state, key)
assert(type(state) == "table", "state must be a table")
assert(type(key) == "string", "key must be a string")
for _, route in ipairs(state.routes) do
if route.key == key then
return route
end
end
return nil
end
-- Get the route at the given index. Returns nil if no match is found.
function StateUtils.getAtIndex(state, index)
assert(type(state) == "table", "state must be a table")
assert(type(index) == "number", "index must be a number")
assert(index >= 0, "index must be non-negative")
return state.routes[index]
end
-- Get the active route from state. Returns nil if no routes.
function StateUtils.getActiveRoute(state)
assert(type(state) == "table", "state must be a table")
local index = state.index
if index <= 0 then
return nil
end
return state.routes[index]
end
-- Get the index of the route matching the given key. Returns nil if no match is found.
function StateUtils.indexOf(state, key)
assert(type(state) == "table", "state must be a table")
assert(type(key) == "string", "key must be a string")
for index, route in ipairs(state.routes) do
if route.key == key then
return index
end
end
return nil
end
-- Returns true if a route exists matching the given key, false otherwise.
function StateUtils.has(state, key)
assert(type(state) == "table", "state must be a table")
assert(type(key) == "string", "key must be a string")
for _, route in ipairs(state.routes) do
if route.key == key then
return true
end
end
return false
end
-- Push a new route into the navigation state. Makes the pushed route active.
function StateUtils.push(state, route)
assert(type(state) == "table", "state must be a table")
assert(type(route) == "table", "route must be a table")
assert(StateUtils.indexOf(state, route.key) == nil,
string.format("route with key '%s' already exists", route.key))
local routes = Cryo.List.join(state.routes, { route })
return Cryo.Dictionary.join(state, {
index = #routes,
routes = routes,
})
end
-- Pop the top-most route from the navigation state (NOT the active route).
-- Makes the new top-most route active.
function StateUtils.pop(state)
assert(type(state) == "table", "state must be a table")
if #state.routes == 0 then
-- NOTE: Popping empty state is a no-op
return state
end
local routes = Cryo.List.removeIndex(state.routes, #state.routes)
return Cryo.Dictionary.join(state, {
index = #routes,
routes = routes,
})
end
-- Sets the active route to match the given index.
function StateUtils.jumpToIndex(state, index)
assert(type(state) == "table", "state must be a table")
assert(type(index) == "number", "index must be a number")
if index == state.index then
return state
end
assert(state.routes[index] ~= nil,
string.format("cannot jump to out-of-range index '%d'", index))
return Cryo.Dictionary.join(state, {
index = index,
})
end
-- Sets the active route to match the given key.
function StateUtils.jumpTo(state, key)
assert(type(state) == "table", "state must be a table")
assert(type(key) == "string", "key must be a string")
local index = StateUtils.indexOf(state, key)
return StateUtils.jumpToIndex(state, index)
end
-- Sets the active route to the previous route in the list.
function StateUtils.back(state)
assert(type(state) == "table", "state must be a table")
local index = state.index - 1
if not state.routes[index] then
return state
end
return StateUtils.jumpToIndex(state, index)
end
-- Sets the active route to the next route in the list.
function StateUtils.forward(state)
assert(type(state) == "table", "state must be a table")
local index = state.index + 1
if not state.routes[index] then
return state
end
return StateUtils.jumpToIndex(state, index)
end
-- Replace the route matching the given key. Sets the active route to the
-- newly replaced entry. Prunes the old entries that follow the replaced one.
function StateUtils.replaceAndPrune(state, key, route)
assert(type(state) == "table", "state must be a table")
assert(type(key) == "string", "key must be a string")
assert(type(route) == "table", "route must be a table")
local index = StateUtils.indexOf(state, key)
local replaced = StateUtils.replaceAtIndex(state, index, route)
return Cryo.Dictionary.join(replaced, {
routes = { unpack(replaced.routes, 1, index) }
})
end
-- Replace the route matching the given key without pruning the following routes.
-- The active route will be updated to match the newly replaced one unless
-- preserveIndex is true.
function StateUtils.replaceAt(state, key, route, preserveIndex)
assert(type(state) == "table", "state must be a table")
assert(type(key) == "string", "key must be a string")
assert(type(route) == "table", "route must be a table")
assert(preserveIndex == nil or type(preserveIndex) == "boolean",
"preserveIndex must be nil or a boolean")
local index = StateUtils.indexOf(state, key)
local nextIndex = preserveIndex and state.index or index
local nextState = StateUtils.replaceAtIndex(state, index, route)
nextState.index = nextIndex
return nextState
end
-- Replace the route at the given index. Updates the active route to point to
-- the replaced entry.
function StateUtils.replaceAtIndex(state, index, route)
assert(type(state) == "table", "state must be a table")
assert(type(index) == "number", "index must be a number")
assert(type(route) == "table", "route must be a table")
assert(state.routes[index] ~= nil,
string.format("index '%d' does not exist in route '%s'", index, route.key))
if state.routes[index] == route and index == state.index then
return state
end
local routes = Cryo.List.join(state.routes)
routes[index] = route
return Cryo.Dictionary.join(state, {
index = index,
routes = routes,
})
end
-- Wipe away the existing routes and replace them with new routes.
-- Sets the active route to the provided index (if provided), otherwise
-- sets the active route to the last one in the list.
function StateUtils.reset(state, routes, index)
assert(type(state) == "table", "state must be a table")
assert(type(routes) == "table" and #routes > 0,
"routes must be a list with at least one element")
assert(index == nil or type(index) == "number",
"index must be a number or nil")
local nextIndex = not index and #routes or index
-- Bail out without replacing IFF index and routes all match
if #state.routes == #routes and state.index == nextIndex then
local routesAreEqual = true
for i = 1, #routes, 1 do
if state.routes[i] ~= routes[i] then
routesAreEqual = false
break
end
end
if routesAreEqual then
return state
end
end
assert(routes[nextIndex] ~= nil,
string.format("cannot reset index '%d' that does not exist", nextIndex))
return Cryo.Dictionary.join(state, {
index = nextIndex,
routes = routes,
})
end
return StateUtils