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

6.6 KiB

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 }