- added List
- added checkbox - added program - added slider - added progressbar - added reactive (dynamicValues) smaller bug fixxes
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
test.lua
|
||||
test2.lua
|
||||
lua-ls-cc-tweaked-main
|
||||
test.xml
|
||||
test.xml
|
||||
ascii.lua
|
||||
@@ -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, ...)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
49
src/elements/Checkbox.lua
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
103
src/elements/List.lua
Normal 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
179
src/elements/Program.lua
Normal 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
|
||||
42
src/elements/ProgressBar.lua
Normal file
42
src/elements/ProgressBar.lua
Normal 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
87
src/elements/Slider.lua
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
99
src/plugins/theme.lua
Normal 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,
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
49
test_weak.lua
Normal 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()
|
||||
Reference in New Issue
Block a user