Add core library and element classes for Basalt framework

This commit is contained in:
Robert Jelic
2025-02-09 13:40:17 +01:00
parent acc5949085
commit 1d31fb8d0c
16 changed files with 1329 additions and 0 deletions

View File

@@ -0,0 +1,91 @@
local PropertySystem = require("propertySystem") -- muss geändert werden.
local BaseElement = setmetatable({}, PropertySystem)
BaseElement.__index = BaseElement
BaseElement._events = {}
BaseElement.defineProperty(BaseElement, "type", {default = "BaseElement", type = "string"})
BaseElement.defineProperty(BaseElement, "eventCallbacks", {default = {}, type = "table"})
function BaseElement.new(id, basalt)
local self = setmetatable({}, BaseElement):__init()
self:init(id, basalt)
self.set("type", "BaseElement")
return self
end
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
function BaseElement.listenTo(class, eventName)
if not class._events then
class._events = {}
end
class._events[eventName] = true
end
function BaseElement:listenEvent(eventName, enable)
enable = enable ~= false
if enable ~= (self._registeredEvents[eventName] or false) then
if enable then
self._registeredEvents[eventName] = true
if self.parent then
self.parent:registerChildEvent(self, eventName)
end
else
self._registeredEvents[eventName] = nil
if self.parent then
self.parent:unregisterChildEvent(self, eventName)
end
end
end
return self
end
function BaseElement:registerCallback(event, callback)
if not self._registeredEvents[event] then
self:listenEvent(event, true)
end
if not self._values.eventCallbacks[event] then
self._values.eventCallbacks[event] = {}
end
table.insert(self._values.eventCallbacks[event], callback)
return self
end
function BaseElement:fireEvent(event, ...)
if self._values.eventCallbacks[event] then
for _, callback in ipairs(self._values.eventCallbacks[event]) do
local result = callback(self, ...)
return result
end
end
return self
end
function BaseElement:updateRender()
if(self.parent) then
self.parent:updateRender()
else
self._renderUpdate = true
end
end
return BaseElement

View File

@@ -0,0 +1,39 @@
local Container = require("elements/Container")
local Render = require("render")
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)
self._renderUpdate = true
local width, height = self.terminal.getSize()
self.set("width", width)
self.set("height", height)
self.set("background", colors.red)
self.set("type", "BaseFrame")
return self
end
function BaseFrame:multiBlit(x, y, width, height, text, fg, bg)
self._render:multiBlit(x, y, width, height, text, fg, bg)
end
function BaseFrame:textFg(x, y, text, fg)
self._render:textFg(x, y, text, fg)
end
---@diagnostic disable-next-line: duplicate-set-field
function BaseFrame:render()
if(self._renderUpdate) then
Container.render(self)
self._render:render()
self._renderUpdate = false
end
end
return BaseFrame

29
src/elements/Button.lua Normal file
View File

@@ -0,0 +1,29 @@
local VisualElement = require("elements/VisualElement")
local getCenteredPosition = require("libraries/utils").getCenteredPosition
local Button = setmetatable({}, VisualElement)
Button.__index = Button
Button.defineProperty(Button, "text", {default = "Button", type = "string"})
Button.listenTo(Button, "mouse_click")
---@diagnostic disable-next-line: duplicate-set-field
function Button.new(id, basalt)
local self = setmetatable({}, Button):__init()
self:init(id, basalt)
self.set("type", "Button")
self.set("width", 10)
self.set("height", 3)
self.set("z", 5)
return self
end
---@diagnostic disable-next-line: duplicate-set-field
function Button:render()
VisualElement.render(self)
local text = self.get("text")
local xO, yO = getCenteredPosition(text, self.get("width"), self.get("height"))
self:textFg(xO, yO, text, self.get("foreground"))
end
return Button

208
src/elements/Container.lua Normal file
View File

@@ -0,0 +1,208 @@
local VisualElement = require("elements/VisualElement")
local elementManager = require("elementManager")
local expect = require("libraries/expect")
local max = math.max
local Container = setmetatable({}, VisualElement)
Container.__index = Container
Container.defineProperty(Container, "children", {default = {}, type = "table"})
Container.defineProperty(Container, "childrenEvents", {default = {}, type = "table"})
Container.defineProperty(Container, "eventListenerCount", {default = {}, type = "table"})
for k, _ in pairs(elementManager:getElementList()) do
local capitalizedName = k:sub(1,1):upper() .. k:sub(2)
--if not 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
end
---@diagnostic disable-next-line: duplicate-set-field
function Container.new(id, basalt)
local self = setmetatable({}, Container):__init()
self:init(id, basalt)
self.set("type", "Container")
return self
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)
child.parent = self
self:registerChildrenEvents(child)
return self
end
function Container:sortChildren()
table.sort(self._values.children, function(a, b)
return a.get("z") < b.get("z")
end)
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)
end
end
function Container:registerChildrenEvents(child)
for event in pairs(child._registeredEvents) do
self:registerChildEvent(child, event)
end
end
function Container:registerChildEvent(child, eventName)
if not self._values.childrenEvents[eventName] then
self._values.childrenEvents[eventName] = {}
self._values.eventListenerCount[eventName] = 0
if self.parent then
self.parent:registerChildEvent(self, eventName)
end
end
for _, registeredChild in ipairs(self._values.childrenEvents[eventName]) do
if registeredChild == child then
return
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._values.eventListenerCount[eventName] = self._values.eventListenerCount[eventName] + 1
end
function Container:removeChildrenEvents(child)
for event in pairs(child._registeredEvents) do
self:unregisterChildEvent(child, event)
end
end
function Container:unregisterChildEvent(child, eventName)
if self._values.childrenEvents[eventName] then
for i, listener in ipairs(self._values.childrenEvents[eventName]) do
if listener == child then
table.remove(self._values.childrenEvents[eventName], i)
self._values.eventListenerCount[eventName] = self._values.eventListenerCount[eventName] - 1
if self._values.eventListenerCount[eventName] <= 0 then
self._values.childrenEvents[eventName] = nil
self._values.eventListenerCount[eventName] = nil
if self.parent then
self.parent:unregisterChildEvent(self, eventName)
end
end
break
end
end
end
end
function Container:removeChild(child)
for i,v in ipairs(self.children) do
if v == child then
table.remove(self._values.children, i)
child.parent = nil
break
end
end
return self
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
end
end
--[[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
end
end
end]]
function Container:multiBlit(x, y, width, height, text, fg, bg)
local w, h = self.get("width"), self.get("height")
width = x < 1 and math.min(width + x - 1, w) or math.min(width, math.max(0, w - x + 1))
height = y < 1 and math.min(height + y - 1, h) or math.min(height, math.max(0, h - y + 1))
if width <= 0 or height <= 0 then return end
VisualElement.multiBlit(self, math.max(1, x), math.max(1, y), width, height, text, fg, bg)
end
function Container:textFg(x, y, text, fg)
local w, h = self.get("width"), self.get("height")
if y < 1 or y > h then return end
local textStart = x < 1 and (2 - x) or 1
local textLen = math.min(#text - textStart + 1, w - math.max(1, x) + 1)
if textLen <= 0 then return end
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 child == self then
self.basalt.LOGGER.error("CIRCULAR REFERENCE DETECTED!")
return
end
child:render()
end
end
return Container

18
src/elements/Frame.lua Normal file
View File

@@ -0,0 +1,18 @@
local Container = require("elements/Container")
local Frame = setmetatable({}, Container)
Frame.__index = Frame
---@diagnostic disable-next-line: duplicate-set-field
function Frame.new(id, basalt)
local self = setmetatable({}, Frame):__init()
self:init(id, 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
return Frame

View File

@@ -0,0 +1,123 @@
local BaseElement = require("elements/BaseElement")
local VisualElement = setmetatable({}, BaseElement)
VisualElement.__index = VisualElement
local tHex = require("libraries/colorHex")
BaseElement.defineProperty(VisualElement, "x", {default = 1, type = "number", canTriggerRender = true})
BaseElement.defineProperty(VisualElement, "y", {default = 1, type = "number", canTriggerRender = true})
BaseElement.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
self.parent:sortChildren()
end
return value
end})
BaseElement.defineProperty(VisualElement, "width", {default = 1, type = "number", canTriggerRender = true})
BaseElement.defineProperty(VisualElement, "height", {default = 1, type = "number", canTriggerRender = true})
BaseElement.defineProperty(VisualElement, "background", {default = colors.black, type = "number", canTriggerRender = true})
BaseElement.defineProperty(VisualElement, "foreground", {default = colors.white, type = "number", canTriggerRender = true})
BaseElement.defineProperty(VisualElement, "clicked", {default = false, type = "boolean"})
---@diagnostic disable-next-line: duplicate-set-field
function VisualElement.new(id, basalt)
local self = setmetatable({}, VisualElement):__init()
self:init(id, basalt)
self.set("type", "VisualElement")
return self
end
function VisualElement:multiBlit(x, y, width, height, text, fg, bg)
x = x + self.get("x") - 1
y = y + self.get("y") - 1
self.parent:multiBlit(x, y, width, height, text, fg, bg)
end
function VisualElement:textFg(x, y, text, fg)
x = x + self.get("x") - 1
y = y + self.get("y") - 1
self.parent:textFg(x, y, text, fg)
end
function VisualElement:isInBounds(x, y)
local xPos, yPos = self.get("x"), self.get("y")
local width, height = self.get("width"), self.get("height")
return x >= xPos and x <= xPos + width - 1 and
y >= yPos and y <= yPos + height - 1
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)
return true
end
end
function VisualElement:mouse_up(button, x, y)
if self:isInBounds(x, y) then
self.set("clicked", false)
self:fireEvent("mouse_up", button, x, y)
return true
end
self:fireEvent("mouse_release", button, x, y)
end
function VisualElement:mouse_release()
self.set("clicked", false)
end
function VisualElement:handleEvent(event, ...)
if(self[event])then
return self[event](self, ...)
end
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")
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
parent = parent.parent
end
return x, y
end
--- Returns the relative position of the element or the given coordinates.
---@param x? number -- x position
---@param y? number -- y position
---@return number, number
function VisualElement:getRelativePosition(x, y)
if (x == nil) or (y == nil) then
x, y = self.get("x"), self.get("y")
end
local parentX, parentY = 1, 1
if self.parent then
parentX, parentY = self.parent:getRelativePosition()
end
local elementX = self.get("x")
local elementY = self.get("y")
return x - (elementX - 1) - (parentX - 1),
y - (elementY - 1) - (parentY - 1)
end
---@diagnostic disable-next-line: duplicate-set-field
function VisualElement:render()
local width, height = self.get("width"), self.get("height")
self:multiBlit(1, 1, width, height, " ", tHex[self.get("foreground")], tHex[self.get("background")])
end
return VisualElement