- 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

3
.gitignore vendored
View File

@@ -1 +1,4 @@
test.lua test.lua
test2.lua
lua-ls-cc-tweaked-main
test.xml

View File

@@ -1,222 +0,0 @@
---@class Button
---@field text string
local Button = {}
--- Gets the Button text
---@generic Element: Button
---@param self Element
---@return string
function Button:getText()
return self.text
end
--- Sets the Button text
---@generic Element: Button
---@param self Element
---@param text string
---@return Element
function Button:setText(text)
self.text = text
return self
end
--- The event that is triggered when the button is clicked
---@generic Element: Button
---@param self Element
---@param callback function
---@return Element
function Button:onMouseClick(callback)
return self
end
---@class Container
local Container = {}
--- Adds a new Button to the container
---@generic Element: Container
---@param self Element
---@return Button
function Container:addButton()
return self
end
--- Adds a new Container to the container
---@generic Element: Container
---@param self Element
---@return Container
function Container:addContainer()
return self
end
--- Adds a new Frame to the container
---@generic Element: Container
---@param self Element
---@return Frame
function Container:addFrame()
return self
end
--- Adds a new VisualElement to the container
---@generic Element: Container
---@param self Element
---@return VisualElement
function Container:addVisualElement()
return self
end
---@class VisualElement
---@field x number
---@field y number
---@field z number
---@field width number
---@field height number
---@field background color
---@field foreground color
---@field clicked boolean
local VisualElement = {}
--- Gets the x position of the element
---@generic Element: VisualElement
---@param self Element
---@return number
function VisualElement:getX()
return self.x
end
--- Sets the x position of the element
---@generic Element: VisualElement
---@param self Element
---@param x number
---@return Element
function VisualElement:setX(x)
self.x = x
return self
end
--- Gets the y position of the element
---@generic Element: VisualElement
---@param self Element
---@return number
function VisualElement:getY()
return self.y
end
--- Sets the y position of the element
---@generic Element: VisualElement
---@param self Element
---@param y number
---@return Element
function VisualElement:setY(y)
self.y = y
return self
end
--- Gets the z position of the element
---@generic Element: VisualElement
---@param self Element
---@return number
function VisualElement:getZ()
return self.z
end
--- Sets the z position of the element
---@generic Element: VisualElement
---@param self Element
---@param z number
---@return Element
function VisualElement:setZ(z)
self.z = z
return self
end
--- Gets the width of the element
---@generic Element: VisualElement
---@param self Element
---@return number
function VisualElement:getWidth()
return self.width
end
--- Sets the width of the element
---@generic Element: VisualElement
---@param self Element
---@param width number
---@return Element
function VisualElement:setWidth(width)
self.width = width
return self
end
--- Gets the height of the element
---@generic Element: VisualElement
---@param self Element
---@return number
function VisualElement:getHeight()
return self.height
end
--- Sets the height of the element
---@generic Element: VisualElement
---@param self Element
---@param height number
---@return Element
function VisualElement:setHeight(height)
self.height = height
return self
end
--- Gets the background color of the element
---@generic Element: VisualElement
---@param self Element
---@return color
function VisualElement:getBackground()
return self.background
end
--- Sets the background color of the element
---@generic Element: VisualElement
---@param self Element
---@param background color
---@return Element
function VisualElement:setBackground(background)
self.background = background
return self
end
--- Gets the foreground color of the element
---@generic Element: VisualElement
---@param self Element
---@return color
function VisualElement:getForeground()
return self.foreground
end
--- Sets the foreground color of the element
---@generic Element: VisualElement
---@param self Element
---@param foreground color
---@return Element
function VisualElement:setForeground(foreground)
self.foreground = foreground
return self
end
--- Gets the element is currently clicked
---@generic Element: VisualElement
---@param self Element
---@return boolean
function VisualElement:getClicked()
return self.clicked
end
--- Sets the element is currently clicked
---@generic Element: VisualElement
---@param self Element
---@param clicked boolean
---@return Element
function VisualElement:setClicked(clicked)
self.clicked = clicked
return self
end

33
src/benchmark.lua Normal file
View File

@@ -0,0 +1,33 @@
-- Will temporary exist while developing, maybe i will create a benchmark plugin in future
local log = require("log")
local Benchmark = {}
function Benchmark.start(name)
Benchmark[name] = {
startTime = os.epoch("local"),
updates = 0
}
end
function Benchmark.update(name)
if Benchmark[name] then
Benchmark[name].updates = Benchmark[name].updates + 1
end
end
function Benchmark.stop(name)
if Benchmark[name] then
local endTime = os.epoch("local")
local duration = endTime - Benchmark[name].startTime
local updates = Benchmark[name].updates
log.debug(string.format("[Benchmark] %s: %dms, %d updates, avg: %.2fms per update",
name, duration, updates, duration/updates))
return duration, updates
end
end
return Benchmark

View File

@@ -1,10 +1,14 @@
local args = table.pack(...) local args = table.pack(...)
local dir = fs.getDir(args[2] or "basalt") local dir = fs.getDir(args[2] or "basalt")
local subDir = args[1]
if(dir==nil)then if(dir==nil)then
error("Unable to find directory "..args[2].." please report this bug to our discord.") error("Unable to find directory "..args[2].." please report this bug to our discord.")
end end
local log = require("log") local log = require("log")
local defaultPath = package.path
local format = "path;/path/?.lua;/path/?/init.lua;"
local main = format:gsub("path", dir)
local ElementManager = {} local ElementManager = {}
ElementManager._elements = {} ElementManager._elements = {}
@@ -27,55 +31,19 @@ if fs.exists(elementsDirectory) then
end end
end end
function ElementManager.extendMethod(element, methodName, newMethod, originalMethod) log.info("Loading plugins from "..pluginsDirectory)
if not originalMethod then if fs.exists(pluginsDirectory) then
element[methodName] = newMethod for _, file in ipairs(fs.list(pluginsDirectory)) do
return local name = file:match("(.+).lua")
if name then
log.debug("Found plugin: "..name)
local plugin = require(fs.combine("plugins", name))
if type(plugin) == "table" then
for k,v in pairs(plugin) do
if(ElementManager._plugins[k]==nil)then
ElementManager._plugins[k] = {}
end end
element[methodName] = function(self, ...) table.insert(ElementManager._plugins[k], v)
if newMethod.before then
newMethod.before(self, ...)
end
local results
if newMethod.override then
results = {newMethod.override(self, originalMethod, ...)}
else
results = {originalMethod(self, ...)}
end
if newMethod.after then
newMethod.after(self, ...)
end
return table.unpack(results)
end
end
function ElementManager.loadPlugin(name)
local plugin = require("plugins/"..name)
-- Apply plugin to each targeted element
for elementName, pluginData in pairs(plugin) do
local element = ElementManager._elements[elementName]
if element then
-- Register properties
if pluginData.properties then
element.class.initialize(elementName.."Plugin")
for propName, config in pairs(pluginData.properties) do
element.class.registerProperty(propName, config)
end
end
-- Register/extend methods
if pluginData.methods then
for methodName, methodData in pairs(pluginData.methods) do
ElementManager.extendMethod(
element.class,
methodName,
methodData,
element.class[methodName]
)
end end
end end
end end
@@ -84,7 +52,9 @@ end
function ElementManager.loadElement(name) function ElementManager.loadElement(name)
if not ElementManager._elements[name].loaded then if not ElementManager._elements[name].loaded then
local element = require("elements/"..name) package.path = main.."rom/?"
local element = require(fs.combine("elements", name))
package.path = defaultPath
ElementManager._elements[name] = { ElementManager._elements[name] = {
class = element, class = element,
plugins = element.plugins, plugins = element.plugins,
@@ -92,20 +62,44 @@ function ElementManager.loadElement(name)
} }
log.debug("Loaded element: "..name) log.debug("Loaded element: "..name)
-- Load element's required plugins if(ElementManager._plugins[name]~=nil)then
if element.requires then for _, plugin in pairs(ElementManager._plugins[name]) do
for pluginName, _ in pairs(element.requires) do if(plugin.setup)then
--ElementManager.loadPlugin(pluginName) plugin.setup(element)
end
if(plugin.hooks)then
for methodName, hooks in pairs(plugin.hooks) do
local original = element[methodName]
if(type(original)~="function")then
error("Element "..name.." does not have a method "..methodName)
end
if(type(hooks)=="function")then
element[methodName] = function(self, ...)
original(self, ...)
return hooks(self, ...)
end
elseif(type(hooks)=="table")then
element[methodName] = function(self, ...)
if hooks.pre then hooks.pre(self, ...) end
local result = original(self, ...)
if hooks.post then hooks.post(self, ...) end
return result
end end
end end
end end
end end
function ElementManager.registerPlugin(name, plugin) for funcName, func in pairs(plugin) do
if not plugin.provides then if funcName ~= "setup" and funcName ~= "hooks" then
error("Plugin must specify what it provides") element[funcName] = function(self, ...)
return func(self, ...)
end
end
end
end
end
end end
ElementManager._plugins[name] = plugin
end end
function ElementManager.getElement(name) function ElementManager.getElement(name)
@@ -119,11 +113,4 @@ function ElementManager.getElementList()
return ElementManager._elements return ElementManager._elements
end end
function ElementManager.generateId()
return string.format('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
math.random(0, 0xffff), math.random(0, 0xffff), math.random(0, 0xffff),
math.random(0, 0x0fff) + 0x4000, math.random(0, 0x3fff) + 0x8000,
math.random(0, 0xffff), math.random(0, 0xffff), math.random(0, 0xffff))
end
return ElementManager return ElementManager

View File

@@ -1,4 +1,5 @@
local PropertySystem = require("propertySystem") local PropertySystem = require("propertySystem")
local uuid = require("/libraries/utils").uuid
--- The base class for all UI elements in Basalt --- The base class for all UI elements in Basalt
--- @class BaseElement : PropertySystem --- @class BaseElement : PropertySystem
@@ -7,45 +8,28 @@ BaseElement.__index = BaseElement
BaseElement._events = {} BaseElement._events = {}
--- @property type string BaseElement The type identifier of the element --- @property type string BaseElement The type identifier of the element
BaseElement.defineProperty(BaseElement, "type", {default = "BaseElement", type = "string"}) BaseElement.defineProperty(BaseElement, "type", {default = {"BaseElement"}, type = "string", setter=function(self, value)
if type(value) == "string" then
table.insert(self._values.type, 1, value)
return self._values.type
end
return value
end, getter = function(self, _, index)
if index~= nil and index < 1 then
return self._values.type
end
return self._values.type[index or 1]
end})
--- @property id string BaseElement The unique identifier for the element
BaseElement.defineProperty(BaseElement, "id", {default = "", type = "string", readonly = true})
--- @property name string BaseElement The name of the element
BaseElement.defineProperty(BaseElement, "name", {default = "", type = "string"})
--- @property eventCallbacks table {} Table containing all registered event callbacks --- @property eventCallbacks table {} Table containing all registered event callbacks
BaseElement.defineProperty(BaseElement, "eventCallbacks", {default = {}, type = "table"}) BaseElement.defineProperty(BaseElement, "eventCallbacks", {default = {}, type = "table"})
--- Creates a new BaseElement instance
--- @param id string The unique identifier for this element
--- @param basalt table The basalt instance
--- @return table The newly created BaseElement instance
--- @usage local element = BaseElement.new("myId", basalt)
function BaseElement.new(id, basalt)
local self = setmetatable({}, BaseElement):__init()
self:init(id, basalt)
self.set("type", "BaseElement")
return self
end
--- Initializes the BaseElement instance
--- @param id string The unique identifier for this element
--- @param basalt table The basalt instance
--- @return table self The initialized instance
function BaseElement:init(id, basalt)
self.id = id
self.basalt = basalt
self._registeredEvents = {}
if BaseElement._events then
for event in pairs(BaseElement._events) do
self._registeredEvents[event] = true
local handlerName = "on" .. event:gsub("_(%l)", function(c)
return c:upper()
end):gsub("^%l", string.upper)
self[handlerName] = function(self, ...)
self:registerCallback(event, ...)
end
end
end
return self
end
--- Registers an event that this class can listen to --- Registers an event that this class can listen to
--- @param class table The class to add the event to --- @param class table The class to add the event to
--- @param eventName string The name of the event to register --- @param eventName string The name of the event to register
@@ -57,6 +41,57 @@ function BaseElement.listenTo(class, eventName)
class._events[eventName] = true class._events[eventName] = true
end end
--- Creates a new BaseElement instance
--- @param props table The properties to initialize the element with
--- @param basalt table The basalt instance
--- @return table The newly created BaseElement instance
--- @usage local element = BaseElement.new("myId", basalt)
function BaseElement.new(props, basalt)
local self = setmetatable({}, BaseElement):__init()
self:init(props, basalt)
return self
end
--- Initializes the BaseElement instance
--- @param props table The properties to initialize the element with
--- @param basalt table The basalt instance
--- @return table self The initialized instance
function BaseElement:init(props, basalt)
if(type(props) == "table")then
for k,v in pairs(props)do
self[k] = v
end
end
self._values.id = uuid()
self.basalt = basalt
self._registeredEvents = {}
if BaseElement._events then
for event in pairs(BaseElement._events) do
self._registeredEvents[event] = true
local handlerName = "on" .. event:gsub("_(%l)", function(c)
return c:upper()
end):gsub("^%l", string.upper)
self[handlerName] = function(self, ...)
self:registerCallback(event, ...)
return self
end
end
end
return self
end
--- Checks if the element is a specific type
--- @param type string The type to check for
--- @return boolean Whether the element is of the specified type
function BaseElement:isType(type)
for _, t in ipairs(self._values.type) do
if t == type then
return true
end
end
return false
end
--- Enables or disables event listening for a specific event --- Enables or disables event listening for a specific event
--- @param eventName string The name of the event to listen for --- @param eventName string The name of the event to listen for
--- @param enable? boolean Whether to enable or disable the event (default: true) --- @param enable? boolean Whether to enable or disable the event (default: true)
@@ -113,6 +148,25 @@ function BaseElement:fireEvent(event, ...)
return self return self
end end
--- Handles all events
--- @param event string The event to handle
--- @vararg any The arguments for the event
--- @return boolean? handled Whether the event was handled
function BaseElement:dispatchEvent(event, ...)
if self[event] then
return self[event](self, ...)
end
return self:handleEvent(event, ...)
end
--- The default event handler for all events
--- @param event string The event to handle
--- @vararg any The arguments for the event
--- @return boolean? handled Whether the event was handled
function BaseElement:handleEvent(event, ...)
return true
end
--- Requests a render update for this element --- Requests a render update for this element
--- @usage element:updateRender() --- @usage element:updateRender()
function BaseElement:updateRender() function BaseElement:updateRender()

View File

@@ -1,25 +1,37 @@
local Container = require("elements/Container") local elementManager = require("elementManager")
local Container = elementManager.getElement("Container")
local Render = require("render") local Render = require("render")
---@class BaseFrame : Container ---@class BaseFrame : Container
local BaseFrame = setmetatable({}, Container) local BaseFrame = setmetatable({}, Container)
BaseFrame.__index = BaseFrame BaseFrame.__index = BaseFrame
---@diagnostic disable-next-line: duplicate-set-field ---@property text term term nil text
function BaseFrame.new(id, basalt) BaseFrame.defineProperty(BaseFrame, "term", {default = nil, type = "table", setter = function(self, value)
local self = setmetatable({}, BaseFrame):__init() if value == nil or value.setCursorPos == nil then
self:init(id, basalt) return value
self.terminal = term.current() -- change to :setTerm later!! end
self._render = Render.new(self.terminal) self._render = Render.new(value)
self._renderUpdate = true self._renderUpdate = true
local width, height = self.terminal.getSize() local width, height = value.getSize()
self.set("width", width) self.set("width", width)
self.set("height", height) self.set("height", height)
return value
end})
function BaseFrame.new(props, basalt)
local self = setmetatable({}, BaseFrame):__init()
self:init(props, basalt)
self.set("term", term.current())
self.set("background", colors.red) self.set("background", colors.red)
self.set("type", "BaseFrame")
return self return self
end end
function BaseFrame:init(props, basalt)
Container.init(self, props, basalt)
self.set("type", "BaseFrame")
end
function BaseFrame:multiBlit(x, y, width, height, text, fg, bg) function BaseFrame:multiBlit(x, y, width, height, text, fg, bg)
self._render:multiBlit(x, y, width, height, text, fg, bg) self._render:multiBlit(x, y, width, height, text, fg, bg)
end end
@@ -28,13 +40,19 @@ function BaseFrame:textFg(x, y, text, fg)
self._render:textFg(x, y, text, fg) self._render:textFg(x, y, text, fg)
end end
---@diagnostic disable-next-line: duplicate-set-field function BaseFrame:setCursor(x, y, blink)
local term = self.get("term")
self._render:setCursor(x, y, blink)
end
function BaseFrame:render() function BaseFrame:render()
if(self._renderUpdate) then if(self._renderUpdate) then
if self._render ~= nil then
Container.render(self) Container.render(self)
self._render:render() self._render:render()
self._renderUpdate = false self._renderUpdate = false
end end
end end
end
return BaseFrame return BaseFrame

View File

@@ -1,4 +1,5 @@
local VisualElement = require("elements/VisualElement") local elementManager = require("elementManager")
local VisualElement = elementManager.getElement("VisualElement")
local getCenteredPosition = require("libraries/utils").getCenteredPosition local getCenteredPosition = require("libraries/utils").getCenteredPosition
---@class Button : VisualElement ---@class Button : VisualElement
@@ -6,23 +7,25 @@ local Button = setmetatable({}, VisualElement)
Button.__index = Button Button.__index = Button
---@property text string Button Button text ---@property text string Button Button text
Button.defineProperty(Button, "text", {default = "Button", type = "string"}) Button.defineProperty(Button, "text", {default = "Button", type = "string", canTriggerRender = true})
---@event mouse_click The event that is triggered when the button is clicked ---@event mouse_click The event that is triggered when the button is clicked
Button.listenTo(Button, "mouse_click") Button.listenTo(Button, "mouse_click")
---@diagnostic disable-next-line: duplicate-set-field function Button.new(props, basalt)
function Button.new(id, basalt)
local self = setmetatable({}, Button):__init() local self = setmetatable({}, Button):__init()
self:init(id, basalt) self:init(props, basalt)
self.set("type", "Button")
self.set("width", 10) self.set("width", 10)
self.set("height", 3) self.set("height", 3)
self.set("z", 5) self.set("z", 5)
return self return self
end end
---@diagnostic disable-next-line: duplicate-set-field function Button:init(props, basalt)
VisualElement.init(self, props, basalt)
self.set("type", "Button")
end
function Button:render() function Button:render()
VisualElement.render(self) VisualElement.render(self)
local text = self.get("text") local text = self.get("text")

View File

@@ -1,6 +1,7 @@
local VisualElement = require("elements/VisualElement")
local elementManager = require("elementManager") local elementManager = require("elementManager")
local VisualElement = elementManager.getElement("VisualElement")
local expect = require("libraries/expect") local expect = require("libraries/expect")
local split = require("libraries/utils").split
local max = math.max local max = math.max
@@ -9,65 +10,129 @@ local Container = setmetatable({}, VisualElement)
Container.__index = Container Container.__index = Container
Container.defineProperty(Container, "children", {default = {}, type = "table"}) Container.defineProperty(Container, "children", {default = {}, type = "table"})
Container.defineProperty(Container, "childrenSorted", {default = true, type = "boolean"})
Container.defineProperty(Container, "childrenEventsSorted", {default = true, type = "boolean"})
Container.defineProperty(Container, "childrenEvents", {default = {}, type = "table"}) Container.defineProperty(Container, "childrenEvents", {default = {}, type = "table"})
Container.defineProperty(Container, "eventListenerCount", {default = {}, type = "table"}) Container.defineProperty(Container, "eventListenerCount", {default = {}, type = "table"})
Container.defineProperty(Container, "focusedChild", {default = nil, type = "table", setter = function(self, value, internal)
local oldChild = self._values.focusedChild
if value == oldChild then return value end
if oldChild then
if oldChild:isType("Container") then
oldChild.set("focusedChild", nil, true)
end
oldChild.set("focused", false, true)
end
if value and not internal then
value.set("focused", true, true)
if self.parent then
self.parent:setFocusedChild(self)
end
end
return value
end})
Container.defineProperty(Container, "visibleChildren", {default = {}, type = "table"})
Container.defineProperty(Container, "visibleChildrenEvents", {default = {}, type = "table"})
function Container:isChildVisible(child)
local childX, childY = child.get("x"), child.get("y")
local childW, childH = child.get("width"), child.get("height")
local containerW, containerH = self.get("width"), self.get("height")
return childX <= containerW and
childY <= containerH and
childX + childW > 0 and
childY + childH > 0
end
for k, _ in pairs(elementManager:getElementList()) do for k, _ in pairs(elementManager:getElementList()) do
local capitalizedName = k:sub(1,1):upper() .. k:sub(2) local capitalizedName = k:sub(1,1):upper() .. k:sub(2)
--if not capitalizedName == "BaseFrame" then if capitalizedName ~= "BaseFrame" then
Container["add"..capitalizedName] = function(self, ...) Container["add"..capitalizedName] = function(self, ...)
expect(1, self, "table") expect(1, self, "table")
local element = self.basalt.create(k, ...) local element = self.basalt.create(k, ...)
self.basalt.LOGGER.debug(capitalizedName.." created with ID: " .. element.id)
self:addChild(element) self:addChild(element)
return element return element
end end
--end Container["addDelayed"..capitalizedName] = function(self, prop)
expect(1, self, "table")
local element = self.basalt.create(k, prop, true, self)
return element
end
end
end end
---@diagnostic disable-next-line: duplicate-set-field function Container.new(props, basalt)
function Container.new(id, basalt)
local self = setmetatable({}, Container):__init() local self = setmetatable({}, Container):__init()
self:init(id, basalt) self:init(props, basalt)
self.set("type", "Container")
return self return self
end end
function Container:init(props, basalt)
VisualElement.init(self, props, basalt)
self.set("type", "Container")
end
function Container:addChild(child) function Container:addChild(child)
if child == self then if child == self then
error("Cannot add container to itself") error("Cannot add container to itself")
end end
local childZ = child.get("z") table.insert(self._values.children, child)
local pos = 1
for i, existing in ipairs(self._values.children) do
if existing.get("z") > childZ then
break
end
pos = i + 1
end
table.insert(self._values.children, pos, child)
child.parent = self child.parent = self
self.set("childrenSorted", false)
self:registerChildrenEvents(child) self:registerChildrenEvents(child)
return self return self
end end
local function sortAndFilterChildren(self, children)
local visibleChildren = {}
for _, child in ipairs(children) do
if self:isChildVisible(child) then
table.insert(visibleChildren, child)
end
end
for i = 2, #visibleChildren do
local current = visibleChildren[i]
local currentZ = current.get("z")
local j = i - 1
while j > 0 do
local compare = visibleChildren[j].get("z")
if compare > currentZ then
visibleChildren[j + 1] = visibleChildren[j]
j = j - 1
else
break
end
end
visibleChildren[j + 1] = current
end
return visibleChildren
end
function Container:sortChildren() function Container:sortChildren()
table.sort(self._values.children, function(a, b) self.set("visibleChildren", sortAndFilterChildren(self, self._values.children))
return a.get("z") < b.get("z") self.set("childrenSorted", true)
end)
end end
function Container:sortChildrenEvents(eventName) function Container:sortChildrenEvents(eventName)
if self._values.childrenEvents[eventName] then if self._values.childrenEvents[eventName] then
table.sort(self._values.childrenEvents[eventName], function(a, b) self._values.visibleChildrenEvents[eventName] = sortAndFilterChildren(self, self._values.childrenEvents[eventName])
return a.get("z") > b.get("z")
end)
end end
self.set("childrenEventsSorted", true)
end end
function Container:registerChildrenEvents(child) function Container:registerChildrenEvents(child)
if(child._registeredEvents == nil)then return end
for event in pairs(child._registeredEvents) do for event in pairs(child._registeredEvents) do
self:registerChildEvent(child, event) self:registerChildEvent(child, event)
end end
@@ -89,20 +154,13 @@ function Container:registerChildEvent(child, eventName)
end end
end end
local childZ = child.get("z") self.set("childrenEventsSorted", false)
local pos = 1 table.insert(self._values.childrenEvents[eventName], child)
for i, existing in ipairs(self._values.childrenEvents[eventName]) do
if existing.get("z") < childZ then
break
end
pos = i + 1
end
table.insert(self._values.childrenEvents[eventName], pos, child)
self._values.eventListenerCount[eventName] = self._values.eventListenerCount[eventName] + 1 self._values.eventListenerCount[eventName] = self._values.eventListenerCount[eventName] + 1
end end
function Container:removeChildrenEvents(child) function Container:removeChildrenEvents(child)
if(child._registeredEvents == nil)then return end
for event in pairs(child._registeredEvents) do for event in pairs(child._registeredEvents) do
self:unregisterChildEvent(child, event) self:unregisterChildEvent(child, event)
end end
@@ -130,45 +188,98 @@ function Container:unregisterChildEvent(child, eventName)
end end
function Container:removeChild(child) function Container:removeChild(child)
for i,v in ipairs(self.children) do for i,v in ipairs(self._values.children) do
if v == child then if v == child then
table.remove(self._values.children, i) table.remove(self._values.children, i)
child.parent = nil child.parent = nil
break break
end end
end end
self:removeChildrenEvents(child)
return self return self
end end
function Container:handleEvent(event, ...) function Container:getChild(path)
if(VisualElement.handleEvent(self, event, ...))then if type(path) == "string" then
local parts = split(path, "/")
for _,v in pairs(self._values.children) do
if v.get("name") == parts[1] then
if #parts == 1 then
return v
else
if(v:isType("Container"))then
return v:find(table.concat(parts, "/", 2))
end
end
end
end
end
return nil
end
local function convertMousePosition(self, event, ...)
local args = {...} local args = {...}
if event:find("mouse_") then if event:find("mouse_") then
local button, absX, absY = ... local button, absX, absY = ...
local relX, relY = self:getRelativePosition(absX, absY) local relX, relY = self:getRelativePosition(absX, absY)
args = {button, relX, relY} args = {button, relX, relY}
end end
if self._values.childrenEvents[event] then return args
for _, child in ipairs(self._values.childrenEvents[event]) do end
if(child:handleEvent(event, table.unpack(args)))then
return true local function callChildrenEvents(self, visibleOnly, event, ...)
local children = visibleOnly and self.get("visibleChildrenEvents") or self.get("childrenEvents")
if children[event] then
local events = children[event]
for i = #events, 1, -1 do
local child = events[i]
if(child:dispatchEvent(event, ...))then
return true, child
end end
end end
end end
return false
end
function Container:handleEvent(event, ...)
if(VisualElement.handleEvent(self, event, ...))then
local args = convertMousePosition(self, event, ...)
return callChildrenEvents(self, false, event, table.unpack(args))
end end
end end
--[[function Container:mouse_click(button, x, y) function Container:mouse_click(button, x, y)
if VisualElement.mouse_click(self, button, x, y) then if VisualElement.mouse_click(self, button, x, y) then
if self._values.childrenEvents.mouse_click then local args = convertMousePosition(self, "mouse_click", button, x, y)
for _, child in ipairs(self._values.childrenEvents.mouse_click) do local success, child = callChildrenEvents(self, true, "mouse_click", table.unpack(args))
if child:mouse_click(button, x, y) then if(success)then
self.set("focusedChild", child)
return true return true
end end
self.set("focusedChild", nil)
end end
end end
function Container:key(key)
if self.get("focusedChild") then
return self.get("focusedChild"):dispatchEvent("key", key)
end
return true
end
function Container:char(char)
if self.get("focusedChild") then
return self.get("focusedChild"):dispatchEvent("char", char)
end
return true
end
function Container:key_up(key)
if self.get("focusedChild") then
return self.get("focusedChild"):dispatchEvent("key_up", key)
end
return true
end end
end]]
function Container:multiBlit(x, y, width, height, text, fg, bg) function Container:multiBlit(x, y, width, height, text, fg, bg)
local w, h = self.get("width"), self.get("height") local w, h = self.get("width"), self.get("height")
@@ -194,10 +305,17 @@ function Container:textFg(x, y, text, fg)
VisualElement.textFg(self, math.max(1, x), math.max(1, y), text:sub(textStart, textStart + textLen - 1), fg) VisualElement.textFg(self, math.max(1, x), math.max(1, y), text:sub(textStart, textStart + textLen - 1), fg)
end end
---@diagnostic disable-next-line: duplicate-set-field
function Container:render() function Container:render()
VisualElement.render(self) VisualElement.render(self)
for _, child in ipairs(self._values.children) do if not self.get("childrenSorted")then
self:sortChildren()
end
if not self.get("childrenEventsSorted")then
for event in pairs(self._values.childrenEvents) do
self:sortChildrenEvents(event)
end
end
for _, child in ipairs(self.get("visibleChildren")) do
if child == self then if child == self then
self.basalt.LOGGER.error("CIRCULAR REFERENCE DETECTED!") self.basalt.LOGGER.error("CIRCULAR REFERENCE DETECTED!")
return return

297
src/elements/Flexbox.lua Normal file
View File

@@ -0,0 +1,297 @@
local elementManager = require("elementManager")
local Container = elementManager.getElement("Container")
---@class Flexbox : Container
local Flexbox = setmetatable({}, Container)
Flexbox.__index = Flexbox
Flexbox.defineProperty(Flexbox, "flexDirection", {default = "row", type = "string"})
Flexbox.defineProperty(Flexbox, "flexSpacing", {default = 1, type = "number"})
Flexbox.defineProperty(Flexbox, "flexJustifyContent", {
default = "flex-start",
type = "string",
setter = function(self, value)
if not value:match("^flex%-") then
value = "flex-" .. value
end
return value
end
})
Flexbox.defineProperty(Flexbox, "flexWrap", {default = false, type = "boolean"})
Flexbox.defineProperty(Flexbox, "flexUpdateLayout", {default = false, type = "boolean"})
local lineBreakElement = {
getHeight = function(self) return 0 end,
getWidth = function(self) return 0 end,
getZ = function(self) return 1 end,
getPosition = function(self) return 0, 0 end,
getSize = function(self) return 0, 0 end,
isType = function(self) return false end,
getType = function(self) return "lineBreak" end,
getName = function(self) return "lineBreak" end,
setPosition = function(self) end,
setParent = function(self) end,
setSize = function(self) end,
getFlexGrow = function(self) return 0 end,
getFlexShrink = function(self) return 0 end,
getFlexBasis = function(self) return 0 end,
init = function(self) end,
getVisible = function(self) return true end,
}
local function sortElements(self, direction, spacing, wrap)
local elements = self.get("children")
local sortedElements = {}
if not(wrap)then
local index = 1
local lineSize = 1
local lineOffset = 1
for _,v in pairs(elements)do
if(sortedElements[index]==nil)then sortedElements[index]={offset=1} end
local childHeight = direction == "row" and v.get("height") or v.get("width")
if childHeight > lineSize then
lineSize = childHeight
end
if(v == lineBreakElement)then
lineOffset = lineOffset + lineSize + spacing
lineSize = 1
index = index + 1
sortedElements[index] = {offset=lineOffset}
else
table.insert(sortedElements[index], v)
end
end
elseif(wrap)then
local lineSize = 1
local lineOffset = 1
local maxSize = direction == "row" and self.get("width") or self.get("height")
local usedSize = 0
local index = 1
for _,v in pairs(elements) do
if(sortedElements[index]==nil) then sortedElements[index]={offset=1} end
if v:getType() == "lineBreak" then
lineOffset = lineOffset + lineSize + spacing
usedSize = 0
lineSize = 1
index = index + 1
sortedElements[index] = {offset=lineOffset}
else
local objSize = direction == "row" and v.get("width") or v.get("height")
if(objSize+usedSize<=maxSize) then
table.insert(sortedElements[index], v)
usedSize = usedSize + objSize + spacing
else
lineOffset = lineOffset + lineSize + spacing
lineSize = direction == "row" and v.get("height") or v.get("width")
index = index + 1
usedSize = objSize + spacing
sortedElements[index] = {offset=lineOffset, v}
end
local childHeight = direction == "row" and v.get("height") or v.get("width")
if childHeight > lineSize then
lineSize = childHeight
end
end
end
end
return sortedElements
end
local function calculateRow(self, children, spacing, justifyContent)
local containerWidth = self.get("width")
local usedSpace = spacing * (#children - 1)
local totalFlexGrow = 0
for _, child in ipairs(children) do
if child ~= lineBreakElement then
usedSpace = usedSpace + child.get("width")
totalFlexGrow = totalFlexGrow + child.get("flexGrow")
end
end
local remainingSpace = containerWidth - usedSpace
local extraSpacePerUnit = totalFlexGrow > 0 and (remainingSpace / totalFlexGrow) or 0
local distributedSpace = 0
local currentX = 1
for i, child in ipairs(children) do
if child ~= lineBreakElement then
local childWidth = child.get("width")
if child.get("flexGrow") > 0 then
if i == #children then
local extraSpace = remainingSpace - distributedSpace
childWidth = childWidth + extraSpace
else
local extraSpace = math.floor(extraSpacePerUnit * child.get("flexGrow"))
childWidth = childWidth + extraSpace
distributedSpace = distributedSpace + extraSpace
end
end
child.set("x", currentX)
child.set("y", children.offset or 1)
child.set("width", childWidth)
currentX = currentX + childWidth + spacing
end
end
if justifyContent == "flex-end" then
local offset = containerWidth - (currentX - spacing - 1)
for _, child in ipairs(children) do
child.set("x", child.get("x") + offset)
end
elseif justifyContent == "flex-center" or justifyContent == "center" then -- Akzeptiere beide Formate
local offset = math.floor((containerWidth - (currentX - spacing - 1)) / 2)
for _, child in ipairs(children) do
child.set("x", child.get("x") + offset)
end
end
end
local function calculateColumn(self, children, spacing, justifyContent)
local containerHeight = self.get("height")
local usedSpace = spacing * (#children - 1)
local totalFlexGrow = 0
for _, child in ipairs(children) do
if child ~= lineBreakElement then
usedSpace = usedSpace + child.get("height")
totalFlexGrow = totalFlexGrow + child.get("flexGrow")
end
end
local remainingSpace = containerHeight - usedSpace
local extraSpacePerUnit = totalFlexGrow > 0 and (remainingSpace / totalFlexGrow) or 0
local distributedSpace = 0
local currentY = 1
for i, child in ipairs(children) do
if child ~= lineBreakElement then
local childHeight = child.get("height")
if child.get("flexGrow") > 0 then
if i == #children then
local extraSpace = remainingSpace - distributedSpace
childHeight = childHeight + extraSpace
else
local extraSpace = math.floor(extraSpacePerUnit * child.get("flexGrow"))
childHeight = childHeight + extraSpace
distributedSpace = distributedSpace + extraSpace
end
end
child.set("x", children.offset or 1)
child.set("y", currentY)
child.set("height", childHeight)
currentY = currentY + childHeight + spacing
end
end
if justifyContent == "flex-end" then
local offset = containerHeight - (currentY - spacing - 1)
for _, child in ipairs(children) do
child.set("y", child.get("y") + offset)
end
elseif justifyContent == "flex-center" or justifyContent == "center" then -- Akzeptiere beide Formate
local offset = math.floor((containerHeight - (currentY - spacing - 1)) / 2)
for _, child in ipairs(children) do
child.set("y", child.get("y") + offset)
end
end
end
local function updateLayout(self, direction, spacing, justifyContent, wrap)
local elements = sortElements(self, direction, spacing, wrap)
if direction == "row" then
for _,v in pairs(elements)do
calculateRow(self, v, spacing, justifyContent)
end
else
for _,v in pairs(elements)do
calculateColumn(self, v, spacing, justifyContent)
end
end
self.set("flexUpdateLayout", false)
end
--- Creates a new Flexbox instance
--- @param props table The properties to initialize the element with
--- @param basalt table The basalt instance
--- @return Flexbox object The newly created Flexbox instance
--- @usage local element = Flexbox.new("myId", basalt)
function Flexbox.new(props, basalt)
local self = setmetatable({}, Flexbox):__init()
self:init(props, basalt)
self.set("width", 12)
self.set("height", 6)
self.set("background", colors.blue)
self.set("z", 10)
self:observe("width", function() self.set("flexUpdateLayout", true) end)
self:observe("height", function() self.set("flexUpdateLayout", true) end)
return self
end
function Flexbox:init(props, basalt)
Container.init(self, props, basalt)
self.set("type", "Flexbox")
end
function Flexbox:addChild(element)
Container.addChild(self, element)
if(element~=lineBreakElement)then
element:instanceProperty("flexGrow", {default = 0, type = "number"})
element:instanceProperty("flexShrink", {default = 0, type = "number"})
element:instanceProperty("flexBasis", {default = 0, type = "number"})
end
self.set("flexUpdateLayout", true)
return self
end
function Flexbox:removeChild(element)
Container.removeChild(self, element)
if(element~=lineBreakElement)then
element.setFlexGrow = nil
element.setFlexShrink = nil
element.setFlexBasis = nil
element.getFlexGrow = nil
element.getFlexShrink = nil
element.getFlexBasis = nil
element.set("flexGrow", nil)
element.set("flexShrink", nil)
element.set("flexBasis", nil)
end
self.set("flexUpdateLayout", true)
return self
end
--- Adds a new line break to the flexbox.
---@param self Flexbox The element itself
---@return Flexbox
function Flexbox:addLineBreak()
self:addChild(lineBreakElement)
return self
end
function Flexbox:render()
if(self.get("flexUpdateLayout"))then
updateLayout(self, self.get("flexDirection"), self.get("flexSpacing"), self.get("flexJustifyContent"), self.get("flexWrap"))
end
Container.render(self)
end
return Flexbox

View File

@@ -1,18 +1,28 @@
local Container = require("elements/Container") local elementManager = require("elementManager")
local Container = elementManager.getElement("Container")
---@class Frame : Container ---@class Frame : Container
local Frame = setmetatable({}, Container) local Frame = setmetatable({}, Container)
Frame.__index = Frame Frame.__index = Frame
function Frame.new(id, basalt) --- Creates a new Frame instance
--- @param props table The properties to initialize the element with
--- @param basalt table The basalt instance
--- @return Frame object The newly created Frame instance
--- @usage local element = Frame.new("myId", basalt)
function Frame.new(props, basalt)
local self = setmetatable({}, Frame):__init() local self = setmetatable({}, Frame):__init()
self:init(id, basalt) self:init(props, basalt)
self.set("width", 12) self.set("width", 12)
self.set("height", 6) self.set("height", 6)
self.set("background", colors.blue) self.set("background", colors.blue)
self.set("type", "Frame")
self.set("z", 10) self.set("z", 10)
return self return self
end end
function Frame:init(props, basalt)
Container.init(self, props, basalt)
self.set("type", "Frame")
end
return Frame return Frame

111
src/elements/Input.lua Normal file
View File

@@ -0,0 +1,111 @@
local VisualElement = require("elements/VisualElement")
---@class Input : VisualElement
local Input = setmetatable({}, VisualElement)
Input.__index = Input
---@property text string Input - text to be displayed
Input.defineProperty(Input, "text", {default = "", type = "string", canTriggerRender = true})
---@property cursorPos number Input - current cursor position
Input.defineProperty(Input, "cursorPos", {default = 1, type = "number"})
---@property viewOffset number Input - offset für Text-Viewport
Input.defineProperty(Input, "viewOffset", {default = 0, type = "number"})
Input.listenTo(Input, "mouse_click")
Input.listenTo(Input, "key")
Input.listenTo(Input, "char")
--- Creates a new Input instance
--- @param id string The unique identifier for this element
--- @param basalt table The basalt instance
--- @return Input object The newly created Input instance
--- @usage local element = Input.new("myId", basalt)
function Input.new(id, basalt)
local self = setmetatable({}, Input):__init()
self:init(id, basalt)
self.set("width", 8)
self.set("z", 3)
return self
end
function Input:init(id, basalt)
VisualElement.init(self, id, basalt)
self.set("type", "Input")
end
function Input:char(char)
if not self.get("focused") then return end
local text = self.get("text")
local pos = self.get("cursorPos")
self.set("text", text:sub(1, pos-1) .. char .. text:sub(pos))
self.set("cursorPos", pos + 1)
self:updateViewport()
end
function Input:key(key)
if not self.get("focused") then return end
local pos = self.get("cursorPos")
local text = self.get("text")
if key == keys.left and pos > 1 then
self.set("cursorPos", pos - 1)
self:setCursor(pos - 1,1, true)
elseif key == keys.right and pos <= #text then
self.set("cursorPos", pos + 1)
self:setCursor(pos + 1,1, true)
elseif key == keys.backspace and pos > 1 then
self.set("text", text:sub(1, pos-2) .. text:sub(pos))
self.set("cursorPos", pos - 1)
self:setCursor(pos - 1,1, true)
end
self:updateViewport()
end
function Input:focus()
VisualElement.focus(self)
self.set("background", colors.blue)
self:setCursor(1,1, true)
end
function Input:blur()
VisualElement.blur(self)
self.set("background", colors.green)
end
function Input:updateViewport()
local width = self.get("width")
local text = self.get("text")
local cursorPos = self.get("cursorPos")
local viewOffset = self.get("viewOffset")
-- Wenn Cursor außerhalb des sichtbaren Bereichs nach rechts
if cursorPos - viewOffset > width then
self.set("viewOffset", cursorPos - width)
end
-- Wenn Cursor außerhalb des sichtbaren Bereichs nach links
if cursorPos <= viewOffset then
self.set("viewOffset", cursorPos - 1)
end
end
function Input:render()
VisualElement.render(self)
local text = self.get("text")
local viewOffset = self.get("viewOffset")
local width = self.get("width")
-- Nur den sichtbaren Teil des Textes rendern
local visibleText = text:sub(viewOffset + 1, viewOffset + width)
self:textFg(1, 1, visibleText, self.get("foreground"))
if self.get("focused") then
local cursorPos = self.get("cursorPos")
-- Cursor relativ zum Viewport positionieren
self:setCursor(cursorPos - viewOffset, 1, true)
end
end
return Input

38
src/elements/Label.lua Normal file
View File

@@ -0,0 +1,38 @@
local elementManager = require("elementManager")
local VisualElement = elementManager.getElement("VisualElement")
---@class Label : VisualElement
local Label = setmetatable({}, VisualElement)
Label.__index = Label
---@property text string Label Label text to be displayed
Label.defineProperty(Label, "text", {default = "Label", type = "string", setter = function(self, value)
self.set("width", #value)
return value
end})
--- Creates a new Label instance
--- @param name table The properties to initialize the element with
--- @param basalt table The basalt instance
--- @return Label object The newly created Label instance
--- @usage local element = Label.new("myId", basalt)
function Label.new(props, basalt)
local self = setmetatable({}, Label):__init()
self:init(props, basalt)
self.set("z", 3)
self.set("backgroundEnabled", false)
return self
end
function Label:init(props, basalt)
VisualElement.init(self, props, basalt)
self.set("type", "Label")
end
function Label:render()
VisualElement.render(self)
local text = self.get("text")
self:textFg(1, 1, text, self.get("foreground"))
end
return Label

View File

@@ -1,4 +1,5 @@
local BaseElement = require("elements/BaseElement") local elementManager = require("elementManager")
local BaseElement = elementManager.getElement("BaseElement")
---@alias color number ---@alias color number
@@ -8,40 +9,70 @@ VisualElement.__index = VisualElement
local tHex = require("libraries/colorHex") local tHex = require("libraries/colorHex")
---@property x number 1 x position of the element ---@property x number 1 x position of the element
BaseElement.defineProperty(VisualElement, "x", {default = 1, type = "number", canTriggerRender = true}) VisualElement.defineProperty(VisualElement, "x", {default = 1, type = "number", canTriggerRender = true})
---@property y number 1 y position of the element ---@property y number 1 y position of the element
BaseElement.defineProperty(VisualElement, "y", {default = 1, type = "number", canTriggerRender = true}) VisualElement.defineProperty(VisualElement, "y", {default = 1, type = "number", canTriggerRender = true})
---@property z number 1 z position of the element ---@property z number 1 z position of the element
BaseElement.defineProperty(VisualElement, "z", {default = 1, type = "number", canTriggerRender = true, setter = function(self, value) VisualElement.defineProperty(VisualElement, "z", {default = 1, type = "number", canTriggerRender = true, setter = function(self, value)
self.basalt.LOGGER.debug("Setting z to " .. value)
if self.parent then if self.parent then
self.parent:sortChildren() self.parent:sortChildren()
end end
return value return value
end}) end})
---@property width number 1 width of the element ---@property width number 1 width of the element
BaseElement.defineProperty(VisualElement, "width", {default = 1, type = "number", canTriggerRender = true}) VisualElement.defineProperty(VisualElement, "width", {default = 1, type = "number", canTriggerRender = true})
---@property height number 1 height of the element ---@property height number 1 height of the element
BaseElement.defineProperty(VisualElement, "height", {default = 1, type = "number", canTriggerRender = true}) VisualElement.defineProperty(VisualElement, "height", {default = 1, type = "number", canTriggerRender = true})
---@property background color black background color of the element ---@property background color black background color of the element
BaseElement.defineProperty(VisualElement, "background", {default = colors.black, type = "number", canTriggerRender = true}) VisualElement.defineProperty(VisualElement, "background", {default = colors.black, type = "number", canTriggerRender = true})
---@property foreground color white foreground color of the element ---@property foreground color white foreground color of the element
BaseElement.defineProperty(VisualElement, "foreground", {default = colors.white, type = "number", canTriggerRender = true}) VisualElement.defineProperty(VisualElement, "foreground", {default = colors.white, type = "number", canTriggerRender = true})
---@property clicked boole an false element is currently clicked ---@property clicked boole an false element is currently clicked
BaseElement.defineProperty(VisualElement, "clicked", {default = false, type = "boolean"}) VisualElement.defineProperty(VisualElement, "clicked", {default = false, type = "boolean"})
---@property backgroundEnabled boolean true whether the background is enabled
VisualElement.defineProperty(VisualElement, "backgroundEnabled", {default = true, type = "boolean", canTriggerRender = true})
---@property focused boolean false whether the element is focused
VisualElement.defineProperty(VisualElement, "focused", {default = false, type = "boolean", setter = function(self, value, internal)
local curValue = self.get("focused")
if value == curValue then return value end
if value then
self:focus()
else
self:blur()
end
if not internal and self.parent then
if value then
self.parent:setFocusedChild(self)
else
self.parent:setFocusedChild(nil)
end
end
return value
end})
VisualElement.listenTo(VisualElement, "focus")
VisualElement.listenTo(VisualElement, "blur")
--- Creates a new VisualElement instance --- Creates a new VisualElement instance
--- @param id string The unique identifier for this element --- @param props table The properties to initialize the element with
--- @param basalt table The basalt instance --- @param basalt table The basalt instance
--- @return VisualElement object The newly created VisualElement instance --- @return VisualElement object The newly created VisualElement instance
--- @usage local element = VisualElement.new("myId", basalt) --- @usage local element = VisualElement.new("myId", basalt)
function VisualElement.new(id, basalt) function VisualElement.new(props, basalt)
local self = setmetatable({}, VisualElement):__init() local self = setmetatable({}, VisualElement):__init()
self:init(id, basalt) self:init(props, basalt)
self.set("type", "VisualElement") self.set("type", "VisualElement")
return self return self
end end
function VisualElement:init(props, basalt)
BaseElement.init(self, props, basalt)
self.set("type", "VisualElement")
end
--- Draws a text character/fg/bg at the specified position with a certain size, used in the rendering system --- Draws a text character/fg/bg at the specified position with a certain size, used in the rendering system
--- @param x number The x position to draw --- @param x number The x position to draw
--- @param y number The y position to draw --- @param y number The y position to draw
@@ -87,9 +118,10 @@ end
function VisualElement:mouse_click(button, x, y) function VisualElement:mouse_click(button, x, y)
if self:isInBounds(x, y) then if self:isInBounds(x, y) then
self.set("clicked", true) self.set("clicked", true)
self:fireEvent("mouse_click", button, x, y) self:fireEvent("mouse_click", button, self:getRelativePosition(x, y))
return true return true
end end
return false
end end
function VisualElement:mouse_up(button, x, y) function VisualElement:mouse_up(button, x, y)
@@ -98,40 +130,43 @@ function VisualElement:mouse_up(button, x, y)
self:fireEvent("mouse_up", button, x, y) self:fireEvent("mouse_up", button, x, y)
return true return true
end end
self:fireEvent("mouse_release", button, x, y) self:fireEvent("mouse_release", button, self:getRelativePosition(x, y))
end end
function VisualElement:mouse_release() function VisualElement:mouse_release()
self.set("clicked", false) self.set("clicked", false)
end end
--- Handles all events function VisualElement:focus()
--- @param event string The event to handle self:fireEvent("focus")
--- @vararg any The arguments for the event
--- @return boolean? handled Whether the event was handled
function VisualElement:handleEvent(event, ...)
if(self[event])then
return self[event](self, ...)
end end
function VisualElement:blur()
self:fireEvent("blur")
self:setCursor(1,1, false)
end end
--- Returns the absolute position of the element or the given coordinates. --- Returns the absolute position of the element or the given coordinates.
---@param x? number x position ---@param x? number x position
---@param y? number y position ---@param y? number y position
function VisualElement:getAbsolutePosition(x, y) function VisualElement:getAbsolutePosition(x, y)
if (x == nil) or (y == nil) then local xPos, yPos = self.get("x"), self.get("y")
x, y = self.get("x"), self.get("y") if(x ~= nil) then
xPos = xPos + x - 1
end
if(y ~= nil) then
yPos = yPos + y - 1
end end
local parent = self.parent local parent = self.parent
while parent do while parent do
local px, py = parent.get("x"), parent.get("y") local px, py = parent.get("x"), parent.get("y")
x = x + px - 1 xPos = xPos + px - 1
y = y + py - 1 yPos = yPos + py - 1
parent = parent.parent parent = parent.parent
end end
return x, y return xPos, yPos
end end
--- Returns the relative position of the element or the given coordinates. --- Returns the relative position of the element or the given coordinates.
@@ -155,10 +190,19 @@ function VisualElement:getRelativePosition(x, y)
y - (elementY - 1) - (parentY - 1) y - (elementY - 1) - (parentY - 1)
end end
function VisualElement:setCursor(x, y, blink)
if self.parent then
local absX, absY = self:getAbsolutePosition(x, y)
return self.parent:setCursor(absX, absY, blink)
end
end
--- Renders the element --- Renders the element
--- @usage element:render() --- @usage element:render()
function VisualElement:render() function VisualElement:render()
if(not self.get("backgroundEnabled"))then
return
end
local width, height = self.get("width"), self.get("height") local width, height = self.get("width"), self.get("height")
self:multiBlit(1, 1, width, height, " ", tHex[self.get("foreground")], tHex[self.get("background")]) self:multiBlit(1, 1, width, height, " ", tHex[self.get("foreground")], tHex[self.get("background")])
end end

View File

@@ -1,7 +1,5 @@
local args = {...} local args = {...}
local basaltPath = fs.getDir(args[2])
local basaltPath = args[1] or "basalt"
local defaultPath = package.path local defaultPath = package.path
local format = "path;/path/?.lua;/path/?/init.lua;" local format = "path;/path/?.lua;/path/?/init.lua;"
@@ -18,6 +16,7 @@ end
-- Use xpcall with error handler -- Use xpcall with error handler
local ok, result = pcall(require, "main") local ok, result = pcall(require, "main")
package.path = defaultPath
if not ok then if not ok then
errorHandler(result) errorHandler(result)
else else

View File

@@ -24,4 +24,32 @@ function utils.deepCopy(obj)
return copy return copy
end end
function utils.uuid()
return string.format('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
math.random(0, 0xffff), math.random(0, 0xffff), math.random(0, 0xffff),
math.random(0, 0x0fff) + 0x4000, math.random(0, 0x3fff) + 0x8000,
math.random(0, 0xffff), math.random(0, 0xffff), math.random(0, 0xffff))
end
function utils.split(str, sep)
local parts = {}
local start = 1
local len = len(str)
local splitIndex = 1
while true do
local index = str:find(sep, start, true)
if not index then
parts[splitIndex] = str:sub(start, len)
break
end
parts[splitIndex] = str:sub(start, index - 1)
start = index + 1
splitIndex = splitIndex + 1
end
return parts
end
return utils return utils

View File

@@ -1,5 +1,9 @@
local benchmark = require("benchmark")
benchmark.start("Basalt Initialization")
local elementManager = require("elementManager") local elementManager = require("elementManager")
local errorManager = require("errorManager") local errorManager = require("errorManager")
local propertySystem = require("propertySystem")
--- This is the UI Manager and the starting point for your project. The following functions allow you to influence the default behavior of Basalt. --- This is the UI Manager and the starting point for your project. The following functions allow you to influence the default behavior of Basalt.
--- ---
@@ -13,24 +17,65 @@ basalt._events = {}
basalt._schedule = {} basalt._schedule = {}
basalt._plugins = {} basalt._plugins = {}
basalt.LOGGER = require("log") basalt.LOGGER = require("log")
basalt.path = fs.getDir(select(2, ...))
local mainFrame = nil local mainFrame = nil
local updaterActive = false local updaterActive = false
local _type = type
local lazyElements = {}
local lazyElementCount = 10
local lazyElementsTimer = 0
local isLazyElementsTimerActive = false
local function queueLazyElements()
if(isLazyElementsTimerActive)then return end
lazyElementsTimer = os.startTimer(0.2)
isLazyElementsTimerActive = true
end
local function loadLazyElements(count)
for _=1,count do
local blueprint = lazyElements[1]
if(blueprint)then
blueprint:create()
end
table.remove(lazyElements, 1)
end
end
local function lazyElementsEventHandler(event, timerId)
if(event=="timer")then
if(timerId==lazyElementsTimer)then
loadLazyElements(lazyElementCount)
isLazyElementsTimerActive = false
lazyElementsTimer = 0
if(#lazyElements>0)then
queueLazyElements()
end
return true
end
end
end
--- Creates and returns a new UI element of the specified type. --- Creates and returns a new UI element of the specified type.
--- @shortDescription Creates a new UI element --- @shortDescription Creates a new UI element
--- @param type string The type of element to create (e.g. "Button", "Label", "BaseFrame") --- @param type string The type of element to create (e.g. "Button", "Label", "BaseFrame")
--- @param id? string Optional unique identifier for the element --- @param properties? string|table Optional name for the element or a table with properties to initialize the element with
--- @return table element The created element instance --- @return table element The created element instance
--- @usage local button = basalt.create("Button") --- @usage local button = basalt.create("Button")
function basalt.create(type, id) function basalt.create(type, properties, lazyLoading, parent)
if(id==nil)then id = elementManager.generateId() end if(_type(properties)=="string")then properties = {name=properties} end
local element = elementManager.getElement(type).new(id, basalt) if(properties == nil)then properties = {name = type} end
local ok, result = pcall(require, "main") local elementClass = elementManager.getElement(type)
if not ok then if(lazyLoading)then
errorManager(false, result) local blueprint = propertySystem.blueprint(elementClass, properties, basalt, parent)
table.insert(lazyElements, blueprint)
queueLazyElements()
return blueprint
else
return elementClass.new(properties, basalt)
end end
return element
end end
--- Creates and returns a new frame --- Creates and returns a new frame
@@ -87,17 +132,10 @@ end
--- @local Internal event handler --- @local Internal event handler
local function updateEvent(event, ...) local function updateEvent(event, ...)
if(event=="terminate")then basalt.stop() end if(event=="terminate")then basalt.stop() end
if lazyElementsEventHandler(event, ...) then return end
if event:find("mouse") then if(mainFrame:dispatchEvent(event, ...))then
if mainFrame then return
mainFrame:handleEvent(event, ...)
end
end
if event:find("key") then
if mainFrame then
mainFrame:handleEvent(event, ...)
end
end end
if basalt._events[event] then if basalt._events[event] then
@@ -137,12 +175,14 @@ end
--- @usage basalt.run() --- @usage basalt.run()
--- @usage basalt.run(false) --- @usage basalt.run(false)
function basalt.run(isActive) function basalt.run(isActive)
benchmark.stop("Basalt Initialization")
updaterActive = isActive updaterActive = isActive
if(isActive==nil)then updaterActive = true end if(isActive==nil)then updaterActive = true end
local function f() local function f()
renderFrames() renderFrames()
while updaterActive do while updaterActive do
updateEvent(os.pullEventRaw()) updateEvent(os.pullEventRaw())
renderFrames()
end end
end end
while updaterActive do while updaterActive do

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
}

View File

@@ -1,11 +1,14 @@
local deepCopy = require("libraries/utils").deepCopy local deepCopy = require("libraries/utils").deepCopy
local expect = require("libraries/expect") local expect = require("libraries/expect")
local errorManager = require("errorManager")
local log = require("log")
--- @class PropertySystem --- @class PropertySystem
local PropertySystem = {} local PropertySystem = {}
PropertySystem.__index = PropertySystem PropertySystem.__index = PropertySystem
PropertySystem._properties = {} PropertySystem._properties = {}
local blueprintTemplates = {}
function PropertySystem.defineProperty(class, name, config) function PropertySystem.defineProperty(class, name, config)
if not rawget(class, '_properties') then if not rawget(class, '_properties') then
@@ -22,43 +25,162 @@ function PropertySystem.defineProperty(class, name, config)
local capitalizedName = name:sub(1,1):upper() .. name:sub(2) local capitalizedName = name:sub(1,1):upper() .. name:sub(2)
class["get" .. capitalizedName] = function(self) class["get" .. capitalizedName] = function(self, ...)
expect(1, self, "element") expect(1, self, "element")
local value = self._values[name] local value = self._values[name]
return config.getter and config.getter(value) or value return config.getter and config.getter(self, value, ...) or value
end end
class["set" .. capitalizedName] = function(self, value) class["set" .. capitalizedName] = function(self, value, ...)
expect(1, self, "element") expect(1, self, "element")
expect(2, value, config.type) expect(2, value, config.type)
if config.setter then if config.setter then
value = config.setter(self, value) value = config.setter(self, value, ...)
end end
self:_updateProperty(name, value) self:_updateProperty(name, value)
return self return self
end end
end end
--- Creates a blueprint of an element class with all its properties
--- @param elementClass table The element class to create a blueprint from
--- @return table blueprint A table containing all property definitions
function PropertySystem.blueprint(elementClass, properties, basalt, parent)
if not blueprintTemplates[elementClass] then
local template = {
basalt = basalt,
__isBlueprint = true,
_values = properties or {},
_events = {},
render = function() end,
dispatchEvent = function() end,
init = function() end,
}
template.loaded = function(self, callback)
self.loadedCallback = callback
return template
end
template.create = function(self)
local element = elementClass.new({}, basalt)
for name, value in pairs(self._values) do
element._values[name] = value
end
for name, callbacks in pairs(self._events) do
for _, callback in ipairs(callbacks) do
element[name](element, callback)
end
end
if(parent~=nil)then
parent:addChild(element)
end
element:updateRender()
self.loadedCallback(element)
return element
end
local currentClass = elementClass
while currentClass do
if rawget(currentClass, '_properties') then
for name, config in pairs(currentClass._properties) do
if type(config.default) == "table" then
template._values[name] = deepCopy(config.default)
else
template._values[name] = config.default
end
end
end
currentClass = getmetatable(currentClass) and rawget(getmetatable(currentClass), '__index')
end
blueprintTemplates[elementClass] = template
end
local blueprint = {
_values = {},
_events = {},
loadedCallback = function() end,
}
blueprint.get = function(name)
return blueprint._values[name]
end
blueprint.set = function(name, value)
blueprint._values[name] = value
return blueprint
end
setmetatable(blueprint, {
__index = function(self, k)
if k:match("^on%u") then
return function(_, callback)
self._events[k] = self._events[k] or {}
table.insert(self._events[k], callback)
return self
end
end
if k:match("^get%u") then
local propName = k:sub(4,4):lower() .. k:sub(5)
return function()
return self._values[propName]
end
end
if k:match("^set%u") then
local propName = k:sub(4,4):lower() .. k:sub(5)
return function(_, value)
self._values[propName] = value
return self
end
end
return blueprintTemplates[elementClass][k]
end
})
return blueprint
end
function PropertySystem.createFromBlueprint(elementClass, blueprint, basalt)
local element = elementClass.new({}, basalt)
for name, value in pairs(blueprint._values) do
if type(value) == "table" then
element._values[name] = deepCopy(value)
else
element._values[name] = value
end
end
return element
end
function PropertySystem:__init() function PropertySystem:__init()
self._values = {} self._values = {}
self._observers = {} self._observers = {}
self.set = function(name, value) self.set = function(name, value, ...)
local oldValue = self._values[name] local oldValue = self._values[name]
self._values[name] = value local config = self._properties[name]
if(self._properties[name].setter) then if(config~=nil)then
value = self._properties[name].setter(self, value) if(config.setter) then
value = config.setter(self, value, ...)
end end
if config.canTriggerRender then
self:updateRender()
end
self._values[name] = value
if oldValue ~= value and self._observers[name] then if oldValue ~= value and self._observers[name] then
for _, callback in ipairs(self._observers[name]) do for _, callback in ipairs(self._observers[name]) do
callback(self, value, oldValue) callback(self, value, oldValue)
end end
end end
end end
end
self.get = function(name) self.get = function(name, ...)
return self._values[name] local value = self._values[name]
local config = self._properties[name]
if(config==nil)then errorManager.error("Property not found: "..name) return end
return config.getter and config.getter(self, value, ...) or value
end end
local properties = {} local properties = {}
@@ -139,4 +261,25 @@ function PropertySystem:observe(name, callback)
return self return self
end end
function PropertySystem:instanceProperty(name, config)
PropertySystem.defineProperty(self, name, config)
self._values[name] = config.default
return self
end
function PropertySystem:removeProperty(name)
self._values[name] = nil
self._properties[name] = nil
self._observers[name] = nil
local capitalizedName = name:sub(1,1):upper() .. name:sub(2)
self["get" .. capitalizedName] = nil
self["set" .. capitalizedName] = nil
return self
end
function PropertySystem:getPropertyConfig(name)
return self._properties[name]
end
return PropertySystem return PropertySystem

View File

@@ -1,7 +1,6 @@
local Render = {} local Render = {}
Render.__index = Render Render.__index = Render
local colorChars = require("libraries/colorHex") local colorChars = require("libraries/colorHex")
local log = require("log")
function Render.new(terminal) function Render.new(terminal)
local self = setmetatable({}, Render) local self = setmetatable({}, Render)
@@ -12,19 +11,27 @@ function Render.new(terminal)
text = {}, text = {},
fg = {}, fg = {},
bg = {}, bg = {},
changed = {} dirtyRects = {}
} }
for y=1, self.height do for y=1, self.height do
self.buffer.text[y] = string.rep(" ", self.width) self.buffer.text[y] = string.rep(" ", self.width)
self.buffer.fg[y] = string.rep("0", self.width) self.buffer.fg[y] = string.rep("0", self.width)
self.buffer.bg[y] = string.rep("f", self.width) self.buffer.bg[y] = string.rep("f", self.width)
self.buffer.changed[y] = false
end end
return self return self
end end
function Render:addDirtyRect(x, y, width, height)
table.insert(self.buffer.dirtyRects, {
x = x,
y = y,
width = width,
height = height
})
end
function Render:blit(x, y, text, fg, bg) function Render:blit(x, y, text, fg, bg)
if y < 1 or y > self.height then return self end if y < 1 or y > self.height then return self end
if(#text ~= #fg or #text ~= #bg)then if(#text ~= #fg or #text ~= #bg)then
@@ -34,7 +41,7 @@ function Render:blit(x, y, text, fg, bg)
self.buffer.text[y] = self.buffer.text[y]:sub(1,x-1) .. text .. self.buffer.text[y]:sub(x+#text) self.buffer.text[y] = self.buffer.text[y]:sub(1,x-1) .. text .. self.buffer.text[y]:sub(x+#text)
self.buffer.fg[y] = self.buffer.fg[y]:sub(1,x-1) .. fg .. self.buffer.fg[y]:sub(x+#fg) self.buffer.fg[y] = self.buffer.fg[y]:sub(1,x-1) .. fg .. self.buffer.fg[y]:sub(x+#fg)
self.buffer.bg[y] = self.buffer.bg[y]:sub(1,x-1) .. bg .. self.buffer.bg[y]:sub(x+#bg) self.buffer.bg[y] = self.buffer.bg[y]:sub(1,x-1) .. bg .. self.buffer.bg[y]:sub(x+#bg)
self.buffer.changed[y] = true self:addDirtyRect(x, y, #text, 1)
return self return self
end end
@@ -55,10 +62,10 @@ function Render:multiBlit(x, y, width, height, text, fg, bg)
self.buffer.text[cy] = self.buffer.text[cy]:sub(1,x-1) .. text .. self.buffer.text[cy]:sub(x+#text) self.buffer.text[cy] = self.buffer.text[cy]:sub(1,x-1) .. text .. self.buffer.text[cy]:sub(x+#text)
self.buffer.fg[cy] = self.buffer.fg[cy]:sub(1,x-1) .. fg .. self.buffer.fg[cy]:sub(x+#fg) self.buffer.fg[cy] = self.buffer.fg[cy]:sub(1,x-1) .. fg .. self.buffer.fg[cy]:sub(x+#fg)
self.buffer.bg[cy] = self.buffer.bg[cy]:sub(1,x-1) .. bg .. self.buffer.bg[cy]:sub(x+#bg) self.buffer.bg[cy] = self.buffer.bg[cy]:sub(1,x-1) .. bg .. self.buffer.bg[cy]:sub(x+#bg)
self.buffer.changed[cy] = true
end end
end end
self:addDirtyRect(x, y, width, height)
return self return self
end end
@@ -68,7 +75,7 @@ function Render:textFg(x, y, text, fg)
self.buffer.text[y] = self.buffer.text[y]:sub(1,x-1) .. text .. self.buffer.text[y]:sub(x+#text) self.buffer.text[y] = self.buffer.text[y]:sub(1,x-1) .. text .. self.buffer.text[y]:sub(x+#text)
self.buffer.fg[y] = self.buffer.fg[y]:sub(1,x-1) .. fg:rep(#text) .. self.buffer.fg[y]:sub(x+#text) self.buffer.fg[y] = self.buffer.fg[y]:sub(1,x-1) .. fg:rep(#text) .. self.buffer.fg[y]:sub(x+#text)
self.buffer.changed[y] = true self:addDirtyRect(x, y, #text, 1)
return self return self
end end
@@ -77,7 +84,7 @@ function Render:text(x, y, text)
if y < 1 or y > self.height then return self end if y < 1 or y > self.height then return self end
self.buffer.text[y] = self.buffer.text[y]:sub(1,x-1) .. text .. self.buffer.text[y]:sub(x+#text) self.buffer.text[y] = self.buffer.text[y]:sub(1,x-1) .. text .. self.buffer.text[y]:sub(x+#text)
self.buffer.changed[y] = true self:addDirtyRect(x, y, #text, 1)
return self return self
end end
@@ -86,7 +93,7 @@ function Render:fg(x, y, fg)
if y < 1 or y > self.height then return self end if y < 1 or y > self.height then return self end
self.buffer.fg[y] = self.buffer.fg[y]:sub(1,x-1) .. fg .. self.buffer.fg[y]:sub(x+#fg) self.buffer.fg[y] = self.buffer.fg[y]:sub(1,x-1) .. fg .. self.buffer.fg[y]:sub(x+#fg)
self.buffer.changed[y] = true self:addDirtyRect(x, y, #fg, 1)
return self return self
end end
@@ -95,7 +102,7 @@ function Render:bg(x, y, bg)
if y < 1 or y > self.height then return self end if y < 1 or y > self.height then return self end
self.buffer.bg[y] = self.buffer.bg[y]:sub(1,x-1) .. bg .. self.buffer.bg[y]:sub(x+#bg) self.buffer.bg[y] = self.buffer.bg[y]:sub(1,x-1) .. bg .. self.buffer.bg[y]:sub(x+#bg)
self.buffer.changed[y] = true self:addDirtyRect(x, y, #bg, 1)
return self return self
end end
@@ -106,23 +113,84 @@ function Render:clear(bg)
self.buffer.text[y] = string.rep(" ", self.width) self.buffer.text[y] = string.rep(" ", self.width)
self.buffer.fg[y] = string.rep("0", self.width) self.buffer.fg[y] = string.rep("0", self.width)
self.buffer.bg[y] = string.rep(bgChar, self.width) self.buffer.bg[y] = string.rep(bgChar, self.width)
self.buffer.changed[y] = true self:addDirtyRect(1, y, self.width, 1)
end end
return self return self
end end
function Render:render() function Render:render()
for y=1, self.height do local benchmark = require("benchmark")
if self.buffer.changed[y] then benchmark.start("render")
self.terminal.setCursorPos(1, y)
local mergedRects = {}
for _, rect in ipairs(self.buffer.dirtyRects) do
local merged = false
for _, existing in ipairs(mergedRects) do
if self:rectOverlaps(rect, existing) then
self:mergeRects(existing, rect)
merged = true
break
end
end
if not merged then
table.insert(mergedRects, rect)
end
end
-- Nur die Dirty Rectangles rendern
for _, rect in ipairs(mergedRects) do
for y = rect.y, rect.y + rect.height - 1 do
if y >= 1 and y <= self.height then
self.terminal.setCursorPos(rect.x, y)
self.terminal.blit( self.terminal.blit(
self.buffer.text[y], self.buffer.text[y]:sub(rect.x, rect.x + rect.width - 1),
self.buffer.fg[y], self.buffer.fg[y]:sub(rect.x, rect.x + rect.width - 1),
self.buffer.bg[y] self.buffer.bg[y]:sub(rect.x, rect.x + rect.width - 1)
) )
self.buffer.changed[y] = false
end end
end end
end
benchmark.update("render")
self.buffer.dirtyRects = {}
if self.blink then
self.terminal.setCursorPos(self.xCursor, self.yCursor)
self.terminal.setCursorBlink(true)
else
self.terminal.setCursorBlink(false)
end
--benchmark.stop("render")
return self
end
-- Hilfsfunktionen für Rectangle-Management
function Render:rectOverlaps(r1, r2)
return not (r1.x + r1.width <= r2.x or
r2.x + r2.width <= r1.x or
r1.y + r1.height <= r2.y or
r2.y + r2.height <= r1.y)
end
function Render:mergeRects(target, source)
local x1 = math.min(target.x, source.x)
local y1 = math.min(target.y, source.y)
local x2 = math.max(target.x + target.width, source.x + source.width)
local y2 = math.max(target.y + target.height, source.y + source.height)
target.x = x1
target.y = y1
target.width = x2 - x1
target.height = y2 - y1
end
function Render:setCursor(x, y, blink)
self.terminal.setCursorPos(x, y)
self.terminal.setCursorBlink(blink)
self.xCursor = x
self.yCursor = y
self.blink = blink
return self return self
end end