This commit is contained in:
NoryiE
2025-02-16 19:31:38 +00:00
parent dd6825f3b6
commit 056897dd1b
36 changed files with 1085 additions and 5065 deletions

View File

@@ -1,328 +1,34 @@
local Animation = {}
Animation.__index = Animation
## Animation.new()
local registeredAnimations = {}
## Animation.registerAnimation()
function Animation.registerAnimation(name, handlers)
registeredAnimations[name] = handlers
## Animation.registerEasing()
Animation[name] = function(self, ...)
local args = {...}
local easing = "linear"
if(type(args[#args]) == "string") then
easing = table.remove(args, #args)
end
local duration = table.remove(args, #args)
return self:addAnimation(name, args, duration, easing)
end
end
## Animation:addAnimation()
local easings = {
linear = function(progress)
return progress
end,
## Animation:event()
easeInQuad = function(progress)
return progress * progress
end,
## Animation:onComplete()
easeOutQuad = function(progress)
return 1 - (1 - progress) * (1 - progress)
end,
## Animation:onStart()
easeInOutQuad = function(progress)
if progress < 0.5 then
return 2 * progress * progress
end
return 1 - (-2 * progress + 2)^2 / 2
end
}
## Animation:onUpdate()
function Animation.registerEasing(name, func)
easings[name] = func
end
## Animation:sequence()
local AnimationInstance = {}
AnimationInstance.__index = AnimationInstance
## Animation:start()
function AnimationInstance.new(element, animType, args, duration, easing)
local self = setmetatable({}, AnimationInstance)
self.element = element
self.type = animType
self.args = args
self.duration = duration
self.startTime = 0
self.isPaused = false
self.handlers = registeredAnimations[animType]
self.easing = easing
return self
end
## AnimationInstance.new()
function AnimationInstance:start()
self.startTime = os.epoch("local") / 1000
if self.handlers.start then
self.handlers.start(self)
end
end
## AnimationInstance:complete()
function AnimationInstance:update(elapsed)
local rawProgress = math.min(1, elapsed / self.duration)
local progress = easings[self.easing](rawProgress)
return self.handlers.update(self, progress)
end
## AnimationInstance:start()
function AnimationInstance:complete()
if self.handlers.complete then
self.handlers.complete(self)
end
end
## AnimationInstance:update()
function Animation.new(element)
local self = {}
self.element = element
self.sequences = {{}}
self.sequenceCallbacks = {}
self.currentSequence = 1
self.timer = nil
setmetatable(self, Animation)
return self
end
## VisualElement.hooks.dispatchEvent()
function Animation:sequence()
table.insert(self.sequences, {})
self.currentSequence = #self.sequences
self.sequenceCallbacks[self.currentSequence] = {
start = nil,
update = nil,
complete = nil
}
return self
end
## VisualElement.setup()
function Animation:onStart(callback)
if not self.sequenceCallbacks[self.currentSequence] then
self.sequenceCallbacks[self.currentSequence] = {}
end
self.sequenceCallbacks[self.currentSequence].start = callback
return self
end
## VisualElement:animate()
function Animation:onUpdate(callback)
if not self.sequenceCallbacks[self.currentSequence] then
self.sequenceCallbacks[self.currentSequence] = {}
end
self.sequenceCallbacks[self.currentSequence].update = callback
return self
end
function Animation:onComplete(callback)
if not self.sequenceCallbacks[self.currentSequence] then
self.sequenceCallbacks[self.currentSequence] = {}
end
self.sequenceCallbacks[self.currentSequence].complete = callback
return self
end
function Animation:addAnimation(type, args, duration, easing)
local anim = AnimationInstance.new(self.element, type, args, duration, easing)
table.insert(self.sequences[self.currentSequence], anim)
return self
end
function Animation:start()
self.currentSequence = 1
if(self.sequenceCallbacks[self.currentSequence])then
if(self.sequenceCallbacks[self.currentSequence].start) then
self.sequenceCallbacks[self.currentSequence].start(self.element)
end
end
if #self.sequences[self.currentSequence] > 0 then
self.timer = os.startTimer(0.05)
for _, anim in ipairs(self.sequences[self.currentSequence]) do
anim:start()
end
end
return self
end
function Animation:event(event, timerId)
if event == "timer" and timerId == self.timer then
local currentTime = os.epoch("local") / 1000
local sequenceFinished = true
local remaining = {}
local callbacks = self.sequenceCallbacks[self.currentSequence]
for _, anim in ipairs(self.sequences[self.currentSequence]) do
local elapsed = currentTime - anim.startTime
local progress = elapsed / anim.duration
local finished = anim:update(elapsed)
if callbacks and callbacks.update then
callbacks.update(self.element, progress)
end
if not finished then
table.insert(remaining, anim)
sequenceFinished = false
else
anim:complete()
end
end
if sequenceFinished then
if callbacks and callbacks.complete then
callbacks.complete(self.element)
end
if self.currentSequence < #self.sequences then
self.currentSequence = self.currentSequence + 1
remaining = {}
local nextCallbacks = self.sequenceCallbacks[self.currentSequence]
if nextCallbacks and nextCallbacks.start then
nextCallbacks.start(self.element)
end
for _, anim in ipairs(self.sequences[self.currentSequence]) do
anim:start()
table.insert(remaining, anim)
end
end
end
if #remaining > 0 then
self.timer = os.startTimer(0.05)
end
end
end
Animation.registerAnimation("move", {
start = function(anim)
anim.startX = anim.element.get("x")
anim.startY = anim.element.get("y")
end,
update = function(anim, progress)
local x = anim.startX + (anim.args[1] - anim.startX) * progress
local y = anim.startY + (anim.args[2] - anim.startY) * progress
anim.element.set("x", math.floor(x))
anim.element.set("y", math.floor(y))
return progress >= 1
end,
complete = function(anim)
anim.element.set("x", anim.args[1])
anim.element.set("y", anim.args[2])
end
})
Animation.registerAnimation("morphText", {
start = function(anim)
local startText = anim.element.get(anim.args[1])
local targetText = anim.args[2]
local maxLength = math.max(#startText, #targetText)
local startSpace = string.rep(" ", math.floor(maxLength - #startText)/2)
anim.startText = startSpace .. startText .. startSpace
anim.targetText = targetText .. string.rep(" ", maxLength - #targetText)
anim.length = maxLength
end,
update = function(anim, progress)
local currentText = ""
for i = 1, anim.length do
local startChar = anim.startText:sub(i,i)
local targetChar = anim.targetText:sub(i,i)
if progress < 0.5 then
currentText = currentText .. (math.random() > progress*2 and startChar or " ")
else
currentText = currentText .. (math.random() > (progress-0.5)*2 and " " or targetChar)
end
end
anim.element.set(anim.args[1], currentText)
return progress >= 1
end,
complete = function(anim)
anim.element.set(anim.args[1], anim.targetText:gsub("%s+$", "")) -- Entferne trailing spaces
end
})
Animation.registerAnimation("typewrite", {
start = function(anim)
anim.targetText = anim.args[2]
anim.element.set(anim.args[1], "")
end,
update = function(anim, progress)
local length = math.floor(#anim.targetText * progress)
anim.element.set(anim.args[1], anim.targetText:sub(1, length))
return progress >= 1
end
})
Animation.registerAnimation("fadeText", {
start = function(anim)
anim.chars = {}
for i=1, #anim.args[2] do
anim.chars[i] = {char = anim.args[2]:sub(i,i), visible = false}
end
end,
update = function(anim, progress)
local text = ""
for i, charData in ipairs(anim.chars) do
if math.random() < progress then
charData.visible = true
end
text = text .. (charData.visible and charData.char or " ")
end
anim.element.set(anim.args[1], text)
return progress >= 1
end
})
Animation.registerAnimation("scrollText", {
start = function(anim)
anim.width = anim.element.get("width")
anim.targetText = anim.args[2]
anim.element.set(anim.args[1], "")
end,
update = function(anim, progress)
local offset = math.floor(anim.width * (1-progress))
local spaces = string.rep(" ", offset)
anim.element.set(anim.args[1], spaces .. anim.targetText)
return progress >= 1
end
})
local VisualElement = {hooks={}}
function VisualElement.hooks.dispatchEvent(self, event, ...)
if event == "timer" then
local animation = self.get("animation")
if animation then
animation:event(event, ...)
end
end
end
function VisualElement.setup(element)
VisualElementBaseDispatchEvent = element.dispatchEvent
element.defineProperty(element, "animation", {default = nil, type = "table"})
element.listenTo(element, "timer")
end
function VisualElement:animate()
local animation = Animation.new(self)
self.set("animation", animation)
return animation
end
return {
VisualElement = VisualElement
}

View File

@@ -1,325 +1,18 @@
local log = require("log")
## BaseElement:benchmark()
local activeProfiles = setmetatable({}, {__mode = "k"})
## BaseElement:endProfile()
local function createProfile()
return {
methods = {},
}
end
## BaseElement:getBenchmarkStats()
local function wrapMethod(element, methodName)
local originalMethod = element[methodName]
## BaseElement:logBenchmark()
if not activeProfiles[element] then
activeProfiles[element] = createProfile()
end
if not activeProfiles[element].methods[methodName] then
activeProfiles[element].methods[methodName] = {
calls = 0,
totalTime = 0,
minTime = math.huge,
maxTime = 0,
lastTime = 0,
startTime = 0,
path = {},
methodName = methodName,
originalMethod = originalMethod
}
end
## BaseElement:startProfile()
element[methodName] = function(self, ...)
self:startProfile(methodName)
local result = originalMethod(self, ...)
self:endProfile(methodName)
return result
end
end
## BaseElement:stopBenchmark()
local BaseElement = {}
## Container:benchmarkContainer()
function BaseElement:startProfile(methodName)
local profile = activeProfiles[self]
if not profile then
profile = createProfile()
activeProfiles[self] = profile
end
## Container:logContainerBenchmarks()
if not profile.methods[methodName] then
profile.methods[methodName] = {
calls = 0,
totalTime = 0,
minTime = math.huge,
maxTime = 0,
lastTime = 0,
startTime = 0,
path = {},
methodName = methodName
}
end
## Container:stopContainerBenchmark()
local methodProfile = profile.methods[methodName]
methodProfile.startTime = os.clock() * 1000
methodProfile.path = {}
local current = self
while current do
table.insert(methodProfile.path, 1, current.get("name") or current.get("id"))
current = current.parent
end
return self
end
function BaseElement:endProfile(methodName)
local profile = activeProfiles[self]
if not profile or not profile.methods[methodName] then return self end
local methodProfile = profile.methods[methodName]
local endTime = os.clock() * 1000
local duration = endTime - methodProfile.startTime
methodProfile.calls = methodProfile.calls + 1
methodProfile.totalTime = methodProfile.totalTime + duration
methodProfile.minTime = math.min(methodProfile.minTime, duration)
methodProfile.maxTime = math.max(methodProfile.maxTime, duration)
methodProfile.lastTime = duration
return self
end
function BaseElement:benchmark(methodName)
if not self[methodName] then
log.error("Method " .. methodName .. " does not exist")
return self
end
activeProfiles[self] = createProfile()
activeProfiles[self].methodName = methodName
activeProfiles[self].isRunning = true
wrapMethod(self, methodName)
return self
end
function BaseElement:logBenchmark(methodName)
local profile = activeProfiles[self]
if not profile or not profile.methods[methodName] then return self end
local stats = profile.methods[methodName]
if stats then
local averageTime = stats.calls > 0 and (stats.totalTime / stats.calls) or 0
log.info(string.format(
"Benchmark results for %s.%s: " ..
"Path: %s " ..
"Calls: %d " ..
"Average time: %.2fms " ..
"Min time: %.2fms " ..
"Max time: %.2fms " ..
"Last time: %.2fms " ..
"Total time: %.2fms",
table.concat(stats.path, "."),
stats.methodName,
table.concat(stats.path, "/"),
stats.calls,
averageTime,
stats.minTime ~= math.huge and stats.minTime or 0,
stats.maxTime,
stats.lastTime,
stats.totalTime
))
end
return self
end
function BaseElement:stopBenchmark(methodName)
local profile = activeProfiles[self]
if not profile or not profile.methods[methodName] then return self end
local stats = profile.methods[methodName]
if stats and stats.originalMethod then
self[methodName] = stats.originalMethod
end
profile.methods[methodName] = nil
if not next(profile.methods) then
activeProfiles[self] = nil
end
return self
end
function BaseElement:getBenchmarkStats(methodName)
local profile = activeProfiles[self]
if not profile or not profile.methods[methodName] then return nil end
local stats = profile.methods[methodName]
return {
averageTime = stats.totalTime / stats.calls,
totalTime = stats.totalTime,
calls = stats.calls,
minTime = stats.minTime,
maxTime = stats.maxTime,
lastTime = stats.lastTime
}
end
local Container = {}
function Container:benchmarkContainer(methodName)
self:benchmark(methodName)
for _, child in pairs(self.get("children")) do
child:benchmark(methodName)
if child:isType("Container") then
child:benchmarkContainer(methodName)
end
end
return self
end
function Container:logContainerBenchmarks(methodName, depth)
depth = depth or 0
local indent = string.rep(" ", depth)
local childrenTotalTime = 0
local childrenStats = {}
for _, child in pairs(self.get("children")) do
local profile = activeProfiles[child]
if profile and profile.methods[methodName] then
local stats = profile.methods[methodName]
childrenTotalTime = childrenTotalTime + stats.totalTime
table.insert(childrenStats, {
element = child,
type = child.get("type"),
calls = stats.calls,
totalTime = stats.totalTime,
avgTime = stats.totalTime / stats.calls
})
end
end
local profile = activeProfiles[self]
if profile and profile.methods[methodName] then
local stats = profile.methods[methodName]
local selfTime = stats.totalTime - childrenTotalTime
local avgSelfTime = selfTime / stats.calls
log.info(string.format(
"%sBenchmark %s (%s): " ..
"%.2fms/call (Self: %.2fms/call) " ..
"[Total: %dms, Calls: %d]",
indent,
self.get("type"),
methodName,
stats.totalTime / stats.calls,
avgSelfTime,
stats.totalTime,
stats.calls
))
if #childrenStats > 0 then
for _, childStat in ipairs(childrenStats) do
if childStat.element:isType("Container") then
childStat.element:logContainerBenchmarks(methodName, depth + 1)
else
log.info(string.format("%s> %s: %.2fms/call [Total: %dms, Calls: %d]",
indent .. " ",
childStat.type,
childStat.avgTime,
childStat.totalTime,
childStat.calls
))
end
end
end
end
return self
end
function Container:stopContainerBenchmark(methodName)
for _, child in pairs(self.get("children")) do
if child:isType("Container") then
child:stopContainerBenchmark(methodName)
else
child:stopBenchmark(methodName)
end
end
self:stopBenchmark(methodName)
return self
end
local API = {
start = function(name, options)
options = options or {}
local profile = createProfile()
profile.name = name
profile.startTime = os.clock() * 1000
profile.custom = true
activeProfiles[name] = profile
end,
stop = function(name)
local profile = activeProfiles[name]
if not profile or not profile.custom then return end
local endTime = os.clock() * 1000
local duration = endTime - profile.startTime
profile.calls = profile.calls + 1
profile.totalTime = profile.totalTime + duration
profile.minTime = math.min(profile.minTime, duration)
profile.maxTime = math.max(profile.maxTime, duration)
profile.lastTime = duration
log.info(string.format(
"Custom Benchmark '%s': " ..
"Calls: %d " ..
"Average time: %.2fms " ..
"Min time: %.2fms " ..
"Max time: %.2fms " ..
"Last time: %.2fms " ..
"Total time: %.2fms",
name,
profile.calls,
profile.totalTime / profile.calls,
profile.minTime,
profile.maxTime,
profile.lastTime,
profile.totalTime
))
end,
getStats = function(name)
local profile = activeProfiles[name]
if not profile then return nil end
return {
averageTime = profile.totalTime / profile.calls,
totalTime = profile.totalTime,
calls = profile.calls,
minTime = profile.minTime,
maxTime = profile.maxTime,
lastTime = profile.lastTime
}
end,
clear = function(name)
activeProfiles[name] = nil
end,
clearAll = function()
for k,v in pairs(activeProfiles) do
if v.custom then
activeProfiles[k] = nil
end
end
end
}
return {
BaseElement = BaseElement,
Container = Container,
API = API
}

View File

@@ -1,181 +0,0 @@
local log = require("log")
local tHex = require("libraries/colorHex")
local maxLines = 10
local isVisible = false
local function createDebugger(element)
local elementInfo = {
renderCount = 0,
eventCount = {},
lastRender = os.epoch("utc"),
properties = {},
children = {}
}
return {
trackProperty = function(name, value)
elementInfo.properties[name] = value
end,
trackRender = function()
elementInfo.renderCount = elementInfo.renderCount + 1
elementInfo.lastRender = os.epoch("utc")
end,
trackEvent = function(event)
elementInfo.eventCount[event] = (elementInfo.eventCount[event] or 0) + 1
end,
dump = function()
return {
type = element.get("type"),
id = element.get("id"),
stats = elementInfo
}
end
}
end
local BaseElement = {
debug = function(self, level)
self._debugger = createDebugger(self)
self._debugLevel = level or DEBUG_LEVELS.INFO
return self
end,
dumpDebug = function(self)
if not self._debugger then return end
return self._debugger.dump()
end
}
local BaseFrame = {
showDebugLog = function(self)
if not self._debugFrame then
local width = self.get("width")
local height = self.get("height")
self._debugFrame = self:addFrame("basaltDebugLog")
:setWidth(width)
:setHeight(height)
:setZ(999)
:listenEvent("mouse_scroll", true)
self.basalt.LOGGER.debug("Created debug log frame " .. self._debugFrame.get("name"))
self._debugFrame:addButton("basaltDebugLogClose")
:setWidth(9)
:setHeight(1)
:setX(width - 8)
:setY(height)
:setText("Close")
:onMouseClick(function()
self:hideDebugLog()
end)
self._debugFrame._scrollOffset = 0
self._debugFrame._processedLogs = {}
local function wrapText(text, width)
local lines = {}
while #text > 0 do
local line = text:sub(1, width)
table.insert(lines, line)
text = text:sub(width + 1)
end
return lines
end
local function processLogs()
local processed = {}
local width = self._debugFrame.get("width")
for _, entry in ipairs(log._logs) do
local lines = wrapText(entry.message, width)
for _, line in ipairs(lines) do
table.insert(processed, {
text = line,
level = entry.level
})
end
end
return processed
end
local totalLines = #processLogs() - self.get("height")
self._scrollOffset = totalLines
local originalRender = self._debugFrame.render
self._debugFrame.render = function(frame)
originalRender(frame)
frame._processedLogs = processLogs()
local height = frame.get("height")-2
local totalLines = #frame._processedLogs
local maxScroll = math.max(0, totalLines - height)
frame._scrollOffset = math.min(frame._scrollOffset, maxScroll)
for i = 1, height-2 do
local logIndex = i + frame._scrollOffset
local entry = frame._processedLogs[logIndex]
if entry then
local color = entry.level == log.LEVEL.ERROR and colors.red
or entry.level == log.LEVEL.WARN and colors.yellow
or entry.level == log.LEVEL.DEBUG and colors.lightGray
or colors.white
frame:textFg(2, i, entry.text, color)
end
end
end
local baseDispatchEvent = self._debugFrame.dispatchEvent
self._debugFrame.dispatchEvent = function(self, event, direction, ...)
if(event == "mouse_scroll") then
self._scrollOffset = math.max(0, self._scrollOffset + direction)
self:updateRender()
return true
else
baseDispatchEvent(self, event, direction, ...)
end
end
end
self._debugFrame.set("visible", true)
return self
end,
hideDebugLog = function(self)
if self._debugFrame then
self._debugFrame.set("visible", false)
end
return self
end,
toggleDebugLog = function(self)
if self._debugFrame and self._debugFrame:isVisible() then
self:hideDebugLog()
else
self:showDebugLog()
end
return self
end
}
local Container = {
debugChildren = function(self, level)
self:debug(level)
for _, child in pairs(self.get("children")) do
if child.debug then
child:debug(level)
end
end
return self
end
}
return {
BaseElement = BaseElement,
Container = Container,
BaseFrame = BaseFrame,
}

View File

@@ -1,23 +1,10 @@
-- Will temporary exist so that we don't lose track of how the plugin system works
Will temporary exist so that we don't lose track of how the plugin system works
## VisualElement.hooks.init()
Hooks into existing methods (you can also use init.pre or init.post)
local VisualElement = {hooks={init={}}}
## VisualElement.setup()
Called on Class level to define properties and setup before instance is created
-- Called on Class level to define properties and setup before instance is created
function VisualElement.setup(element)
element.defineProperty(element, "testProp", {default = 5, type = "number"})
end
-- Hooks into existing methods (you can also use init.pre or init.post)
function VisualElement.hooks.init(self)
--self.basalt.LOGGER.debug("VisualElement initialized")
end
-- Adds a new method to the class
function VisualElement:testFunc()
--self.basalt.LOGGER.debug("Hello World", self.get("testProp"))
end
return {
VisualElement = VisualElement
}
## VisualElement:testFunc()
Adds a new method to the class

View File

@@ -1,214 +0,0 @@
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
}

View File

@@ -1,135 +1,14 @@
local PropertySystem = require("propertySystem")
local errorManager = require("errorManager")
local BaseElement = {}
## BaseElement.setup()
function BaseElement.setup(element)
element.defineProperty(element, "states", {default = {}, type = "table"})
element.defineProperty(element, "computedStates", {default = {}, type = "table"})
element.defineProperty(element, "stateUpdate", {
default = {key = "", value = nil, oldValue = nil},
type = "table"
})
end
## BaseElement:computed()
function BaseElement:initializeState(name, default, canTriggerRender, persist, path)
local states = self.get("states")
## BaseElement:getState()
if states[name] then
errorManager.error("State '" .. name .. "' already exists")
return self
end
## BaseElement:initializeState()
if persist then
local file = path or ("states/" .. self.get("name") .. "_" .. name .. ".state")
## BaseElement:onStateChange()
if fs.exists(file) then
local f = fs.open(file, "r")
states[name] = {
value = textutils.unserialize(f.readAll()),
persist = true,
file = file
}
f.close()
else
states[name] = {
value = default,
persist = true,
file = file,
canTriggerRender = canTriggerRender
}
end
else
states[name] = {
value = default,
canTriggerRender = canTriggerRender
}
end
return self
end
## BaseElement:setState()
function BaseElement:setState(name, value)
local states = self.get("states")
if not states[name] then
error("State '"..name.."' not initialized")
end
## BaseElement:shareState()
local oldValue = states[name].value
states[name].value = value
if states[name].persist then
local dir = fs.getDir(states[name].file)
if not fs.exists(dir) then
fs.makeDir(dir)
end
local f = fs.open(states[name].file, "w")
f.write(textutils.serialize(value))
f.close()
end
if states[name].canTriggerRender then
self:updateRender()
end
self.set("stateUpdate", {
key = name,
value = value,
oldValue = oldValue
})
return self
end
function BaseElement:getState(name)
local states = self.get("states")
if not states[name] then
errorManager.error("State '"..name.."' not initialized")
end
return states[name].value
end
function BaseElement:computed(key, computeFn)
local computed = self.get("computedStates")
computed[key] = setmetatable({}, {
__call = function()
return computeFn(self)
end
})
return self
end
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
function BaseElement:onStateChange(stateName, callback)
if not self.get("states")[stateName] then
errorManager.error("Cannot observe state '" .. stateName .. "': State not initialized")
return self
end
self:observe("stateUpdate", function(self, update)
if update.key == stateName then
callback(self, update.value, update.oldValue)
end
end)
return self
end
return {
BaseElement = BaseElement
}

View File

@@ -1,191 +1,4 @@
local defaultTheme = {
default = {
background = colors.lightGray,
foreground = colors.black,
},
BaseFrame = {
background = colors.white,
foreground = colors.black,
## BaseElement.____getElementPath()
Frame = {
background = colors.black,
names = {
basaltDebugLogClose = {
background = colors.blue,
foreground = colors.white
}
},
},
Button = {
background = "{self.clicked and colors.black or colors.cyan}",
foreground = "{self.clicked and colors.cyan or colors.black}",
},
## BaseElement:getTheme()
names = {
basaltDebugLog = {
background = colors.red,
foreground = colors.white
},
test = {
background = "{self.clicked and colors.black or colors.green}",
foreground = "{self.clicked and colors.green or colors.black}"
}
},
}
}
local themes = {
default = defaultTheme
}
local currentTheme = "default"
local BaseElement = {
hooks = {
postInit = {
pre = function(self)
self:applyTheme()
end}
}
}
function BaseElement.____getElementPath(self, types)
if types then
table.insert(types, 1, self._values.type)
else
types = {self._values.type}
end
local parent = self.parent
if parent then
return parent.____getElementPath(parent, types)
else
return types
end
end
local function lookUpTemplate(theme, path)
local current = theme
for i = 1, #path do
local found = false
local types = path[i]
for _, elementType in ipairs(types) do
if current[elementType] then
current = current[elementType]
found = true
break
end
end
if not found then
return nil
end
end
return current
end
local function getDefaultProperties(theme, elementType)
local result = {}
if theme.default then
for k,v in pairs(theme.default) do
if type(v) ~= "table" then
result[k] = v
end
end
if theme.default[elementType] then
for k,v in pairs(theme.default[elementType]) do
if type(v) ~= "table" then
result[k] = v
end
end
end
end
return result
end
local function applyNamedStyles(result, theme, elementType, elementName, themeTable)
if theme.default and theme.default.names and theme.default.names[elementName] then
for k,v in pairs(theme.default.names[elementName]) do
if type(v) ~= "table" then result[k] = v end
end
end
if theme.default and theme.default[elementType] and theme.default[elementType].names
and theme.default[elementType].names[elementName] then
for k,v in pairs(theme.default[elementType].names[elementName]) do
if type(v) ~= "table" then result[k] = v end
end
end
if themeTable and themeTable.names and themeTable.names[elementName] then
for k,v in pairs(themeTable.names[elementName]) do
if type(v) ~= "table" then result[k] = v end
end
end
end
local function collectThemeProps(theme, path, elementType, elementName)
local result = {}
local themeTable = lookUpTemplate(theme, path)
if themeTable then
for k,v in pairs(themeTable) do
if type(v) ~= "table" then
result[k] = v
end
end
end
if next(result) == nil then
result = getDefaultProperties(theme, elementType)
end
applyNamedStyles(result, theme, elementType, elementName, themeTable)
return result
end
function BaseElement:applyTheme()
local styles = self:getTheme()
if(styles ~= nil) then
for prop, value in pairs(styles) do
self.set(prop, value)
end
end
end
function BaseElement:getTheme()
local path = self:____getElementPath()
local elementType = self.get("type")
local elementName = self.get("name")
return collectThemeProps(themes[currentTheme], path, elementType, elementName)
end
local themeAPI = {
setTheme = function(newTheme)
defaultTheme = newTheme
end,
getTheme = function()
return defaultTheme
end,
loadTheme = function(path)
local file = fs.open(path, "r")
if file then
local content = file.readAll()
file.close()
defaultTheme = textutils.unserializeJSON(content)
end
end
}
local Theme = {
BaseElement = BaseElement,
API = themeAPI
}
return Theme

View File

@@ -1,184 +1,4 @@
local errorManager = require("errorManager")
## BaseElement:fromXML()
local function parseTag(str)
local tag = {
attributes = {}
}
tag.name = str:match("<(%w+)")
for k,v in str:gmatch('%s(%w+)="([^"]-)"') do
tag.attributes[k] = v
end
return tag
end
local function parseXML(self, xmlString)
local stack = {}
local root = {children = {}}
local current = root
local inCDATA = false
local cdataContent = ""
for line in xmlString:gmatch("[^\r\n]+") do
line = line:match("^%s*(.-)%s*$")
self.basalt.LOGGER.debug("Parsing line: " .. line)
if line:match("^<!%[CDATA%[") then
inCDATA = true
cdataContent = ""
elseif line:match("%]%]>$") and inCDATA then
inCDATA = false
current.content = cdataContent
elseif inCDATA then
cdataContent = cdataContent .. line .. "\n"
elseif line:match("^<[^/]") then
local tag = parseTag(line)
tag.children = {}
tag.content = ""
table.insert(current.children, tag)
if not line:match("/>$") then
table.insert(stack, current)
current = tag
end
elseif line:match("^</") then
current = table.remove(stack)
end
end
return root
end
local function evaluateExpression(expr, scope)
if not expr:match("^%${.*}$") then
return expr:gsub("%${(.-)}", function(e)
local env = setmetatable({}, {__index = function(_, k)
return scope and scope[k] or _ENV[k]
end})
local func, err = load("return " .. e, "expression", "t", env)
if not func then
errorManager.error("Failed to parse expression: " .. err)
end
return tostring(func())
end)
end
expr = expr:match("^%${(.*)}$")
local env = setmetatable({}, {__index = function(_, k)
return scope and scope[k] or _ENV[k]
end})
local func, err = load("return " .. expr, "expression", "t", env)
if not func then
errorManager.error("Failed to parse expression: " .. err)
end
return func()
end
local function convertValue(value, propertyType, scope)
if propertyType == "string" and type(value) == "string" then
if value:find("${") then
return evaluateExpression(value, scope)
end
end
if type(value) == "string" and value:match("^%${.*}$") then
return evaluateExpression(value, scope)
end
if propertyType == "number" then
return tonumber(value)
elseif propertyType == "boolean" then
return value == "true"
elseif propertyType == "color" then
return colors[value]
elseif propertyType == "table" then
local env = setmetatable({}, { __index = _ENV })
local func = load("return "..value, nil, "t", env)
if func then
return func()
end
end
return value
end
local function handleEvent(node, element, scope)
for attr, value in pairs(node.attributes) do
if attr:match("^on%u") then
local eventName = attr:sub(3,3):lower() .. attr:sub(4)
if scope[value] then
element["on"..eventName:sub(1,1):upper()..eventName:sub(2)](element, scope[value])
end
end
end
for _, child in ipairs(node.children or {}) do
if child.name and child.name:match("^on%u") then
local eventName = child.name:sub(3,3):lower() .. child.name:sub(4)
if child.content then
local code = child.content:gsub("^%s+", ""):gsub("%s+$", "")
local func, err = load(string.format([[
return %s
]], code), "event", "t", scope)
if err then
errorManager.error("Failed to parse event: " .. err)
elseif func then
element["on"..eventName:sub(1,1):upper()..eventName:sub(2)](element, func())
end
end
end
end
end
local BaseElement = {}
function BaseElement:fromXML(node)
for attr, value in pairs(node.attributes) do
local config = self:getPropertyConfig(attr)
if config then
local convertedValue = convertValue(value, config.type)
self.set(attr, convertedValue)
end
end
return self
end
local Container = {}
function Container:loadXML(content, scope)
local tree = parseXML(self, content)
local function createElements(nodes, parent, scope)
for _, node in ipairs(nodes.children) do
if not node.name:match("^on") then
local elementType = node.name:sub(1,1):upper() .. node.name:sub(2)
local element = parent["add"..elementType](parent, node.attributes.name)
for attr, value in pairs(node.attributes) do
local config = element:getPropertyConfig(attr)
if config then
local convertedValue = convertValue(value, config.type, scope)
element.set(attr, convertedValue)
end
end
handleEvent(node, element, scope)
if #node.children > 0 then
createElements(node, element, scope)
end
end
end
end
createElements(tree, self, scope)
return self
end
return {
BaseElement = BaseElement,
Container = Container
}
## Container:loadXML()