- 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

5
.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 dir = fs.getDir(args[2] or "basalt")
local subDir = args[1]
if(dir==nil)then
error("Unable to find directory "..args[2].." please report this bug to our discord.")
end
local log = require("log")
local defaultPath = package.path
local format = "path;/path/?.lua;/path/?/init.lua;"
local main = format:gsub("path", dir)
local ElementManager = {}
ElementManager._elements = {}
@@ -27,55 +31,19 @@ if fs.exists(elementsDirectory) then
end
end
function ElementManager.extendMethod(element, methodName, newMethod, originalMethod)
if not originalMethod then
element[methodName] = newMethod
return
end
element[methodName] = function(self, ...)
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]
)
log.info("Loading plugins from "..pluginsDirectory)
if fs.exists(pluginsDirectory) then
for _, file in ipairs(fs.list(pluginsDirectory)) do
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
table.insert(ElementManager._plugins[k], v)
end
end
end
@@ -84,7 +52,9 @@ end
function ElementManager.loadElement(name)
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] = {
class = element,
plugins = element.plugins,
@@ -92,22 +62,46 @@ function ElementManager.loadElement(name)
}
log.debug("Loaded element: "..name)
-- Load element's required plugins
if element.requires then
for pluginName, _ in pairs(element.requires) do
--ElementManager.loadPlugin(pluginName)
if(ElementManager._plugins[name]~=nil)then
for _, plugin in pairs(ElementManager._plugins[name]) do
if(plugin.setup)then
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
for funcName, func in pairs(plugin) do
if funcName ~= "setup" and funcName ~= "hooks" then
element[funcName] = function(self, ...)
return func(self, ...)
end
end
end
end
end
end
end
function ElementManager.registerPlugin(name, plugin)
if not plugin.provides then
error("Plugin must specify what it provides")
end
ElementManager._plugins[name] = plugin
end
function ElementManager.getElement(name)
if not ElementManager._elements[name].loaded then
ElementManager.loadElement(name)
@@ -119,11 +113,4 @@ function ElementManager.getElementList()
return ElementManager._elements
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

View File

@@ -1,4 +1,5 @@
local PropertySystem = require("propertySystem")
local uuid = require("/libraries/utils").uuid
--- The base class for all UI elements in Basalt
--- @class BaseElement : PropertySystem
@@ -7,45 +8,28 @@ BaseElement.__index = BaseElement
BaseElement._events = {}
--- @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
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
--- @param class table The class to add the event to
--- @param eventName string The name of the event to register
@@ -57,6 +41,57 @@ function BaseElement.listenTo(class, eventName)
class._events[eventName] = true
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
--- @param eventName string The name of the event to listen for
--- @param enable? boolean Whether to enable or disable the event (default: true)
@@ -113,6 +148,25 @@ function BaseElement:fireEvent(event, ...)
return self
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
--- @usage element: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")
---@class BaseFrame : Container
local BaseFrame = setmetatable({}, Container)
BaseFrame.__index = BaseFrame
---@diagnostic disable-next-line: duplicate-set-field
function BaseFrame.new(id, basalt)
local self = setmetatable({}, BaseFrame):__init()
self:init(id, basalt)
self.terminal = term.current() -- change to :setTerm later!!
self._render = Render.new(self.terminal)
---@property text term term nil text
BaseFrame.defineProperty(BaseFrame, "term", {default = nil, type = "table", setter = function(self, value)
if value == nil or value.setCursorPos == nil then
return value
end
self._render = Render.new(value)
self._renderUpdate = true
local width, height = self.terminal.getSize()
local width, height = value.getSize()
self.set("width", width)
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("type", "BaseFrame")
return self
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)
self._render:multiBlit(x, y, width, height, text, fg, bg)
end
@@ -28,12 +40,18 @@ function BaseFrame:textFg(x, y, text, fg)
self._render:textFg(x, y, text, fg)
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()
if(self._renderUpdate) then
Container.render(self)
self._render:render()
self._renderUpdate = false
if self._render ~= nil then
Container.render(self)
self._render:render()
self._renderUpdate = false
end
end
end

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
---@class Button : VisualElement
@@ -6,23 +7,25 @@ local Button = setmetatable({}, VisualElement)
Button.__index = Button
---@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
Button.listenTo(Button, "mouse_click")
---@diagnostic disable-next-line: duplicate-set-field
function Button.new(id, basalt)
function Button.new(props, basalt)
local self = setmetatable({}, Button):__init()
self:init(id, basalt)
self.set("type", "Button")
self:init(props, basalt)
self.set("width", 10)
self.set("height", 3)
self.set("z", 5)
return self
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()
VisualElement.render(self)
local text = self.get("text")

View File

@@ -1,6 +1,7 @@
local VisualElement = require("elements/VisualElement")
local elementManager = require("elementManager")
local VisualElement = elementManager.getElement("VisualElement")
local expect = require("libraries/expect")
local split = require("libraries/utils").split
local max = math.max
@@ -9,65 +10,129 @@ local Container = setmetatable({}, VisualElement)
Container.__index = Container
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, "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
local capitalizedName = k:sub(1,1):upper() .. k:sub(2)
--if not capitalizedName == "BaseFrame" then
if capitalizedName ~= "BaseFrame" then
Container["add"..capitalizedName] = function(self, ...)
expect(1, self, "table")
local element = self.basalt.create(k, ...)
self.basalt.LOGGER.debug(capitalizedName.." created with ID: " .. element.id)
self:addChild(element)
return element
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
---@diagnostic disable-next-line: duplicate-set-field
function Container.new(id, basalt)
function Container.new(props, basalt)
local self = setmetatable({}, Container):__init()
self:init(id, basalt)
self.set("type", "Container")
self:init(props, basalt)
return self
end
function Container:init(props, basalt)
VisualElement.init(self, props, basalt)
self.set("type", "Container")
end
function Container:addChild(child)
if child == self then
error("Cannot add container to itself")
end
local childZ = child.get("z")
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)
table.insert(self._values.children, child)
child.parent = self
self.set("childrenSorted", false)
self:registerChildrenEvents(child)
return self
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()
table.sort(self._values.children, function(a, b)
return a.get("z") < b.get("z")
end)
self.set("visibleChildren", sortAndFilterChildren(self, self._values.children))
self.set("childrenSorted", true)
end
function Container:sortChildrenEvents(eventName)
if self._values.childrenEvents[eventName] then
table.sort(self._values.childrenEvents[eventName], function(a, b)
return a.get("z") > b.get("z")
end)
self._values.visibleChildrenEvents[eventName] = sortAndFilterChildren(self, self._values.childrenEvents[eventName])
end
self.set("childrenEventsSorted", true)
end
function Container:registerChildrenEvents(child)
if(child._registeredEvents == nil)then return end
for event in pairs(child._registeredEvents) do
self:registerChildEvent(child, event)
end
@@ -89,20 +154,13 @@ function Container:registerChildEvent(child, eventName)
end
end
local childZ = child.get("z")
local pos = 1
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.set("childrenEventsSorted", false)
table.insert(self._values.childrenEvents[eventName], child)
self._values.eventListenerCount[eventName] = self._values.eventListenerCount[eventName] + 1
end
function Container:removeChildrenEvents(child)
if(child._registeredEvents == nil)then return end
for event in pairs(child._registeredEvents) do
self:unregisterChildEvent(child, event)
end
@@ -130,45 +188,98 @@ function Container:unregisterChildEvent(child, eventName)
end
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
table.remove(self._values.children, i)
child.parent = nil
break
end
end
self:removeChildrenEvents(child)
return self
end
function Container:getChild(path)
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 = {...}
if event:find("mouse_") then
local button, absX, absY = ...
local relX, relY = self:getRelativePosition(absX, absY)
args = {button, relX, relY}
end
return args
end
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
return false
end
function Container:handleEvent(event, ...)
if(VisualElement.handleEvent(self, event, ...))then
local args = {...}
if event:find("mouse_") then
local button, absX, absY = ...
local relX, relY = self:getRelativePosition(absX, absY)
args = {button, relX, relY}
end
if self._values.childrenEvents[event] then
for _, child in ipairs(self._values.childrenEvents[event]) do
if(child:handleEvent(event, table.unpack(args)))then
return true
end
end
end
local args = convertMousePosition(self, event, ...)
return callChildrenEvents(self, false, event, table.unpack(args))
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 self._values.childrenEvents.mouse_click then
for _, child in ipairs(self._values.childrenEvents.mouse_click) do
if child:mouse_click(button, x, y) then
return true
end
end
local args = convertMousePosition(self, "mouse_click", button, x, y)
local success, child = callChildrenEvents(self, true, "mouse_click", table.unpack(args))
if(success)then
self.set("focusedChild", child)
return true
end
self.set("focusedChild", nil)
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
function Container:multiBlit(x, y, width, height, text, fg, bg)
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)
end
---@diagnostic disable-next-line: duplicate-set-field
function Container:render()
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
self.basalt.LOGGER.error("CIRCULAR REFERENCE DETECTED!")
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
local Frame = setmetatable({}, Container)
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()
self:init(id, basalt)
self:init(props, basalt)
self.set("width", 12)
self.set("height", 6)
self.set("background", colors.blue)
self.set("type", "Frame")
self.set("z", 10)
return self
end
function Frame:init(props, basalt)
Container.init(self, props, basalt)
self.set("type", "Frame")
end
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
@@ -8,40 +9,70 @@ VisualElement.__index = VisualElement
local tHex = require("libraries/colorHex")
---@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
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
BaseElement.defineProperty(VisualElement, "z", {default = 1, type = "number", canTriggerRender = true, setter = function(self, value)
self.basalt.LOGGER.debug("Setting z to " .. value)
VisualElement.defineProperty(VisualElement, "z", {default = 1, type = "number", canTriggerRender = true, setter = function(self, value)
if self.parent then
self.parent:sortChildren()
end
return value
end})
---@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
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
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
BaseElement.defineProperty(VisualElement, "foreground", {default = colors.white, type = "number", canTriggerRender = true})
---@property clicked boolean false element is currently clicked
BaseElement.defineProperty(VisualElement, "clicked", {default = false, type = "boolean"})
VisualElement.defineProperty(VisualElement, "foreground", {default = colors.white, type = "number", canTriggerRender = true})
---@property clicked boole an false element is currently clicked
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
--- @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
--- @return VisualElement object The newly created VisualElement instance
--- @usage local element = VisualElement.new("myId", basalt)
function VisualElement.new(id, basalt)
function VisualElement.new(props, basalt)
local self = setmetatable({}, VisualElement):__init()
self:init(id, basalt)
self:init(props, basalt)
self.set("type", "VisualElement")
return self
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
--- @param x number The x position to draw
--- @param y number The y position to draw
@@ -87,9 +118,10 @@ end
function VisualElement:mouse_click(button, x, y)
if self:isInBounds(x, y) then
self.set("clicked", true)
self:fireEvent("mouse_click", button, x, y)
self:fireEvent("mouse_click", button, self:getRelativePosition(x, y))
return true
end
return false
end
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)
return true
end
self:fireEvent("mouse_release", button, x, y)
self:fireEvent("mouse_release", button, self:getRelativePosition(x, y))
end
function VisualElement:mouse_release()
self.set("clicked", false)
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 VisualElement:handleEvent(event, ...)
if(self[event])then
return self[event](self, ...)
end
function VisualElement:focus()
self:fireEvent("focus")
end
function VisualElement:blur()
self:fireEvent("blur")
self:setCursor(1,1, false)
end
--- Returns the absolute position of the element or the given coordinates.
---@param x? number x position
---@param y? number y position
function VisualElement:getAbsolutePosition(x, y)
if (x == nil) or (y == nil) then
x, y = self.get("x"), self.get("y")
local xPos, yPos = self.get("x"), self.get("y")
if(x ~= nil) then
xPos = xPos + x - 1
end
if(y ~= nil) then
yPos = yPos + y - 1
end
local parent = self.parent
while parent do
local px, py = parent.get("x"), parent.get("y")
x = x + px - 1
y = y + py - 1
xPos = xPos + px - 1
yPos = yPos + py - 1
parent = parent.parent
end
return x, y
return xPos, yPos
end
--- 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)
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
--- @usage element:render()
function VisualElement:render()
if(not self.get("backgroundEnabled"))then
return
end
local width, height = self.get("width"), self.get("height")
self:multiBlit(1, 1, width, height, " ", tHex[self.get("foreground")], tHex[self.get("background")])
end

View File

@@ -95,7 +95,7 @@ function errorHandler.error(errMsg)
file.close()
end
end
term.setBackgroundColor(colors.black)
LOGGER.error(errMsg)
error()

View File

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

View File

@@ -24,4 +24,32 @@ function utils.deepCopy(obj)
return copy
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

View File

@@ -1,5 +1,9 @@
local benchmark = require("benchmark")
benchmark.start("Basalt Initialization")
local elementManager = require("elementManager")
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.
---
@@ -13,24 +17,65 @@ basalt._events = {}
basalt._schedule = {}
basalt._plugins = {}
basalt.LOGGER = require("log")
basalt.path = fs.getDir(select(2, ...))
local mainFrame = nil
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.
--- @shortDescription Creates a new UI element
--- @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
--- @usage local button = basalt.create("Button")
function basalt.create(type, id)
if(id==nil)then id = elementManager.generateId() end
local element = elementManager.getElement(type).new(id, basalt)
local ok, result = pcall(require, "main")
if not ok then
errorManager(false, result)
function basalt.create(type, properties, lazyLoading, parent)
if(_type(properties)=="string")then properties = {name=properties} end
if(properties == nil)then properties = {name = type} end
local elementClass = elementManager.getElement(type)
if(lazyLoading)then
local blueprint = propertySystem.blueprint(elementClass, properties, basalt, parent)
table.insert(lazyElements, blueprint)
queueLazyElements()
return blueprint
else
return elementClass.new(properties, basalt)
end
return element
end
--- Creates and returns a new frame
@@ -87,18 +132,11 @@ end
--- @local Internal event handler
local function updateEvent(event, ...)
if(event=="terminate")then basalt.stop() end
if lazyElementsEventHandler(event, ...) then return end
if event:find("mouse") then
if mainFrame then
mainFrame:handleEvent(event, ...)
end
end
if event:find("key") then
if mainFrame then
mainFrame:handleEvent(event, ...)
end
end
if(mainFrame:dispatchEvent(event, ...))then
return
end
if basalt._events[event] then
for _, callback in ipairs(basalt._events[event]) do
@@ -137,12 +175,14 @@ end
--- @usage basalt.run()
--- @usage basalt.run(false)
function basalt.run(isActive)
benchmark.stop("Basalt Initialization")
updaterActive = isActive
if(isActive==nil)then updaterActive = true end
local function f()
renderFrames()
while updaterActive do
updateEvent(os.pullEventRaw())
renderFrames()
end
end
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 expect = require("libraries/expect")
local errorManager = require("errorManager")
local log = require("log")
--- @class PropertySystem
local PropertySystem = {}
PropertySystem.__index = PropertySystem
PropertySystem._properties = {}
local blueprintTemplates = {}
function PropertySystem.defineProperty(class, name, config)
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)
class["get" .. capitalizedName] = function(self)
class["get" .. capitalizedName] = function(self, ...)
expect(1, self, "element")
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
class["set" .. capitalizedName] = function(self, value)
class["set" .. capitalizedName] = function(self, value, ...)
expect(1, self, "element")
expect(2, value, config.type)
if config.setter then
value = config.setter(self, value)
value = config.setter(self, value, ...)
end
self:_updateProperty(name, value)
return self
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()
self._values = {}
self._observers = {}
self.set = function(name, value)
self.set = function(name, value, ...)
local oldValue = self._values[name]
self._values[name] = value
if(self._properties[name].setter) then
value = self._properties[name].setter(self, value)
end
if oldValue ~= value and self._observers[name] then
for _, callback in ipairs(self._observers[name]) do
callback(self, value, oldValue)
local config = self._properties[name]
if(config~=nil)then
if(config.setter) then
value = config.setter(self, value, ...)
end
if config.canTriggerRender then
self:updateRender()
end
self._values[name] = value
if oldValue ~= value and self._observers[name] then
for _, callback in ipairs(self._observers[name]) do
callback(self, value, oldValue)
end
end
end
end
self.get = function(name)
return self._values[name]
self.get = function(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
local properties = {}
@@ -139,4 +261,25 @@ function PropertySystem:observe(name, callback)
return self
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

View File

@@ -1,7 +1,6 @@
local Render = {}
Render.__index = Render
local colorChars = require("libraries/colorHex")
local log = require("log")
function Render.new(terminal)
local self = setmetatable({}, Render)
@@ -12,19 +11,27 @@ function Render.new(terminal)
text = {},
fg = {},
bg = {},
changed = {}
dirtyRects = {}
}
for y=1, self.height do
self.buffer.text[y] = string.rep(" ", self.width)
self.buffer.fg[y] = string.rep("0", self.width)
self.buffer.bg[y] = string.rep("f", self.width)
self.buffer.changed[y] = false
end
return self
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)
if y < 1 or y > self.height then return self end
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.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.changed[y] = true
self:addDirtyRect(x, y, #text, 1)
return self
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.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.changed[cy] = true
end
end
self:addDirtyRect(x, y, width, height)
return self
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.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
end
@@ -77,7 +84,7 @@ function Render:text(x, y, text)
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.changed[y] = true
self:addDirtyRect(x, y, #text, 1)
return self
end
@@ -86,7 +93,7 @@ function Render:fg(x, y, fg)
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.changed[y] = true
self:addDirtyRect(x, y, #fg, 1)
return self
end
@@ -95,7 +102,7 @@ function Render:bg(x, y, bg)
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.changed[y] = true
self:addDirtyRect(x, y, #bg, 1)
return self
end
@@ -106,23 +113,84 @@ function Render:clear(bg)
self.buffer.text[y] = string.rep(" ", self.width)
self.buffer.fg[y] = string.rep("0", self.width)
self.buffer.bg[y] = string.rep(bgChar, self.width)
self.buffer.changed[y] = true
self:addDirtyRect(1, y, self.width, 1)
end
return self
end
function Render:render()
for y=1, self.height do
if self.buffer.changed[y] then
self.terminal.setCursorPos(1, y)
self.terminal.blit(
self.buffer.text[y],
self.buffer.fg[y],
self.buffer.bg[y]
)
self.buffer.changed[y] = false
local benchmark = require("benchmark")
benchmark.start("render")
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.buffer.text[y]:sub(rect.x, rect.x + rect.width - 1),
self.buffer.fg[y]:sub(rect.x, rect.x + rect.width - 1),
self.buffer.bg[y]:sub(rect.x, rect.x + rect.width - 1)
)
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
end