deploy: 1c90c6cf04
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user