Files
Basalt2/docs/references/plugins/reactive.md
2025-02-16 14:12:49 +00:00

215 lines
6.6 KiB
Markdown

local errorManager = require("errorManager")
local PropertySystem = require("propertySystem")
local log = require("log")
local protectedNames = {
colors = true,
math = true,
clamp = true,
round = true
}
local mathEnv = {
clamp = function(val, min, max)
return math.min(math.max(val, min), max)
end,
round = function(val)
return math.floor(val + 0.5)
end
}
local function parseExpression(expr, element, propName)
expr = expr:gsub("^{(.+)}$", "%1")
expr = expr:gsub("([%w_]+)%$([%w_]+)", function(obj, prop)
if obj == "self" then
return string.format('__getState("%s")', prop)
elseif obj == "parent" then
return string.format('__getParentState("%s")', prop)
else
return string.format('__getElementState("%s", "%s")', obj, prop)
end
end)
expr = expr:gsub("([%w_]+)%.([%w_]+)", function(obj, prop)
if protectedNames[obj] then
return obj.."."..prop
end
return string.format('__getProperty("%s", "%s")', obj, prop)
end)
local env = setmetatable({
colors = colors,
math = math,
tostring = tostring,
tonumber = tonumber,
__getState = function(prop)
return element:getState(prop)
end,
__getParentState = function(prop)
return element.parent:getState(prop)
end,
__getElementState = function(objName, prop)
local target = element:getBaseFrame():getChild(objName)
if not target then
errorManager.header = "Reactive evaluation error"
errorManager.error("Could not find element: " .. objName)
return nil
end
return target:getState(prop).value
end,
__getProperty = function(objName, propName)
if objName == "self" then
return element.get(propName)
elseif objName == "parent" then
return element.parent.get(propName)
else
local target = element:getBaseFrame():getChild(objName)
if not target then
errorManager.header = "Reactive evaluation error"
errorManager.error("Could not find element: " .. objName)
return nil
end
return target.get(propName)
end
end
}, { __index = mathEnv })
if(element._properties[propName].type == "string")then
expr = "tostring(" .. expr .. ")"
elseif(element._properties[propName].type == "number")then
expr = "tonumber(" .. expr .. ")"
end
local func, err = load("return "..expr, "reactive", "t", env)
if not func then
errorManager.header = "Reactive evaluation error"
errorManager.error("Invalid expression: " .. err)
return function() return nil end
end
return func
end
local function validateReferences(expr, element)
for ref in expr:gmatch("([%w_]+)%.") do
if not protectedNames[ref] then
if ref == "self" then
elseif ref == "parent" then
if not element.parent then
errorManager.header = "Reactive evaluation error"
errorManager.error("No parent element available")
return false
end
else
local target = element:getBaseFrame():getChild(ref)
if not target then
errorManager.header = "Reactive evaluation error"
errorManager.error("Referenced element not found: " .. ref)
return false
end
end
end
end
return true
end
local functionCache = setmetatable({}, {__mode = "k"})
local observerCache = setmetatable({}, {
__mode = "k",
__index = function(t, k)
t[k] = {}
return t[k]
end
})
local function setupObservers(element, expr, propertyName)
if observerCache[element][propertyName] then
for _, observer in ipairs(observerCache[element][propertyName]) do
observer.target:removeObserver(observer.property, observer.callback)
end
end
local observers = {}
for ref, prop in expr:gmatch("([%w_]+)%.([%w_]+)") do
if not protectedNames[ref] then
local target
if ref == "self" then
target = element
elseif ref == "parent" then
target = element.parent
else
target = element:getBaseFrame():getChild(ref)
end
if target then
local observer = {
target = target,
property = prop,
callback = function()
element:updateRender()
end
}
target:observe(prop, observer.callback)
table.insert(observers, observer)
end
end
end
observerCache[element][propertyName] = observers
end
PropertySystem.addSetterHook(function(element, propertyName, value, config)
if type(value) == "string" and value:match("^{.+}$") then
local expr = value:gsub("^{(.+)}$", "%1")
if not validateReferences(expr, element) then
return config.default
end
setupObservers(element, expr, propertyName)
if not functionCache[element] then
functionCache[element] = {}
end
if not functionCache[element][value] then
local parsedFunc = parseExpression(value, element, propertyName)
functionCache[element][value] = parsedFunc
end
return function(self)
local success, result = pcall(functionCache[element][value])
if not success then
errorManager.header = "Reactive evaluation error"
if type(result) == "string" then
errorManager.error("Error evaluating expression: " .. result)
else
errorManager.error("Error evaluating expression")
end
return config.default
end
return result
end
end
end)
local BaseElement = {}
BaseElement.hooks = {
destroy = function(self)
if observerCache[self] then
for propName, observers in pairs(observerCache[self]) do
for _, observer in ipairs(observers) do
observer.target:removeObserver(observer.property, observer.callback)
end
end
observerCache[self] = nil
end
end
}
return {
BaseElement = BaseElement
}