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

12 KiB

local deepCopy = require("libraries/utils").deepCopy local expect = require("libraries/expect") local errorManager = require("errorManager") local log = require("log")

--- @class PropertySystem local PropertySystem = {} PropertySystem.__index = PropertySystem

PropertySystem._properties = {} local blueprintTemplates = {}

PropertySystem._setterHooks = {}

function PropertySystem.addSetterHook(hook) table.insert(PropertySystem._setterHooks, hook) end

local function applyHooks(element, propertyName, value, config) for _, hook in ipairs(PropertySystem._setterHooks) do local newValue = hook(element, propertyName, value, config) if newValue ~= nil then value = newValue end end return value end

function PropertySystem.defineProperty(class, name, config) if not rawget(class, '_properties') then class._properties = {} end

class._properties[name] = {
    type = config.type,
    default = config.default,
    canTriggerRender = config.canTriggerRender,
    getter = config.getter,
    setter = config.setter,
}

local capitalizedName = name:sub(1,1):upper() .. name:sub(2)

class["get" .. capitalizedName] = function(self, ...)
    expect(1, self, "element")
    local value = self._values[name]
    if type(value) == "function" and config.type ~= "function" then
        value = value(self)
    end
    return config.getter and config.getter(self, value, ...) or value
end

class["set" .. capitalizedName] = function(self, value, ...)
    expect(1, self, "element")
    value = applyHooks(self, name, value, config)

    if type(value) ~= "function" then
        expect(2, value, config.type)
    end

    if config.setter then
        value = config.setter(self, value, ...)
    end

    self:_updateProperty(name, value)
    return self
end

end

function PropertySystem.combineProperties(class, name, ...) local properties = {...} for k,v in pairs(properties)do if not class._properties[v] then errorManager.error("Property not found: "..v) end end local capitalizedName = name:sub(1,1):upper() .. name:sub(2)

class["get" .. capitalizedName] = function(self, ...)
    expect(1, self, "element")
    local value = {}
    for _,v in pairs(properties)do
        value[v] = self.get(v)
    end
    return table.unpack(value)
end

class["set" .. capitalizedName] = function(self, ...)
    expect(1, self, "element")
    local values = {...}
    for i,v in pairs(properties)do
        self.set(v, values[i])
    end
    return self
end

end

--- Creates a blueprint of an element class with all its properties --- @param elementClass table The element class to create a blueprint from --- @return table blueprint A table containing all property definitions function PropertySystem.blueprint(elementClass, properties, basalt, parent) if not blueprintTemplates[elementClass] then local template = { basalt = basalt, __isBlueprint = true, _values = properties or {}, _events = {}, render = function() end, dispatchEvent = function() end, init = function() end, }

    template.loaded = function(self, callback)
        self.loadedCallback = callback
        return template
    end

    template.create = function(self)
        local element = elementClass.new()
        element:init({}, self.basalt)
        for name, value in pairs(self._values) do
            element._values[name] = value
        end
        for name, callbacks in pairs(self._events) do
            for _, callback in ipairs(callbacks) do
                element[name](element, callback)
            end
        end
        if(parent~=nil)then
            parent:addChild(element)
        end
        element:updateRender()
        self.loadedCallback(element)
        element:postInit()
        return element
    end

    local currentClass = elementClass
    while currentClass do
        if rawget(currentClass, '_properties') then
            for name, config in pairs(currentClass._properties) do
                if type(config.default) == "table" then
                    template._values[name] = deepCopy(config.default)
                else
                    template._values[name] = config.default
                end
            end
        end
        currentClass = getmetatable(currentClass) and rawget(getmetatable(currentClass), '__index')
    end

    blueprintTemplates[elementClass] = template
end

local blueprint = {
    _values = {},
    _events = {},
    loadedCallback = function() end,
}

blueprint.get = function(name)
    local value = blueprint._values[name]
    local config = elementClass._properties[name]
    if type(value) == "function" and config.type ~= "function" then
        value = value(blueprint)
    end
    return value
end
blueprint.set = function(name, value)
    blueprint._values[name] = value
    return blueprint
end

setmetatable(blueprint, {
    __index = function(self, k)
        if k:match("^on%u") then
            return function(_, callback)
                self._events[k] = self._events[k] or {}
                table.insert(self._events[k], callback)
                return self
            end
        end
        if k:match("^get%u") then
            local propName = k:sub(4,4):lower() .. k:sub(5)
            return function()
                return self._values[propName]
            end
        end
        if k:match("^set%u") then
            local propName = k:sub(4,4):lower() .. k:sub(5)
            return function(_, value)
                self._values[propName] = value
                return self
            end
        end
        return blueprintTemplates[elementClass][k]
    end
})

return blueprint

end

function PropertySystem.createFromBlueprint(elementClass, blueprint, basalt) local element = elementClass.new({}, basalt) for name, value in pairs(blueprint._values) do if type(value) == "table" then element._values[name] = deepCopy(value) else element._values[name] = value end end

return element

end

function PropertySystem:__init() self._values = {} self._observers = {}

self.set = function(name, value, ...)
    local oldValue = self._values[name]
    local config = self._properties[name]
    if(config~=nil)then
        if(config.setter) then
            value = config.setter(self, value, ...)
        end
        if config.canTriggerRender then
            self:updateRender()
        end
        self._values[name] = applyHooks(self, name, value, config)
        if oldValue ~= value and self._observers[name] then
            for _, callback in ipairs(self._observers[name]) do
                callback(self, value, oldValue)
            end
        end
    end
end

self.get = function(name, ...)
    local value = self._values[name]
    local config = self._properties[name]
    if(config==nil)then errorManager.error("Property not found: "..name) return end
    if type(value) == "function" and config.type ~= "function" then
        value = value(self)
    end
    return config.getter and config.getter(self, value, ...) or value
end

local properties = {}
local currentClass = getmetatable(self).__index

while currentClass do
    if rawget(currentClass, '_properties') then
        for name, config in pairs(currentClass._properties) do
            if not properties[name] then
                properties[name] = config
            end
        end
    end
    currentClass = getmetatable(currentClass) and rawget(getmetatable(currentClass), '__index')
end

self._properties = properties

local originalMT = getmetatable(self)
local originalIndex = originalMT.__index
setmetatable(self, {
    __index = function(t, k)
        local config = self._properties[k]
        if config then
            local value = self._values[k]
            if type(value) == "function" and config.type ~= "function" then
                value = value(self)
            end
            return value
        end
        if type(originalIndex) == "function" then
            return originalIndex(t, k)
        else
            return originalIndex[k]
        end
    end,
    __newindex = function(t, k, v)
        local config = self._properties[k]
        if config then
            if config.setter then
                v = config.setter(self, v)
            end
            v = applyHooks(self, k, v, config)
            self:_updateProperty(k, v)
        else
            rawset(t, k, v)
        end
    end,
    __tostring = function(self)
        return string.format("Object: %s (id: %s)", self._values.type, self.id)
    end
})

for name, config in pairs(properties) do
    if self._values[name] == nil then
        if type(config.default) == "table" then
            self._values[name] = deepCopy(config.default)
        else
            self._values[name] = config.default
        end
    end
end

return self

end

function PropertySystem:_updateProperty(name, value) local oldValue = self._values[name] if type(oldValue) == "function" then oldValue = oldValue(self) end

self._values[name] = value
local newValue = type(value) == "function" and value(self) or value

if oldValue ~= newValue then
    if self._properties[name].canTriggerRender then
        self:updateRender()
    end
    if self._observers[name] then
        for _, callback in ipairs(self._observers[name]) do
            callback(self, newValue, oldValue)
        end
    end
end

end

function PropertySystem:observe(name, callback) self._observers[name] = self._observers[name] or {} table.insert(self._observers[name], callback) return self end

function PropertySystem:removeObserver(name, callback) if self._observers[name] then for i, cb in ipairs(self._observers[name]) do if cb == callback then table.remove(self._observers[name], i) if #self._observers[name] == 0 then self._observers[name] = nil end break end end end return self end

function PropertySystem:removeAllObservers(name) if name then self._observers[name] = nil else self._observers = {} end return self end

function PropertySystem:instanceProperty(name, config) PropertySystem.defineProperty(self, name, config) self._values[name] = config.default return self end

function PropertySystem:removeProperty(name) self._values[name] = nil self._properties[name] = nil self._observers[name] = nil

local capitalizedName = name:sub(1,1):upper() .. name:sub(2)
self["get" .. capitalizedName] = nil
self["set" .. capitalizedName] = nil
return self

end

function PropertySystem:getPropertyConfig(name) return self._properties[name] end

return PropertySystem