- Created Plugin loading system

- Added lazy loading system for elements (optional feature)
- Improved rendering performance
- Added ID system which is separated from Eement Names
- Added Focussystem for container
- Improved container performance by only rendering and handling events from visible childrens instead of all
- Added label and input
- Added animation and xml
This commit is contained in:
Robert Jelic
2025-02-13 10:51:12 +01:00
parent bca8889fbd
commit 6dfa554523
23 changed files with 1833 additions and 494 deletions

327
src/plugins/animation.lua Normal file
View File

@@ -0,0 +1,327 @@
local Animation = {}
Animation.__index = Animation
local registeredAnimations = {}
function Animation.registerAnimation(name, handlers)
registeredAnimations[name] = handlers
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
local easings = {
linear = function(progress)
return progress
end,
easeInQuad = function(progress)
return progress * progress
end,
easeOutQuad = function(progress)
return 1 - (1 - progress) * (1 - progress)
end,
easeInOutQuad = function(progress)
if progress < 0.5 then
return 2 * progress * progress
end
return 1 - (-2 * progress + 2)^2 / 2
end
}
function Animation.registerEasing(name, func)
easings[name] = func
end
local AnimationInstance = {}
AnimationInstance.__index = AnimationInstance
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
function AnimationInstance:start()
self.startTime = os.epoch("local") / 1000
if self.handlers.start then
self.handlers.start(self)
end
end
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
function AnimationInstance:complete()
if self.handlers.complete then
self.handlers.complete(self)
end
end
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
function Animation:sequence()
table.insert(self.sequences, {})
self.currentSequence = #self.sequences
self.sequenceCallbacks[self.currentSequence] = {
start = nil,
update = nil,
complete = nil
}
return self
end
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
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)
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

@@ -0,0 +1,23 @@
-- Will temporary exist so that we don't lose track of how the plugin system works
local VisualElement = {hooks={init={}}}
-- 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
}

33
src/plugins/reactive.lua Normal file
View File

@@ -0,0 +1,33 @@
local function setupReactiveProperty(element, propertyName, expression)
end
local function createReactiveFunction(expression, scope)
local code = expression:gsub(
"(%w+)%s*%?%s*([^:]+)%s*:%s*([^}]+)",
"%1 and %2 or %3"
)
return load(string.format([[
return function(self)
return %s
end
]], code), "reactive", "t", scope)()
end
local BaseElement = {}
function BaseElement:setReactiveProperty(propertyName, expression)
setupReactiveProperty(self, propertyName, expression)
return self
end
function BaseElement:setReactive(propertyName, expression)
local reactiveFunc = createReactiveFunction(expression, self)
self.set(propertyName, reactiveFunc)
return self
end
return {
BaseElement = BaseElement
}

184
src/plugins/xml.lua Normal file
View File

@@ -0,0 +1,184 @@
local errorManager = require("errorManager")
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
}