Changed state plugin
- Moved state-storing to baseframe - Fixed :computed - Fixed a bug with storing states to same file - Added :bind to bind properties with states
This commit is contained in:
@@ -1,31 +1,23 @@
|
|||||||
local PropertySystem = require("propertySystem")
|
local PropertySystem = require("propertySystem")
|
||||||
local errorManager = require("errorManager")
|
local errorManager = require("errorManager")
|
||||||
|
|
||||||
--- This is the state plugin. It provides a state management system for UI elements with support for
|
---@class BaseFrame : Container
|
||||||
--- persistent states, computed states, and state sharing between elements.
|
local BaseFrame = {}
|
||||||
---@class BaseElement
|
|
||||||
local BaseElement = {}
|
|
||||||
|
|
||||||
---@private
|
function BaseFrame.setup(element)
|
||||||
function BaseElement.setup(element)
|
|
||||||
element.defineProperty(element, "states", {default = {}, type = "table"})
|
element.defineProperty(element, "states", {default = {}, type = "table"})
|
||||||
element.defineProperty(element, "computedStates", {default = {}, type = "table"})
|
element.defineProperty(element, "stateObserver", {default = {}, type = "table"})
|
||||||
element.defineProperty(element, "stateUpdate", {
|
|
||||||
default = {key = "", value = nil, oldValue = nil},
|
|
||||||
type = "table"
|
|
||||||
})
|
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Initializes a new state for this element
|
--- Initializes a new state for this element
|
||||||
--- @shortDescription Initializes a new state
|
--- @shortDescription Initializes a new state
|
||||||
--- @param self BaseElement The element to initialize state for
|
--- @param self BaseFrame The element to initialize state for
|
||||||
--- @param name string The name of the state
|
--- @param name string The name of the state
|
||||||
--- @param default any The default value of the state
|
--- @param default any The default value of the state
|
||||||
--- @param canTriggerRender? boolean Whether state changes trigger a render
|
|
||||||
--- @param persist? boolean Whether to persist the state to disk
|
--- @param persist? boolean Whether to persist the state to disk
|
||||||
--- @param path? string Custom file path for persistence
|
--- @param path? string Custom file path for persistence
|
||||||
--- @return BaseElement self The element instance
|
--- @return BaseFrame self The element instance
|
||||||
function BaseElement:initializeState(name, default, canTriggerRender, persist, path)
|
function BaseFrame:initializeState(name, default, persist, path)
|
||||||
local states = self.get("states")
|
local states = self.get("states")
|
||||||
|
|
||||||
if states[name] then
|
if states[name] then
|
||||||
@@ -33,34 +25,29 @@ function BaseElement:initializeState(name, default, canTriggerRender, persist, p
|
|||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
if persist then
|
local file = path or "states/" .. self.get("name") .. ".state"
|
||||||
local file = path or ("states/" .. self.get("name") .. "_" .. name .. ".state")
|
local persistedData = {}
|
||||||
|
|
||||||
if fs.exists(file) then
|
if persist and fs.exists(file) then
|
||||||
local f = fs.open(file, "r")
|
local f = fs.open(file, "r")
|
||||||
states[name] = {
|
persistedData = textutils.unserialize(f.readAll()) or {}
|
||||||
value = textutils.unserialize(f.readAll()),
|
|
||||||
persist = true,
|
|
||||||
file = file
|
|
||||||
}
|
|
||||||
f.close()
|
f.close()
|
||||||
else
|
|
||||||
states[name] = {
|
|
||||||
value = default,
|
|
||||||
persist = true,
|
|
||||||
file = file,
|
|
||||||
canTriggerRender = canTriggerRender
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
else
|
|
||||||
states[name] = {
|
states[name] = {
|
||||||
value = default,
|
value = persist and persistedData[name] or default,
|
||||||
canTriggerRender = canTriggerRender
|
persist = persist,
|
||||||
}
|
}
|
||||||
end
|
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
--- This is the state plugin. It provides a state management system for UI elements with support for
|
||||||
|
--- persistent states, computed states, and state sharing between elements.
|
||||||
|
---@class BaseElement
|
||||||
|
local BaseElement = {}
|
||||||
|
|
||||||
--- Sets the value of a state
|
--- Sets the value of a state
|
||||||
--- @shortDescription Sets a state value
|
--- @shortDescription Sets a state value
|
||||||
--- @param self BaseElement The element to set state for
|
--- @param self BaseElement The element to set state for
|
||||||
@@ -68,33 +55,56 @@ end
|
|||||||
--- @param value any The new value for the state
|
--- @param value any The new value for the state
|
||||||
--- @return BaseElement self The element instance
|
--- @return BaseElement self The element instance
|
||||||
function BaseElement:setState(name, value)
|
function BaseElement:setState(name, value)
|
||||||
local states = self.get("states")
|
local main = self:getBaseFrame()
|
||||||
|
local states = main.get("states")
|
||||||
|
local observers = main.get("stateObserver")
|
||||||
if not states[name] then
|
if not states[name] then
|
||||||
error("State '"..name.."' not initialized")
|
errorManager.error("State '"..name.."' not initialized")
|
||||||
end
|
end
|
||||||
|
|
||||||
local oldValue = states[name].value
|
|
||||||
states[name].value = value
|
|
||||||
|
|
||||||
if states[name].persist then
|
if states[name].persist then
|
||||||
local dir = fs.getDir(states[name].file)
|
local file = "states/" .. main.get("name") .. ".state"
|
||||||
if not fs.exists(dir) then
|
local persistedData = {}
|
||||||
fs.makeDir(dir)
|
|
||||||
end
|
if fs.exists(file) then
|
||||||
local f = fs.open(states[name].file, "w")
|
local f = fs.open(file, "r")
|
||||||
f.write(textutils.serialize(value))
|
persistedData = textutils.unserialize(f.readAll()) or {}
|
||||||
f.close()
|
f.close()
|
||||||
end
|
end
|
||||||
|
|
||||||
if states[name].canTriggerRender then
|
persistedData[name] = value
|
||||||
self:updateRender()
|
|
||||||
|
local dir = fs.getDir(file)
|
||||||
|
if not fs.exists(dir) then
|
||||||
|
fs.makeDir(dir)
|
||||||
|
end
|
||||||
|
|
||||||
|
local f = fs.open(file, "w")
|
||||||
|
f.write(textutils.serialize(persistedData))
|
||||||
|
f.close()
|
||||||
|
end
|
||||||
|
|
||||||
|
states[name].value = value
|
||||||
|
|
||||||
|
-- Trigger observers
|
||||||
|
if observers[name] then
|
||||||
|
for _, callback in ipairs(observers[name]) do
|
||||||
|
callback(self, name, value, states[name].value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Recompute all computed states
|
||||||
|
for stateName, state in pairs(states) do
|
||||||
|
if state.computed then
|
||||||
|
state.value = state.computeFn(self)
|
||||||
|
if observers[stateName] then
|
||||||
|
for _, callback in ipairs(observers[stateName]) do
|
||||||
|
callback(self, state.value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
self.set("stateUpdate", {
|
|
||||||
key = name,
|
|
||||||
value = value,
|
|
||||||
oldValue = oldValue
|
|
||||||
})
|
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -104,55 +114,19 @@ end
|
|||||||
--- @param name string The name of the state
|
--- @param name string The name of the state
|
||||||
--- @return any value The current state value
|
--- @return any value The current state value
|
||||||
function BaseElement:getState(name)
|
function BaseElement:getState(name)
|
||||||
local states = self.get("states")
|
local main = self:getBaseFrame()
|
||||||
|
local states = main.get("states")
|
||||||
|
|
||||||
if not states[name] then
|
if not states[name] then
|
||||||
errorManager.error("State '"..name.."' not initialized")
|
errorManager.error("State '"..name.."' not initialized")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if states[name].computed then
|
||||||
|
return states[name].value(self)
|
||||||
|
end
|
||||||
return states[name].value
|
return states[name].value
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Creates a computed state that derives its value from other states
|
|
||||||
--- @shortDescription Creates a computed state
|
|
||||||
--- @param self BaseElement The element to create computed state for
|
|
||||||
--- @param key string The name of the computed state
|
|
||||||
--- @param computeFn function Function that computes the state value
|
|
||||||
--- @return BaseElement self The element instance
|
|
||||||
function BaseElement:computed(key, computeFn)
|
|
||||||
local computed = self.get("computedStates")
|
|
||||||
computed[key] = setmetatable({}, {
|
|
||||||
__call = function()
|
|
||||||
return computeFn(self)
|
|
||||||
end
|
|
||||||
})
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Shares a state with other elements, keeping them in sync
|
|
||||||
--- @shortDescription Shares state between elements
|
|
||||||
--- @param self BaseElement The source element
|
|
||||||
--- @param stateKey string The state to share
|
|
||||||
--- @vararg BaseElement The target elements to share with
|
|
||||||
--- @return BaseElement self The source element
|
|
||||||
function BaseElement:shareState(stateKey, ...)
|
|
||||||
local value = self:getState(stateKey)
|
|
||||||
|
|
||||||
for _, element in ipairs({...}) do
|
|
||||||
if element.get("states")[stateKey] then
|
|
||||||
errorManager.error("Cannot share state '" .. stateKey .. "': Target element already has this state")
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
element:initializeState(stateKey, value)
|
|
||||||
|
|
||||||
self:observe("stateUpdate", function(self, update)
|
|
||||||
if update.key == stateKey then
|
|
||||||
element:setState(stateKey, update.value)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Registers a callback for state changes
|
--- Registers a callback for state changes
|
||||||
--- @shortDescription Watches for state changes
|
--- @shortDescription Watches for state changes
|
||||||
--- @param self BaseElement The element to watch
|
--- @param self BaseElement The element to watch
|
||||||
@@ -160,19 +134,93 @@ end
|
|||||||
--- @param callback function Called with (element, newValue, oldValue)
|
--- @param callback function Called with (element, newValue, oldValue)
|
||||||
--- @return BaseElement self The element instance
|
--- @return BaseElement self The element instance
|
||||||
function BaseElement:onStateChange(stateName, callback)
|
function BaseElement:onStateChange(stateName, callback)
|
||||||
if not self.get("states")[stateName] then
|
local main = self:getBaseFrame()
|
||||||
|
if not main.get("states")[stateName] then
|
||||||
errorManager.error("Cannot observe state '" .. stateName .. "': State not initialized")
|
errorManager.error("Cannot observe state '" .. stateName .. "': State not initialized")
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
local observers = main.get("stateObserver")
|
||||||
|
|
||||||
self:observe("stateUpdate", function(self, update)
|
if not observers[stateName] then
|
||||||
if update.key == stateName then
|
observers[stateName] = {}
|
||||||
callback(self, update.value, update.oldValue)
|
|
||||||
end
|
end
|
||||||
|
table.insert(observers[stateName], callback)
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Removes a state change observer
|
||||||
|
--- @shortDescription Removes a state change observer
|
||||||
|
--- @param self BaseElement The element to remove observer from
|
||||||
|
--- @param stateName string The state to remove observer from
|
||||||
|
--- @param callback function The callback function to remove
|
||||||
|
--- @return BaseElement self The element instance
|
||||||
|
function BaseElement:removeStateChange(stateName, callback)
|
||||||
|
local main = self:getBaseFrame()
|
||||||
|
local observers = main.get("stateObserver")
|
||||||
|
|
||||||
|
if observers[stateName] then
|
||||||
|
for i, observer in ipairs(observers[stateName]) do
|
||||||
|
if observer == callback then
|
||||||
|
table.remove(observers[stateName], i)
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function BaseElement:computed(name, func)
|
||||||
|
local main = self:getBaseFrame()
|
||||||
|
local states = main.get("states")
|
||||||
|
|
||||||
|
if states[name] then
|
||||||
|
errorManager.error("Computed state '" .. name .. "' already exists")
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
states[name] = {
|
||||||
|
computeFn = func,
|
||||||
|
value = func(self),
|
||||||
|
computed = true,
|
||||||
|
}
|
||||||
|
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Binds a property to a state
|
||||||
|
--- @param self BaseElement The element to bind
|
||||||
|
--- @param propertyName string The property to bind
|
||||||
|
--- @param stateName string The state to bind to (optional, uses propertyName if not provided)
|
||||||
|
--- @return BaseElement self The element instance
|
||||||
|
function BaseElement:bind(propertyName, stateName)
|
||||||
|
stateName = stateName or propertyName
|
||||||
|
local main = self:getBaseFrame()
|
||||||
|
local internalCall = false
|
||||||
|
|
||||||
|
if self.get(propertyName) ~= nil then
|
||||||
|
self.set(propertyName, main:getState(stateName))
|
||||||
|
end
|
||||||
|
|
||||||
|
self:onChange(propertyName, function(self, value)
|
||||||
|
if internalCall then return end
|
||||||
|
internalCall = true
|
||||||
|
self:setState(stateName, value)
|
||||||
|
internalCall = false
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
self:onStateChange(stateName, function(self, value)
|
||||||
|
if internalCall then return end
|
||||||
|
internalCall = true
|
||||||
|
if self.get(propertyName) ~= nil then
|
||||||
|
self.set(propertyName, value)
|
||||||
|
end
|
||||||
|
internalCall = false
|
||||||
|
end)
|
||||||
|
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
return {
|
return {
|
||||||
BaseElement = BaseElement
|
BaseElement = BaseElement,
|
||||||
|
BaseFrame = BaseFrame
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user