slabOS/lib/basalt/plugins/reactive.lua
2025-06-02 10:23:40 +03:00

203 lines
6.3 KiB
Lua

local XMLParser = require("xmlParser")
local Reactive = {}
Reactive.currentEffect = nil
Reactive.observable = function(initialValue)
local value = initialValue
local observerEffects = {}
local get = function()
if (Reactive.currentEffect ~= nil) then
table.insert(observerEffects, Reactive.currentEffect)
table.insert(Reactive.currentEffect.dependencies, observerEffects)
end
return value
end
local set = function(newValue)
value = newValue
local observerEffectsCopy = {}
for index, effect in ipairs(observerEffects) do
observerEffectsCopy[index] = effect
end
for _, effect in ipairs(observerEffectsCopy) do
effect.execute()
end
end
return get, set
end
Reactive.untracked = function(getter)
local parentEffect = Reactive.currentEffect
Reactive.currentEffect = nil
local value = getter()
Reactive.currentEffect = parentEffect
return value
end
Reactive.effect = function(effectFn)
local effect = {dependencies = {}}
local execute = function()
Reactive.clearEffectDependencies(effect)
local parentEffect = Reactive.currentEffect
Reactive.currentEffect = effect
effectFn()
Reactive.currentEffect = parentEffect
end
effect.execute = execute
effect.execute()
end
Reactive.derived = function(computeFn)
local getValue, setValue = Reactive.observable();
Reactive.effect(function()
setValue(computeFn())
end)
return getValue;
end
Reactive.clearEffectDependencies = function(effect)
for _, dependency in ipairs(effect.dependencies) do
for index, backlink in ipairs(dependency) do
if (backlink == effect) then
table.remove(dependency, index)
end
end
end
effect.dependencies = {};
end
local Layout = {
fromXML = function(text)
local nodes = XMLParser.parseText(text)
local script = nil
for index, node in ipairs(nodes) do
if (node.tag == "script") then
script = node.value
table.remove(nodes, index)
break
end
end
return {
nodes = nodes,
script = script
}
end
}
local executeScript = function(script, env)
return load(script, nil, "t", env)()
end
local registerFunctionEvent = function(object, event, script, env)
event(object, function(...)
local success, msg = pcall(load(script, nil, "t", env))
if not success then
error("XML Error: "..msg)
end
end)
end
return {
basalt = function(basalt)
local createObjectsFromXMLNode = function(node, env)
local layout = env[node.tag]
if (layout ~= nil) then
local props = {}
for prop, expression in pairs(node.attributes) do
props[prop] = load("return " .. expression, nil, "t", env)
end
return basalt.createObjectsFromLayout(layout, props)
end
local objectName = node.tag:gsub("^%l", string.upper)
local object = basalt:createObject(objectName, node.attributes["id"])
for attribute, expression in pairs(node.attributes) do
if (attribute:sub(1, 2) == "on") then
registerFunctionEvent(object, object[attribute], expression .. "()", env)
else
local update = function()
local value = load("return " .. expression, nil, "t", env)()
object:setProperty(attribute, value)
end
basalt.effect(update)
end
end
for _, child in ipairs(node.children) do
local childObjects = basalt.createObjectsFromXMLNode(child, env)
for _, childObject in ipairs(childObjects) do
object:addChild(childObject)
end
end
return {object}
end
local object = {
observable = Reactive.observable,
untracked = Reactive.untracked,
effect = Reactive.effect,
derived = Reactive.derived,
layout = function(path)
if (not fs.exists(path)) then
error("Can't open file " .. path)
end
local f = fs.open(path, "r")
local text = f.readAll()
f.close()
return Layout.fromXML(text)
end,
createObjectsFromLayout = function(layout, props)
local env = _ENV
env.props = {}
local updateFns = {}
for prop, getFn in pairs(props) do
updateFns[prop] = basalt.derived(function()
return getFn()
end)
end
setmetatable(env.props, {
__index = function(_, k)
return updateFns[k]()
end
})
if (layout.script ~= nil) then
executeScript(layout.script, env)
end
local objects = {}
for _, node in ipairs(layout.nodes) do
local _objects = createObjectsFromXMLNode(node, env)
for _, object in ipairs(_objects) do
table.insert(objects, object)
end
end
return objects
end
}
return object
end,
Container = function(base, basalt)
local object = {
loadLayout = function(self, path, props)
local wrappedProps = {}
if (props == nil) then
props = {}
end
for prop, value in pairs(props) do
wrappedProps[prop] = function()
return value
end
end
local layout = basalt.layout(path)
local objects = basalt.createObjectsFromLayout(layout, wrappedProps)
for _, object in ipairs(objects) do
self:addChild(object)
end
return self
end
}
return object
end
}