- 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
|
test.lua
|
||||||
test2.lua
|
test2.lua
|
||||||
lua-ls-cc-tweaked-main
|
lua-ls-cc-tweaked-main
|
||||||
test.xml
|
test.xml
|
||||||
|
ascii.lua
|
||||||
@@ -76,8 +76,9 @@ function ElementManager.loadElement(name)
|
|||||||
end
|
end
|
||||||
if(type(hooks)=="function")then
|
if(type(hooks)=="function")then
|
||||||
element[methodName] = function(self, ...)
|
element[methodName] = function(self, ...)
|
||||||
original(self, ...)
|
local result = original(self, ...)
|
||||||
return hooks(self, ...)
|
local hookResult = hooks(self, ...)
|
||||||
|
return hookResult == nil and result or hookResult
|
||||||
end
|
end
|
||||||
elseif(type(hooks)=="table")then
|
elseif(type(hooks)=="table")then
|
||||||
element[methodName] = function(self, ...)
|
element[methodName] = function(self, ...)
|
||||||
|
|||||||
@@ -167,6 +167,17 @@ function BaseElement:handleEvent(event, ...)
|
|||||||
return true
|
return true
|
||||||
end
|
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
|
--- Requests a render update for this element
|
||||||
--- @usage element:updateRender()
|
--- @usage element:updateRender()
|
||||||
function BaseElement:updateRender()
|
function BaseElement:updateRender()
|
||||||
|
|||||||
@@ -40,6 +40,14 @@ function BaseFrame:textFg(x, y, text, fg)
|
|||||||
self._render:textFg(x, y, text, fg)
|
self._render:textFg(x, y, text, fg)
|
||||||
end
|
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)
|
function BaseFrame:setCursor(x, y, blink)
|
||||||
local term = self.get("term")
|
local term = self.get("term")
|
||||||
self._render:setCursor(x, y, blink)
|
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)
|
VisualElement.textFg(self, math.max(1, x), math.max(1, y), text:sub(textStart, textStart + textLen - 1), fg)
|
||||||
end
|
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()
|
function Container:render()
|
||||||
VisualElement.render(self)
|
VisualElement.render(self)
|
||||||
if not self.get("childrenSorted")then
|
if not self.get("childrenSorted")then
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
local VisualElement = require("elements/VisualElement")
|
local VisualElement = require("elements/VisualElement")
|
||||||
|
local tHex = require("libraries/colorHex")
|
||||||
|
|
||||||
---@class Input : VisualElement
|
---@class Input : VisualElement
|
||||||
local Input = setmetatable({}, VisualElement)
|
local Input = setmetatable({}, VisualElement)
|
||||||
@@ -10,19 +11,26 @@ Input.defineProperty(Input, "text", {default = "", type = "string", canTriggerRe
|
|||||||
---@property cursorPos number Input - current cursor position
|
---@property cursorPos number Input - current cursor position
|
||||||
Input.defineProperty(Input, "cursorPos", {default = 1, type = "number"})
|
Input.defineProperty(Input, "cursorPos", {default = 1, type = "number"})
|
||||||
|
|
||||||
---@property viewOffset number Input - offset für Text-Viewport
|
---@property viewOffset number Input - offset of view
|
||||||
Input.defineProperty(Input, "viewOffset", {default = 0, type = "number"})
|
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, "mouse_click")
|
||||||
Input.listenTo(Input, "key")
|
Input.listenTo(Input, "key")
|
||||||
Input.listenTo(Input, "char")
|
Input.listenTo(Input, "char")
|
||||||
|
|
||||||
--- Creates a new Input instance
|
--- 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
|
--- @param basalt table The basalt instance
|
||||||
--- @return Input object The newly created Input instance
|
--- @return Input object The newly created Input instance
|
||||||
--- @usage local element = Input.new("myId", basalt)
|
--- @usage local element = Input.new("myId", basalt)
|
||||||
function Input.new(id, basalt)
|
function Input.new(props, basalt)
|
||||||
local self = setmetatable({}, Input):__init()
|
local self = setmetatable({}, Input):__init()
|
||||||
self:init(id, basalt)
|
self:init(id, basalt)
|
||||||
self.set("width", 8)
|
self.set("width", 8)
|
||||||
@@ -30,8 +38,8 @@ function Input.new(id, basalt)
|
|||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
function Input:init(id, basalt)
|
function Input:init(props, basalt)
|
||||||
VisualElement.init(self, id, basalt)
|
VisualElement.init(self, props, basalt)
|
||||||
self.set("type", "Input")
|
self.set("type", "Input")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -39,8 +47,15 @@ function Input:char(char)
|
|||||||
if not self.get("focused") then return end
|
if not self.get("focused") then return end
|
||||||
local text = self.get("text")
|
local text = self.get("text")
|
||||||
local pos = self.get("cursorPos")
|
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("text", text:sub(1, pos-1) .. char .. text:sub(pos))
|
||||||
self.set("cursorPos", pos + 1)
|
self.set("cursorPos", pos + 1)
|
||||||
|
self:updateRender()
|
||||||
self:updateViewport()
|
self:updateViewport()
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -48,64 +63,91 @@ function Input:key(key)
|
|||||||
if not self.get("focused") then return end
|
if not self.get("focused") then return end
|
||||||
local pos = self.get("cursorPos")
|
local pos = self.get("cursorPos")
|
||||||
local text = self.get("text")
|
local text = self.get("text")
|
||||||
|
local viewOffset = self.get("viewOffset")
|
||||||
|
local width = self.get("width")
|
||||||
|
|
||||||
if key == keys.left and pos > 1 then
|
if key == keys.left then
|
||||||
self.set("cursorPos", pos - 1)
|
if pos > 1 then
|
||||||
self:setCursor(pos - 1,1, true)
|
self.set("cursorPos", pos - 1)
|
||||||
elseif key == keys.right and pos <= #text then
|
if pos - 1 <= viewOffset then
|
||||||
self.set("cursorPos", pos + 1)
|
self.set("viewOffset", math.max(0, pos - 2))
|
||||||
self:setCursor(pos + 1,1, true)
|
end
|
||||||
elseif key == keys.backspace and pos > 1 then
|
end
|
||||||
self.set("text", text:sub(1, pos-2) .. text:sub(pos))
|
elseif key == keys.right then
|
||||||
self.set("cursorPos", pos - 1)
|
if pos <= #text then
|
||||||
self:setCursor(pos - 1,1, true)
|
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
|
end
|
||||||
self:updateViewport()
|
|
||||||
|
local relativePos = self.get("cursorPos") - self.get("viewOffset")
|
||||||
|
self:setCursor(relativePos, 1, true)
|
||||||
end
|
end
|
||||||
|
|
||||||
function Input:focus()
|
function Input:focus()
|
||||||
VisualElement.focus(self)
|
VisualElement.focus(self)
|
||||||
self.set("background", colors.blue)
|
self:updateRender()
|
||||||
self:setCursor(1,1, true)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function Input:blur()
|
function Input:blur()
|
||||||
VisualElement.blur(self)
|
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
|
end
|
||||||
|
|
||||||
function Input:updateViewport()
|
function Input:updateViewport()
|
||||||
local width = self.get("width")
|
local width = self.get("width")
|
||||||
local text = self.get("text")
|
|
||||||
local cursorPos = self.get("cursorPos")
|
local cursorPos = self.get("cursorPos")
|
||||||
local viewOffset = self.get("viewOffset")
|
local viewOffset = self.get("viewOffset")
|
||||||
|
local textLength = #self.get("text")
|
||||||
-- Wenn Cursor außerhalb des sichtbaren Bereichs nach rechts
|
|
||||||
if cursorPos - viewOffset > width then
|
if cursorPos - viewOffset > width then
|
||||||
self.set("viewOffset", cursorPos - width)
|
self.set("viewOffset", cursorPos - width)
|
||||||
|
elseif cursorPos <= viewOffset then
|
||||||
|
|
||||||
|
self.set("viewOffset", math.max(0, cursorPos - 1))
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Wenn Cursor außerhalb des sichtbaren Bereichs nach links
|
if viewOffset > textLength - width then
|
||||||
if cursorPos <= viewOffset then
|
self.set("viewOffset", math.max(0, textLength - width))
|
||||||
self.set("viewOffset", cursorPos - 1)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function Input:render()
|
function Input:render()
|
||||||
VisualElement.render(self)
|
|
||||||
local text = self.get("text")
|
local text = self.get("text")
|
||||||
local viewOffset = self.get("viewOffset")
|
local viewOffset = self.get("viewOffset")
|
||||||
local width = self.get("width")
|
local width = self.get("width")
|
||||||
|
local placeholder = self.get("placeholder")
|
||||||
-- Nur den sichtbaren Teil des Textes rendern
|
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)
|
local visibleText = text:sub(viewOffset + 1, viewOffset + width)
|
||||||
self:textFg(1, 1, visibleText, self.get("foreground"))
|
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
|
end
|
||||||
|
|
||||||
return Input
|
return Input
|
||||||
@@ -12,7 +12,7 @@ Label.defineProperty(Label, "text", {default = "Label", type = "string", setter
|
|||||||
end})
|
end})
|
||||||
|
|
||||||
--- Creates a new Label instance
|
--- 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
|
--- @param basalt table The basalt instance
|
||||||
--- @return Label object The newly created Label instance
|
--- @return Label object The newly created Label instance
|
||||||
--- @usage local element = Label.new("myId", basalt)
|
--- @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 elementManager = require("elementManager")
|
||||||
local BaseElement = elementManager.getElement("BaseElement")
|
local BaseElement = elementManager.getElement("BaseElement")
|
||||||
|
local tHex = require("libraries/colorHex")
|
||||||
|
|
||||||
---@alias color number
|
---@alias color number
|
||||||
|
|
||||||
---@class VisualElement : BaseElement
|
---@class VisualElement : BaseElement
|
||||||
local VisualElement = setmetatable({}, BaseElement)
|
local VisualElement = setmetatable({}, BaseElement)
|
||||||
VisualElement.__index = VisualElement
|
VisualElement.__index = VisualElement
|
||||||
local tHex = require("libraries/colorHex")
|
|
||||||
|
|
||||||
---@property x number 1 x position of the element
|
---@property x number 1 x position of the element
|
||||||
VisualElement.defineProperty(VisualElement, "x", {default = 1, type = "number", canTriggerRender = true})
|
VisualElement.defineProperty(VisualElement, "x", {default = 1, type = "number", canTriggerRender = true})
|
||||||
@@ -56,6 +56,8 @@ end})
|
|||||||
VisualElement.listenTo(VisualElement, "focus")
|
VisualElement.listenTo(VisualElement, "focus")
|
||||||
VisualElement.listenTo(VisualElement, "blur")
|
VisualElement.listenTo(VisualElement, "blur")
|
||||||
|
|
||||||
|
local max, min = math.max, math.min
|
||||||
|
|
||||||
--- Creates a new VisualElement instance
|
--- Creates a new VisualElement instance
|
||||||
--- @param props table The properties to initialize the element with
|
--- @param props table The properties to initialize the element with
|
||||||
--- @param basalt table The basalt instance
|
--- @param basalt table The basalt instance
|
||||||
@@ -98,6 +100,18 @@ function VisualElement:textFg(x, y, text, fg)
|
|||||||
self.parent:textFg(x, y, text, fg)
|
self.parent:textFg(x, y, text, fg)
|
||||||
end
|
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
|
--- Checks if the specified coordinates are within the bounds of the element
|
||||||
--- @param x number The x position to check
|
--- @param x number The x position to check
|
||||||
--- @param y number The y position to check
|
--- @param y number The y position to check
|
||||||
@@ -193,6 +207,7 @@ end
|
|||||||
function VisualElement:setCursor(x, y, blink)
|
function VisualElement:setCursor(x, y, blink)
|
||||||
if self.parent then
|
if self.parent then
|
||||||
local absX, absY = self:getAbsolutePosition(x, y)
|
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)
|
return self.parent:setCursor(absX, absY, blink)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ local function coloredPrint(message, color)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function errorHandler.error(errMsg)
|
function errorHandler.error(errMsg)
|
||||||
|
if errorHandler.errorHandled then
|
||||||
|
error()
|
||||||
|
end
|
||||||
term.setBackgroundColor(colors.black)
|
term.setBackgroundColor(colors.black)
|
||||||
|
|
||||||
term.clear()
|
term.clear()
|
||||||
@@ -98,6 +101,7 @@ function errorHandler.error(errMsg)
|
|||||||
|
|
||||||
term.setBackgroundColor(colors.black)
|
term.setBackgroundColor(colors.black)
|
||||||
LOGGER.error(errMsg)
|
LOGGER.error(errMsg)
|
||||||
|
errorHandler.errorHandled = true
|
||||||
error()
|
error()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ local function errorHandler(err)
|
|||||||
errorManager.error(err)
|
errorManager.error(err)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Use xpcall with error handler
|
|
||||||
local ok, result = pcall(require, "main")
|
local ok, result = pcall(require, "main")
|
||||||
|
|
||||||
package.path = defaultPath
|
package.path = defaultPath
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ local lazyElementCount = 10
|
|||||||
local lazyElementsTimer = 0
|
local lazyElementsTimer = 0
|
||||||
local isLazyElementsTimerActive = false
|
local isLazyElementsTimerActive = false
|
||||||
|
|
||||||
|
|
||||||
local function queueLazyElements()
|
local function queueLazyElements()
|
||||||
if(isLazyElementsTimerActive)then return end
|
if(isLazyElementsTimerActive)then return end
|
||||||
lazyElementsTimer = os.startTimer(0.2)
|
lazyElementsTimer = os.startTimer(0.2)
|
||||||
@@ -134,9 +135,11 @@ local function updateEvent(event, ...)
|
|||||||
if(event=="terminate")then basalt.stop() end
|
if(event=="terminate")then basalt.stop() end
|
||||||
if lazyElementsEventHandler(event, ...) then return end
|
if lazyElementsEventHandler(event, ...) then return end
|
||||||
|
|
||||||
if(mainFrame:dispatchEvent(event, ...))then
|
if(mainFrame)then
|
||||||
return
|
if(mainFrame:dispatchEvent(event, ...))then
|
||||||
end
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
if basalt._events[event] then
|
if basalt._events[event] then
|
||||||
for _, callback in ipairs(basalt._events[event]) do
|
for _, callback in ipairs(basalt._events[event]) do
|
||||||
|
|||||||
@@ -312,6 +312,7 @@ function VisualElement.hooks.dispatchEvent(self, event, ...)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function VisualElement.setup(element)
|
function VisualElement.setup(element)
|
||||||
|
VisualElementBaseDispatchEvent = element.dispatchEvent
|
||||||
element.defineProperty(element, "animation", {default = nil, type = "table"})
|
element.defineProperty(element, "animation", {default = nil, type = "table"})
|
||||||
element.listenTo(element, "timer")
|
element.listenTo(element, "timer")
|
||||||
end
|
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 mathEnv = {
|
||||||
local code = expression:gsub(
|
clamp = function(val, min, max)
|
||||||
"(%w+)%s*%?%s*([^:]+)%s*:%s*([^}]+)",
|
return math.min(math.max(val, min), max)
|
||||||
"%1 and %2 or %3"
|
end,
|
||||||
)
|
round = function(val)
|
||||||
|
return math.floor(val + 0.5)
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
return load(string.format([[
|
local function parseExpression(expr, element)
|
||||||
return function(self)
|
expr = expr:gsub("^{(.+)}$", "%1")
|
||||||
return %s
|
|
||||||
|
for k,v in pairs(colors) do
|
||||||
|
if type(k) == "string" then
|
||||||
|
expr = expr:gsub("%f[%w]"..k.."%f[%W]", "colors."..k)
|
||||||
end
|
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
|
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 = {}
|
local BaseElement = {}
|
||||||
|
|
||||||
function BaseElement:setReactiveProperty(propertyName, expression)
|
BaseElement.hooks = {
|
||||||
setupReactiveProperty(self, propertyName, expression)
|
destroy = function(self)
|
||||||
return self
|
if observerCache[self] then
|
||||||
end
|
for _, observer in ipairs(observerCache[self]) do
|
||||||
|
observer.target:observe(observer.property, observer.callback)
|
||||||
function BaseElement:setReactive(propertyName, expression)
|
end
|
||||||
local reactiveFunc = createReactiveFunction(expression, self)
|
observerCache[self] = nil
|
||||||
self.set(propertyName, reactiveFunc)
|
end
|
||||||
return self
|
end
|
||||||
end
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
BaseElement = BaseElement
|
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 = {}
|
PropertySystem._properties = {}
|
||||||
local blueprintTemplates = {}
|
local blueprintTemplates = {}
|
||||||
|
|
||||||
|
PropertySystem._setterHooks = {}
|
||||||
|
|
||||||
|
function PropertySystem.addSetterHook(hook)
|
||||||
|
table.insert(PropertySystem._setterHooks, hook)
|
||||||
|
end
|
||||||
|
|
||||||
function PropertySystem.defineProperty(class, name, config)
|
function PropertySystem.defineProperty(class, name, config)
|
||||||
if not rawget(class, '_properties') then
|
if not rawget(class, '_properties') then
|
||||||
class._properties = {}
|
class._properties = {}
|
||||||
@@ -28,15 +34,32 @@ function PropertySystem.defineProperty(class, name, config)
|
|||||||
class["get" .. capitalizedName] = function(self, ...)
|
class["get" .. capitalizedName] = function(self, ...)
|
||||||
expect(1, self, "element")
|
expect(1, self, "element")
|
||||||
local value = self._values[name]
|
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
|
return config.getter and config.getter(self, value, ...) or value
|
||||||
end
|
end
|
||||||
|
|
||||||
class["set" .. capitalizedName] = function(self, value, ...)
|
class["set" .. capitalizedName] = function(self, value, ...)
|
||||||
expect(1, self, "element")
|
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
|
if config.setter then
|
||||||
value = config.setter(self, value, ...)
|
value = config.setter(self, value, ...)
|
||||||
end
|
end
|
||||||
|
|
||||||
self:_updateProperty(name, value)
|
self:_updateProperty(name, value)
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
@@ -104,7 +127,12 @@ function PropertySystem.blueprint(elementClass, properties, basalt, parent)
|
|||||||
}
|
}
|
||||||
|
|
||||||
blueprint.get = function(name)
|
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
|
end
|
||||||
blueprint.set = function(name, value)
|
blueprint.set = function(name, value)
|
||||||
blueprint._values[name] = value
|
blueprint._values[name] = value
|
||||||
@@ -180,6 +208,9 @@ function PropertySystem:__init()
|
|||||||
local value = self._values[name]
|
local value = self._values[name]
|
||||||
local config = self._properties[name]
|
local config = self._properties[name]
|
||||||
if(config==nil)then errorManager.error("Property not found: "..name) return end
|
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
|
return config.getter and config.getter(self, value, ...) or value
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -203,8 +234,13 @@ function PropertySystem:__init()
|
|||||||
local originalIndex = originalMT.__index
|
local originalIndex = originalMT.__index
|
||||||
setmetatable(self, {
|
setmetatable(self, {
|
||||||
__index = function(t, k)
|
__index = function(t, k)
|
||||||
if self._properties[k] then
|
local config = self._properties[k]
|
||||||
return self._values[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
|
end
|
||||||
if type(originalIndex) == "function" then
|
if type(originalIndex) == "function" then
|
||||||
return originalIndex(t, k)
|
return originalIndex(t, k)
|
||||||
@@ -242,14 +278,22 @@ end
|
|||||||
|
|
||||||
function PropertySystem:_updateProperty(name, value)
|
function PropertySystem:_updateProperty(name, value)
|
||||||
local oldValue = self._values[name]
|
local oldValue = self._values[name]
|
||||||
if oldValue ~= value then
|
-- Wenn der alte Wert eine Funktion ist, müssen wir den tatsächlichen Wert holen
|
||||||
self._values[name] = value
|
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
|
if self._properties[name].canTriggerRender then
|
||||||
self:updateRender()
|
self:updateRender()
|
||||||
end
|
end
|
||||||
if self._observers[name] then
|
if self._observers[name] then
|
||||||
for _, callback in ipairs(self._observers[name]) do
|
for _, callback in ipairs(self._observers[name]) do
|
||||||
callback(self, value, oldValue)
|
callback(self, newValue, oldValue)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -261,6 +305,30 @@ function PropertySystem:observe(name, callback)
|
|||||||
return self
|
return self
|
||||||
end
|
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)
|
function PropertySystem:instanceProperty(name, config)
|
||||||
PropertySystem.defineProperty(self, name, config)
|
PropertySystem.defineProperty(self, name, config)
|
||||||
self._values[name] = config.default
|
self._values[name] = config.default
|
||||||
|
|||||||
@@ -80,6 +80,17 @@ function Render:textFg(x, y, text, fg)
|
|||||||
return self
|
return self
|
||||||
end
|
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)
|
function Render:text(x, y, text)
|
||||||
if y < 1 or y > self.height then return self end
|
if y < 1 or y > self.height then return self end
|
||||||
|
|
||||||
@@ -107,6 +118,20 @@ function Render:bg(x, y, bg)
|
|||||||
return self
|
return self
|
||||||
end
|
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)
|
function Render:clear(bg)
|
||||||
local bgChar = colorChars[bg] or "f"
|
local bgChar = colorChars[bg] or "f"
|
||||||
for y=1, self.height do
|
for y=1, self.height do
|
||||||
@@ -153,7 +178,7 @@ function Render:render()
|
|||||||
|
|
||||||
benchmark.update("render")
|
benchmark.update("render")
|
||||||
self.buffer.dirtyRects = {}
|
self.buffer.dirtyRects = {}
|
||||||
|
|
||||||
if self.blink then
|
if self.blink then
|
||||||
self.terminal.setCursorPos(self.xCursor, self.yCursor)
|
self.terminal.setCursorPos(self.xCursor, self.yCursor)
|
||||||
self.terminal.setCursorBlink(true)
|
self.terminal.setCursorBlink(true)
|
||||||
@@ -165,9 +190,8 @@ function Render:render()
|
|||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Hilfsfunktionen für Rectangle-Management
|
|
||||||
function Render:rectOverlaps(r1, r2)
|
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
|
r2.x + r2.width <= r1.x or
|
||||||
r1.y + r1.height <= r2.y or
|
r1.y + r1.height <= r2.y or
|
||||||
r2.y + r2.height <= r1.y)
|
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