- added List

- added checkbox
- added program
- added slider
- added progressbar
- added reactive (dynamicValues)
smaller bug fixxes
This commit is contained in:
Robert Jelic
2025-02-14 14:40:20 +01:00
parent 6dfa554523
commit b7f22bf63f
22 changed files with 1021 additions and 76 deletions

3
.gitignore vendored
View File

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

View File

@@ -76,8 +76,9 @@ function ElementManager.loadElement(name)
end
if(type(hooks)=="function")then
element[methodName] = function(self, ...)
original(self, ...)
return hooks(self, ...)
local result = original(self, ...)
local hookResult = hooks(self, ...)
return hookResult == nil and result or hookResult
end
elseif(type(hooks)=="table")then
element[methodName] = function(self, ...)

View File

@@ -167,6 +167,17 @@ function BaseElement:handleEvent(event, ...)
return true
end
function BaseElement:getBaseFrame()
if self.parent then
return self.parent:getBaseFrame()
end
return self
end
function BaseElement:destroy()
end
--- Requests a render update for this element
--- @usage element:updateRender()
function BaseElement:updateRender()

View File

@@ -40,6 +40,14 @@ function BaseFrame:textFg(x, y, text, fg)
self._render:textFg(x, y, text, fg)
end
function BaseFrame:textBg(x, y, text, bg)
self._render:textBg(x, y, text, bg)
end
function BaseFrame:blit(x, y, text, fg, bg)
self._render:blit(x, y, text, fg, bg)
end
function BaseFrame:setCursor(x, y, blink)
local term = self.get("term")
self._render:setCursor(x, y, blink)

49
src/elements/Checkbox.lua Normal file
View File

@@ -0,0 +1,49 @@
local VisualElement = require("elements/VisualElement")
---@class Checkbox : VisualElement
local Checkbox = setmetatable({}, VisualElement)
Checkbox.__index = Checkbox
---@property checked boolean Whether checkbox is checked
Checkbox.defineProperty(Checkbox, "checked", {default = false, type = "boolean", canTriggerRender = true})
---@property text string Label text
Checkbox.defineProperty(Checkbox, "text", {default = "", type = "string", canTriggerRender = true})
---@property symbol string Check symbol
Checkbox.defineProperty(Checkbox, "symbol", {default = "x", type = "string"})
Checkbox.listenTo(Checkbox, "mouse_click")
function Checkbox.new(props, basalt)
local self = setmetatable({}, Checkbox):__init()
self:init(props, basalt)
self.set("width", 1)
self.set("height", 1)
return self
end
function Checkbox:init(props, basalt)
VisualElement.init(self, props, basalt)
self.set("type", "Checkbox")
end
function Checkbox:mouse_click(button, x, y)
if VisualElement.mouse_click(self, button, x, y) then
self.set("checked", not self.get("checked"))
self:fireEvent("change", self.get("checked"))
return true
end
end
function Checkbox:render()
VisualElement.render(self)
local text = self.get("checked") and self.get("symbol") or " "
self:textFg(1, 1, "["..text.."]", self.get("foreground"))
local label = self.get("text")
if #label > 0 then
self:textFg(4, 1, label, self.get("foreground"))
end
end
return Checkbox

View File

@@ -305,6 +305,25 @@ 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
function Container:blit(x, y, text, fg, bg)
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)
local fgLen = math.min(#fg - textStart + 1, w - math.max(1, x) + 1)
local bgLen = math.min(#bg - textStart + 1, w - math.max(1, x) + 1)
if textLen <= 0 then return end
local finalText = text:sub(textStart, textStart + textLen - 1)
local finalFg = fg:sub(textStart, textStart + fgLen - 1)
local finalBg = bg:sub(textStart, textStart + bgLen - 1)
VisualElement.blit(self, math.max(1, x), math.max(1, y), finalText, finalFg, finalBg)
end
function Container:render()
VisualElement.render(self)
if not self.get("childrenSorted")then

View File

@@ -1,4 +1,5 @@
local VisualElement = require("elements/VisualElement")
local tHex = require("libraries/colorHex")
---@class Input : VisualElement
local Input = setmetatable({}, VisualElement)
@@ -10,19 +11,26 @@ Input.defineProperty(Input, "text", {default = "", type = "string", canTriggerRe
---@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"})
---@property viewOffset number Input - offset of view
Input.defineProperty(Input, "viewOffset", {default = 0, type = "number", canTriggerRender = true})
-- Neue Properties
Input.defineProperty(Input, "maxLength", {default = nil, type = "number"})
Input.defineProperty(Input, "placeholder", {default = "asd", type = "string"})
Input.defineProperty(Input, "placeholderColor", {default = colors.gray, type = "number"})
Input.defineProperty(Input, "focusedColor", {default = colors.blue, type = "number"})
Input.defineProperty(Input, "pattern", {default = nil, type = "string"})
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 props table The properties to initialize the element with
--- @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)
function Input.new(props, basalt)
local self = setmetatable({}, Input):__init()
self:init(id, basalt)
self.set("width", 8)
@@ -30,8 +38,8 @@ function Input.new(id, basalt)
return self
end
function Input:init(id, basalt)
VisualElement.init(self, id, basalt)
function Input:init(props, basalt)
VisualElement.init(self, props, basalt)
self.set("type", "Input")
end
@@ -39,8 +47,15 @@ function Input:char(char)
if not self.get("focused") then return end
local text = self.get("text")
local pos = self.get("cursorPos")
local maxLength = self.get("maxLength")
local pattern = self.get("pattern")
if maxLength and #text >= maxLength then return end
if pattern and not char:match(pattern) then return end
self.set("text", text:sub(1, pos-1) .. char .. text:sub(pos))
self.set("cursorPos", pos + 1)
self:updateRender()
self:updateViewport()
end
@@ -48,64 +63,91 @@ function Input:key(key)
if not self.get("focused") then return end
local pos = self.get("cursorPos")
local text = self.get("text")
local viewOffset = self.get("viewOffset")
local width = self.get("width")
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)
if key == keys.left then
if pos > 1 then
self.set("cursorPos", pos - 1)
if pos - 1 <= viewOffset then
self.set("viewOffset", math.max(0, pos - 2))
end
end
elseif key == keys.right then
if pos <= #text then
self.set("cursorPos", pos + 1)
if pos - viewOffset >= width then
self.set("viewOffset", pos - width + 1)
end
end
elseif key == keys.backspace then
if pos > 1 then
self.set("text", text:sub(1, pos-2) .. text:sub(pos))
self.set("cursorPos", pos - 1)
self:updateRender()
self:updateViewport()
end
end
self:updateViewport()
local relativePos = self.get("cursorPos") - self.get("viewOffset")
self:setCursor(relativePos, 1, true)
end
function Input:focus()
VisualElement.focus(self)
self.set("background", colors.blue)
self:setCursor(1,1, true)
self:updateRender()
end
function Input:blur()
VisualElement.blur(self)
self.set("background", colors.green)
self:updateRender()
end
function Input:mouse_click(button, x, y)
if VisualElement.mouse_click(self, button, x, y) then
local relX, relY = self:getRelativePosition(x, y)
local text = self.get("text")
self:setCursor(math.min(relX, #text + 1), relY, true)
self:set("cursorPos", relX + self.get("viewOffset"))
return true
end
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
local textLength = #self.get("text")
if cursorPos - viewOffset > width then
self.set("viewOffset", cursorPos - width)
elseif cursorPos <= viewOffset then
self.set("viewOffset", math.max(0, cursorPos - 1))
end
-- Wenn Cursor außerhalb des sichtbaren Bereichs nach links
if cursorPos <= viewOffset then
self.set("viewOffset", cursorPos - 1)
if viewOffset > textLength - width then
self.set("viewOffset", math.max(0, textLength - width))
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 placeholder = self.get("placeholder")
local focusedColor = self.get("focusedColor")
local focused = self.get("focused")
local width, height = self.get("width"), self.get("height")
self:multiBlit(1, 1, width, height, " ", tHex[self.get("foreground")], tHex[focused and focusedColor or self.get("background")])
if #text == 0 and #placeholder ~= 0 and self.get("focused") == false then
self:textFg(1, 1, placeholder:sub(1, width), self.get("placeholderColor"))
return
end
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

View File

@@ -12,7 +12,7 @@ Label.defineProperty(Label, "text", {default = "Label", type = "string", setter
end})
--- Creates a new Label instance
--- @param name table The properties to initialize the element with
--- @param props 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)

103
src/elements/List.lua Normal file
View File

@@ -0,0 +1,103 @@
local VisualElement = require("elements/VisualElement")
---@class List : VisualElement
local List = setmetatable({}, VisualElement)
List.__index = List
---@property items table List of items to display
List.defineProperty(List, "items", {default = {}, type = "table", canTriggerRender = true})
---@property selectedIndex number Currently selected item index
List.defineProperty(List, "selectedIndex", {default = 0, type = "number", canTriggerRender = true})
---@property selectable boolean Whether items can be selected
List.defineProperty(List, "selectable", {default = true, type = "boolean"})
---@property offset number Scrolling offset
List.defineProperty(List, "offset", {default = 0, type = "number", canTriggerRender = true})
---@property selectedColor color Color for selected item
List.defineProperty(List, "selectedColor", {default = colors.blue, type = "number"})
List.listenTo(List, "mouse_click")
List.listenTo(List, "mouse_scroll")
function List.new(props, basalt)
local self = setmetatable({}, List):__init()
self:init(props, basalt)
self.set("width", 16)
self.set("height", 8)
return self
end
function List:init(props, basalt)
VisualElement.init(self, props, basalt)
self.set("type", "List")
end
function List:addItem(text)
local items = self.get("items")
table.insert(items, text)
self:updateRender()
return self
end
function List:removeItem(index)
local items = self.get("items")
table.remove(items, index)
self:updateRender()
return self
end
function List:clear()
self.set("items", {})
self.set("selectedIndex", 0)
self:updateRender()
return self
end
function List:mouse_click(button, x, y)
if button == 1 and self:isInBounds(x, y) and self.get("selectable") then
local relY = self:getRelativePosition(x, y)
local index = relY + self.get("offset")
if index <= #self.get("items") then
self.set("selectedIndex", index)
self:fireEvent("select", index, self.get("items")[index])
self:updateRender()
return true
end
end
end
function List:mouse_scroll(direction, x, y)
if self:isInBounds(x, y) then
local offset = self.get("offset")
local maxOffset = math.max(0, #self.get("items") - self.get("height"))
offset = math.min(maxOffset, math.max(0, offset + direction))
self.set("offset", offset)
return true
end
end
function List:render()
VisualElement.render(self)
local items = self.get("items")
local height = self.get("height")
local offset = self.get("offset")
local selected = self.get("selectedIndex")
for i = 1, height do
local itemIndex = i + offset
local item = items[itemIndex]
if item then
if itemIndex == selected then
self:textBg(1, i, string.rep(" ", self.get("width")), self.get("selectedColor"))
self:textFg(1, i, item, colors.white)
else
self:textFg(1, i, item, self.get("foreground"))
end
end
end
end
return List

179
src/elements/Program.lua Normal file
View File

@@ -0,0 +1,179 @@
local elementManager = require("elementManager")
local VisualElement = elementManager.getElement("VisualElement")
local errorManager = require("errorManager")
--TODO:
-- Rendering optimization (only render when screen changed)
-- Eventsystem improvement
-- Cursor is sometimes not visible on time
---@class Program : VisualElement
local Program = setmetatable({}, VisualElement)
Program.__index = Program
Program.defineProperty(Program, "program", {default = nil, type = "table"})
Program.defineProperty(Program, "path", {default = "", type = "string"})
Program.defineProperty(Program, "running", {default = false, type = "boolean"})
Program.listenTo(Program, "key")
Program.listenTo(Program, "char")
Program.listenTo(Program, "key_up")
Program.listenTo(Program, "paste")
Program.listenTo(Program, "mouse_click")
Program.listenTo(Program, "mouse_drag")
Program.listenTo(Program, "mouse_scroll")
Program.listenTo(Program, "mouse_up")
local BasaltProgram = {}
BasaltProgram.__index = BasaltProgram
local newPackage = dofile("rom/modules/main/cc/require.lua").make
function BasaltProgram.new()
local self = setmetatable({}, BasaltProgram)
self.env = {}
self.args = {}
return self
end
function BasaltProgram:run(path, width, height)
self.window = window.create(term.current(), 1, 1, width, height, false)
local pPath = shell.resolveProgram(path)
if(pPath~=nil)then
if(fs.exists(pPath)) then
local file = fs.open(pPath, "r")
local content = file.readAll()
file.close()
local env = setmetatable(self.env, {__index=_ENV})
env.shell = shell
env.term = self.window
env.require, env.package = newPackage(env, fs.getDir(pPath))
env.term.current = term.current
env.term.redirect = term.redirect
env.term.native = term.native
self.coroutine = coroutine.create(function()
local program = load(content, path, "bt", env)
if program then
local current = term.current()
term.redirect(self.window)
local result = program(path, table.unpack(self.args))
term.redirect(current)
return result
end
end)
local current = term.current()
term.redirect(self.window)
local ok, result = coroutine.resume(self.coroutine)
term.redirect(current)
if not ok then
errorManager.header = "Basalt Program Error ".. path
errorManager.error(result)
end
else
errorManager.header = "Basalt Program Error ".. path
errorManager.error("File not found")
end
else
errorManager.header = "Basalt Program Error"
errorManager.error("Program "..path.." not found")
end
end
function BasaltProgram:resize(width, height)
self.window.reposition(1, 1, width, height)
end
function BasaltProgram:resume(event, ...)
if self.coroutine==nil or coroutine.status(self.coroutine)=="dead" then return end
if(self.filter~=nil)then
if(event~=self.filter)then return end
self.filter=nil
end
local current = term.current()
term.redirect(self.window)
local ok, result = coroutine.resume(self.coroutine, event, ...)
term.redirect(current)
if ok then
self.filter = result
else
errorManager.header = "Basalt Program Error"
errorManager.error(result)
end
return ok, result
end
function BasaltProgram:stop()
end
--- Creates a new Program instance
--- @param props table The properties to initialize the element with
--- @param basalt table The basalt instance
--- @return Program object The newly created Program instance
--- @usage local element = Program.new("myId", basalt)
function Program.new(props, basalt)
local self = setmetatable({}, Program):__init()
self.set("z", 5)
self.set("width", 30)
self.set("height", 12)
self:init(props, basalt)
return self
end
function Program:init(props, basalt)
VisualElement.init(self, props, basalt)
self.set("type", "Program")
end
function Program:execute(path)
self.set("path", path)
self.set("running", true)
local program = BasaltProgram.new()
self.set("program", program)
program:run(path, self.get("width"), self.get("height"))
self:updateRender()
return self
end
function Program:dispatchEvent(event, ...)
local program = self.get("program")
local result = VisualElement.dispatchEvent(self, event, ...)
if program then
program:resume(event, ...)
if(self.get("focused"))then
local cursorBlink = program.window.getCursorBlink()
local cursorX, cursorY = program.window.getCursorPos()
self:setCursor(cursorX, cursorY, cursorBlink)
end
self:updateRender()
end
return result
end
function Program:focus()
if(VisualElement.focus(self))then
local program = self.get("program")
if program then
local cursorBlink = program.window.getCursorBlink()
local cursorX, cursorY = program.window.getCursorPos()
self:setCursor(cursorX, cursorY, cursorBlink)
end
end
end
function Program:render()
VisualElement.render(self)
local program = self.get("program")
if program then
local _, height = program.window.getSize()
for y = 1, height do
local text, fg, bg = program.window.getLine(y)
if text then
self:blit(1, y, text, fg, bg)
end
end
end
end
return Program

View File

@@ -0,0 +1,42 @@
local VisualElement = require("elements/VisualElement")
---@class ProgressBar : VisualElement
local ProgressBar = setmetatable({}, VisualElement)
ProgressBar.__index = ProgressBar
---@property progress number Current progress (0-100)
ProgressBar.defineProperty(ProgressBar, "progress", {default = 0, type = "number", canTriggerRender = true})
---@property showPercentage boolean Show percentage text
ProgressBar.defineProperty(ProgressBar, "showPercentage", {default = false, type = "boolean"})
---@property progressColor color Progress bar color
ProgressBar.defineProperty(ProgressBar, "progressColor", {default = colors.lime, type = "number"})
function ProgressBar.new(props, basalt)
local self = setmetatable({}, ProgressBar):__init()
self:init(props, basalt)
self.set("width", 10)
self.set("height", 1)
return self
end
function ProgressBar:init(props, basalt)
VisualElement.init(self, props, basalt)
self.set("type", "ProgressBar")
end
function ProgressBar:render()
VisualElement.render(self)
local width = self.get("width")
local progress = math.min(100, math.max(0, self.get("progress")))
local fillWidth = math.floor((width * progress) / 100)
self:textBg(1, 1, string.rep(" ", fillWidth), self.get("progressColor"))
if self.get("showPercentage") then
local text = tostring(progress).."%"
local x = math.floor((width - #text) / 2) + 1
self:textFg(x, 1, text, self.get("foreground"))
end
end
return ProgressBar

87
src/elements/Slider.lua Normal file
View File

@@ -0,0 +1,87 @@
local VisualElement = require("elements/VisualElement")
---@class Slider : VisualElement
local Slider = setmetatable({}, VisualElement)
Slider.__index = Slider
---@property step number 1 Current step position (1 to width/height)
Slider.defineProperty(Slider, "step", {default = 1, type = "number", canTriggerRender = true})
---@property max number 100 Maximum value for value conversion
Slider.defineProperty(Slider, "max", {default = 100, type = "number"})
---@property horizontal boolean true Whether the slider is horizontal
Slider.defineProperty(Slider, "horizontal", {default = true, type = "boolean", canTriggerRender = true})
---@property barColor color color Colors for the slider bar
Slider.defineProperty(Slider, "barColor", {default = colors.gray, type = "number", canTriggerRender = true})
---@property sliderColor color The color of the slider handle
Slider.defineProperty(Slider, "sliderColor", {default = colors.blue, type = "number", canTriggerRender = true})
Slider.listenTo(Slider, "mouse_click")
Slider.listenTo(Slider, "mouse_drag")
Slider.listenTo(Slider, "mouse_up")
function Slider.new(props, basalt)
local self = setmetatable({}, Slider):__init()
self:init(props, basalt)
self.set("width", 8)
self.set("height", 1)
self.set("backgroundEnabled", false)
return self
end
function Slider:init(props, basalt)
VisualElement.init(self, props, basalt)
self.set("type", "Slider")
end
function Slider:getValue()
local step = self.get("step")
local max = self.get("max")
local maxSteps = self.get("horizontal") and self.get("width") or self.get("height")
return math.floor((step - 1) * (max / (maxSteps - 1)))
end
function Slider:mouse_click(button, x, y)
if button == 1 and self:isInBounds(x, y) then
local relX, relY = self:getRelativePosition(x, y)
local pos = self.get("horizontal") and relX or relY
local maxSteps = self.get("horizontal") and self.get("width") or self.get("height")
self.set("step", math.min(maxSteps, math.max(1, pos)))
self:updateRender()
return true
end
end
Slider.mouse_drag = Slider.mouse_click
function Slider:mouse_scroll(direction, x, y)
if self:isInBounds(x, y) then
local step = self.get("step")
local maxSteps = self.get("horizontal") and self.get("width") or self.get("height")
self.set("step", math.min(maxSteps, math.max(1, step + direction)))
self:updateRender()
return true
end
end
function Slider:render()
VisualElement.render(self)
local width = self.get("width")
local height = self.get("height")
local horizontal = self.get("horizontal")
local step = self.get("step")
local barChar = horizontal and "\140" or ""
local text = string.rep(barChar, horizontal and width or height)
if horizontal then
self:textFg(1, 1, text, self.get("barColor"))
self:textBg(step, 1, " ", self.get("sliderColor"))
else
for y = 1, height do
self:textFg(1, y, barChar, self.get("barColor"))
end
self:textFg(1, step, "\140", self.get("sliderColor"))
end
end
return Slider

View File

@@ -1,12 +1,12 @@
local elementManager = require("elementManager")
local BaseElement = elementManager.getElement("BaseElement")
local tHex = require("libraries/colorHex")
---@alias color number
---@class VisualElement : BaseElement
local VisualElement = setmetatable({}, BaseElement)
VisualElement.__index = VisualElement
local tHex = require("libraries/colorHex")
---@property x number 1 x position of the element
VisualElement.defineProperty(VisualElement, "x", {default = 1, type = "number", canTriggerRender = true})
@@ -56,6 +56,8 @@ end})
VisualElement.listenTo(VisualElement, "focus")
VisualElement.listenTo(VisualElement, "blur")
local max, min = math.max, math.min
--- Creates a new VisualElement instance
--- @param props table The properties to initialize the element with
--- @param basalt table The basalt instance
@@ -98,6 +100,18 @@ function VisualElement:textFg(x, y, text, fg)
self.parent:textFg(x, y, text, fg)
end
function VisualElement:textBg(x, y, text, bg)
x = x + self.get("x") - 1
y = y + self.get("y") - 1
self.parent:textBg(x, y, text, bg)
end
function VisualElement:blit(x, y, text, fg, bg)
x = x + self.get("x") - 1
y = y + self.get("y") - 1
self.parent:blit(x, y, text, fg, bg)
end
--- Checks if the specified coordinates are within the bounds of the element
--- @param x number The x position to check
--- @param y number The y position to check
@@ -193,6 +207,7 @@ end
function VisualElement:setCursor(x, y, blink)
if self.parent then
local absX, absY = self:getAbsolutePosition(x, y)
absX = max(self.get("x"), min(absX, self.get("width") + self.get("x") - 1))
return self.parent:setCursor(absX, absY, blink)
end
end

View File

@@ -12,6 +12,9 @@ local function coloredPrint(message, color)
end
function errorHandler.error(errMsg)
if errorHandler.errorHandled then
error()
end
term.setBackgroundColor(colors.black)
term.clear()
@@ -98,6 +101,7 @@ function errorHandler.error(errMsg)
term.setBackgroundColor(colors.black)
LOGGER.error(errMsg)
errorHandler.errorHandled = true
error()
end

View File

@@ -13,7 +13,6 @@ local function errorHandler(err)
errorManager.error(err)
end
-- Use xpcall with error handler
local ok, result = pcall(require, "main")
package.path = defaultPath

View File

@@ -28,6 +28,7 @@ local lazyElementCount = 10
local lazyElementsTimer = 0
local isLazyElementsTimerActive = false
local function queueLazyElements()
if(isLazyElementsTimerActive)then return end
lazyElementsTimer = os.startTimer(0.2)
@@ -134,9 +135,11 @@ local function updateEvent(event, ...)
if(event=="terminate")then basalt.stop() end
if lazyElementsEventHandler(event, ...) then return end
if(mainFrame:dispatchEvent(event, ...))then
return
end
if(mainFrame)then
if(mainFrame:dispatchEvent(event, ...))then
return
end
end
if basalt._events[event] then
for _, callback in ipairs(basalt._events[event]) do

View File

@@ -312,6 +312,7 @@ function VisualElement.hooks.dispatchEvent(self, event, ...)
end
function VisualElement.setup(element)
VisualElementBaseDispatchEvent = element.dispatchEvent
element.defineProperty(element, "animation", {default = nil, type = "table"})
element.listenTo(element, "timer")
end

View File

@@ -1,32 +1,173 @@
local function setupReactiveProperty(element, propertyName, expression)
local errorManager = require("errorManager")
local PropertySystem = require("propertySystem")
local log = require("log")
end
local protectedNames = {
colors = true,
math = true,
clamp = true,
round = true
}
local function createReactiveFunction(expression, scope)
local code = expression:gsub(
"(%w+)%s*%?%s*([^:]+)%s*:%s*([^}]+)",
"%1 and %2 or %3"
)
local mathEnv = {
clamp = function(val, min, max)
return math.min(math.max(val, min), max)
end,
round = function(val)
return math.floor(val + 0.5)
end
}
return load(string.format([[
return function(self)
return %s
local function parseExpression(expr, element)
expr = expr:gsub("^{(.+)}$", "%1")
for k,v in pairs(colors) do
if type(k) == "string" then
expr = expr:gsub("%f[%w]"..k.."%f[%W]", "colors."..k)
end
]], code), "reactive", "t", scope)()
end
expr = expr:gsub("([%w_]+)%.([%w_]+)", function(obj, prop)
if protectedNames[obj] then
return obj.."."..prop
end
return string.format('__getProperty("%s", "%s")', obj, prop)
end)
local env = setmetatable({
colors = colors,
math = math,
__getProperty = function(objName, propName)
if objName == "self" then
return element.get(propName)
elseif objName == "parent" then
return element.parent.get(propName)
else
local target = element:getBaseFrame():getChild(objName)
if not target then
errorManager.header = "Reactive evaluation error"
errorManager.error("Could not find element: " .. objName)
return nil
end
return target.get(propName)
end
end
}, { __index = mathEnv })
local func, err = load("return "..expr, "reactive", "t", env)
if not func then
errorManager.header = "Reactive evaluation error"
errorManager.error("Invalid expression: " .. err)
return function() return nil end
end
return func
end
local function validateReferences(expr, element)
for ref in expr:gmatch("([%w_]+)%.") do
if not protectedNames[ref] then
if ref == "parent" then
if not element.parent then
errorManager.header = "Reactive evaluation error"
errorManager.error("No parent element available")
return false
end
else
local target = element:getBaseFrame():getChild(ref)
if not target then
errorManager.header = "Reactive evaluation error"
errorManager.error("Referenced element not found: " .. ref)
return false
end
end
end
end
return true
end
local functionCache = {}
local observerCache = setmetatable({}, {__mode = "k"})
local function setupObservers(element, expr)
if observerCache[element] then
for _, observer in ipairs(observerCache[element]) do
observer.target:removeObserver(observer.property, observer.callback)
end
end
local observers = {}
for ref, prop in expr:gmatch("([%w_]+)%.([%w_]+)") do
if not protectedNames[ref] then
local target
if ref == "self" then
target = element
elseif ref == "parent" then
target = element.parent
else
target = element:getBaseFrame():getChild(ref)
end
if target then
local observer = {
target = target,
property = prop,
callback = function()
element:updateRender()
end
}
target:observe(prop, observer.callback)
table.insert(observers, observer)
end
end
end
observerCache[element] = observers
end
PropertySystem.addSetterHook(function(element, propertyName, value, config)
if type(value) == "string" and value:match("^{.+}$") then
local expr = value:gsub("^{(.+)}$", "%1")
if not validateReferences(expr, element) then
return config.default
end
setupObservers(element, expr)
if not functionCache[value] then
local parsedFunc = parseExpression(value, element)
functionCache[value] = parsedFunc
end
return function(self)
local success, result = pcall(functionCache[value])
if not success then
errorManager.header = "Reactive evaluation error"
if type(result) == "string" then
errorManager.error("Error evaluating expression: " .. result)
else
errorManager.error("Error evaluating expression")
end
return config.default
end
return result
end
end
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
BaseElement.hooks = {
destroy = function(self)
if observerCache[self] then
for _, observer in ipairs(observerCache[self]) do
observer.target:observe(observer.property, observer.callback)
end
observerCache[self] = nil
end
end
}
return {
BaseElement = BaseElement

99
src/plugins/theme.lua Normal file
View File

@@ -0,0 +1,99 @@
-- Has to be reworked
local Theme = {}
local defaultTheme = {
colors = {
primary = colors.blue,
secondary = colors.cyan,
background = colors.black,
text = colors.white,
borders = colors.gray,
error = colors.red,
success = colors.green,
},
elementStyles = {
Button = {
background = "background",
foreground = "text",
activeBackground = "primary",
activeForeground = "text",
},
Input = {
background = "background",
foreground = "text",
focusBackground = "primary",
focusForeground = "text",
},
Frame = {
background = "background",
foreground = "text"
}
}
}
local themes = {
default = defaultTheme
}
local currentTheme = "default"
function Theme.registerTheme(name, theme)
themes[name] = theme
end
local function resolveThemeValue(value, theme)
if type(value) == "string" and theme.colors[value] then
return theme.colors[value]
end
return value
end
local BaseElement = {
hooks = {
init = function(self)
-- Theme Properties für das Element registrieren
self.defineProperty(self, "theme", {
default = currentTheme,
type = "string",
setter = function(self, value)
self:applyTheme(value)
return value
end
})
end
}
}
function BaseElement:applyTheme(themeName)
local theme = themes[themeName] or themes.default
local elementType = self.get("type")
if theme.elementStyles[elementType] then
local styles = theme.elementStyles[elementType]
for prop, value in pairs(styles) do
if self:getPropertyConfig(prop) then
self.set(prop, resolveThemeValue(value, theme))
end
end
end
end
local Container = {
hooks = {
addChild = function(self, child)
if self.get("themeInherit") then
child.set("theme", self.get("theme"))
end
end
}
}
function Container.setup(element)
element.defineProperty(element, "themeInherit", {default = true, type = "boolean"})
end
return {
BaseElement = BaseElement,
Container = Container,
}

View File

@@ -10,6 +10,12 @@ PropertySystem.__index = PropertySystem
PropertySystem._properties = {}
local blueprintTemplates = {}
PropertySystem._setterHooks = {}
function PropertySystem.addSetterHook(hook)
table.insert(PropertySystem._setterHooks, hook)
end
function PropertySystem.defineProperty(class, name, config)
if not rawget(class, '_properties') then
class._properties = {}
@@ -28,15 +34,32 @@ function PropertySystem.defineProperty(class, name, config)
class["get" .. capitalizedName] = function(self, ...)
expect(1, self, "element")
local value = self._values[name]
if type(value) == "function" and config.type ~= "function" then
value = value(self)
end
return config.getter and config.getter(self, value, ...) or value
end
class["set" .. capitalizedName] = function(self, value, ...)
expect(1, self, "element")
expect(2, value, config.type)
-- Setter Hooks ausführen
for _, hook in ipairs(PropertySystem._setterHooks) do
local newValue = hook(self, name, value, config)
if newValue ~= nil then
value = newValue
end
end
-- Type checking: Entweder korrekter Typ ODER Function
if type(value) ~= "function" then
expect(2, value, config.type)
end
if config.setter then
value = config.setter(self, value, ...)
end
self:_updateProperty(name, value)
return self
end
@@ -104,7 +127,12 @@ function PropertySystem.blueprint(elementClass, properties, basalt, parent)
}
blueprint.get = function(name)
return blueprint._values[name]
local value = blueprint._values[name]
local config = elementClass._properties[name]
if type(value) == "function" and config.type ~= "function" then
value = value(blueprint)
end
return value
end
blueprint.set = function(name, value)
blueprint._values[name] = value
@@ -180,6 +208,9 @@ function PropertySystem:__init()
local value = self._values[name]
local config = self._properties[name]
if(config==nil)then errorManager.error("Property not found: "..name) return end
if type(value) == "function" and config.type ~= "function" then
value = value(self)
end
return config.getter and config.getter(self, value, ...) or value
end
@@ -203,8 +234,13 @@ function PropertySystem:__init()
local originalIndex = originalMT.__index
setmetatable(self, {
__index = function(t, k)
if self._properties[k] then
return self._values[k]
local config = self._properties[k]
if config then
local value = self._values[k]
if type(value) == "function" and config.type ~= "function" then
value = value(self)
end
return value
end
if type(originalIndex) == "function" then
return originalIndex(t, k)
@@ -242,14 +278,22 @@ end
function PropertySystem:_updateProperty(name, value)
local oldValue = self._values[name]
if oldValue ~= value then
self._values[name] = value
-- Wenn der alte Wert eine Funktion ist, müssen wir den tatsächlichen Wert holen
if type(oldValue) == "function" then
oldValue = oldValue(self)
end
self._values[name] = value
-- Wenn der neue Wert eine Funktion ist, evaluieren für Observer
local newValue = type(value) == "function" and value(self) or value
if oldValue ~= newValue then
if self._properties[name].canTriggerRender then
self:updateRender()
end
if self._observers[name] then
for _, callback in ipairs(self._observers[name]) do
callback(self, value, oldValue)
callback(self, newValue, oldValue)
end
end
end
@@ -261,6 +305,30 @@ function PropertySystem:observe(name, callback)
return self
end
function PropertySystem:removeObserver(name, callback)
if self._observers[name] then
for i, cb in ipairs(self._observers[name]) do
if cb == callback then
table.remove(self._observers[name], i)
if #self._observers[name] == 0 then
self._observers[name] = nil
end
break
end
end
end
return self
end
function PropertySystem:removeAllObservers(name)
if name then
self._observers[name] = nil
else
self._observers = {}
end
return self
end
function PropertySystem:instanceProperty(name, config)
PropertySystem.defineProperty(self, name, config)
self._values[name] = config.default

View File

@@ -80,6 +80,17 @@ function Render:textFg(x, y, text, fg)
return self
end
function Render:textBg(x, y, text, bg)
if y < 1 or y > self.height then return self end
bg = colorChars[bg] or "f"
self.buffer.text[y] = self.buffer.text[y]:sub(1,x-1) .. text .. self.buffer.text[y]:sub(x+#text)
self.buffer.bg[y] = self.buffer.bg[y]:sub(1,x-1) .. bg:rep(#text) .. self.buffer.bg[y]:sub(x+#text)
self:addDirtyRect(x, y, #text, 1)
return self
end
function Render:text(x, y, text)
if y < 1 or y > self.height then return self end
@@ -107,6 +118,20 @@ function Render:bg(x, y, bg)
return self
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
error("Text, fg, and bg must be the same length")
end
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:addDirtyRect(x, y, #text, 1)
return self
end
function Render:clear(bg)
local bgChar = colorChars[bg] or "f"
for y=1, self.height do
@@ -153,7 +178,7 @@ function Render:render()
benchmark.update("render")
self.buffer.dirtyRects = {}
if self.blink then
self.terminal.setCursorPos(self.xCursor, self.yCursor)
self.terminal.setCursorBlink(true)
@@ -165,9 +190,8 @@ function Render:render()
return self
end
-- Hilfsfunktionen für Rectangle-Management
function Render:rectOverlaps(r1, r2)
return not (r1.x + r1.width <= r2.x or
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)

49
test_weak.lua Normal file
View File

@@ -0,0 +1,49 @@
local cache = setmetatable({}, {__mode = "k"})
-- Funktion um den Cache-Status zu prüfen
local function printCache()
local count = 0
for k,v in pairs(cache) do
count = count + 1
end
print("Cache entries: " .. count)
end
-- Test 1: Objekte im Cache speichern
local function test1()
print("Test 1: Adding objects")
local obj1 = {name = "obj1"}
local obj2 = {name = "obj2"}
cache[obj1] = "value1"
cache[obj2] = "value2"
printCache() -- Sollte 2 ausgeben
end
-- Test 2: Referenzen löschen und GC ausführen
local function test2()
print("\nTest 2: After garbage collection")
collectgarbage() -- Force GC
printCache() -- Sollte 0 ausgeben, da keine Referenzen mehr existieren
end
-- Test 3: Objekt mit starker Referenz
local function test3()
print("\nTest 3: Strong reference")
local strongRef = {name = "strong"}
cache[strongRef] = "value3"
printCache() -- Sollte 1 ausgeben
print("Keeping strong reference...")
collectgarbage()
printCache() -- Sollte immer noch 1 ausgeben
print("Removing strong reference...")
strongRef = nil
collectgarbage()
printCache() -- Sollte 0 ausgeben
end
test1()
test2()
test3()