- 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

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