Basalt2 update
- Finished themes - Added state plugins for persistance - Finished reactive plugin - Added debug plugin (80% finished) - Added benchmark plugin - Added Tree, Table, List, Dropdown and Menu Elements - Bugfixes
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,3 +3,4 @@ test2.lua
|
|||||||
lua-ls-cc-tweaked-main
|
lua-ls-cc-tweaked-main
|
||||||
test.xml
|
test.xml
|
||||||
ascii.lua
|
ascii.lua
|
||||||
|
tests
|
||||||
32
README.md
32
README.md
@@ -1 +1,31 @@
|
|||||||
Basalt v2 Dev
|
# Basalt - A UI Framework for CC:Tweaked
|
||||||
|
|
||||||
|

|
||||||
|
[](https://discord.gg/yNNnmBVBpE)
|
||||||
|
|
||||||
|
This is a complete rework of Basalt. It provides an intuitive way to create complex user interfaces for your CC:Tweaked programs.
|
||||||
|
|
||||||
|
Basalt is intended to be an easy-to-understand UI Framework designed for CC:Tweaked - a popular minecraft mod. For more information about CC:Tweaked, checkout the project's [wiki](https://tweaked.cc/) or [download](https://modrinth.com/mod/cc-tweaked).
|
||||||
|
**Note:** Basalt is still under developement and you may find bugs!
|
||||||
|
|
||||||
|
Check out the [wiki](https://basalt.madefor.cc/) for more information.
|
||||||
|
If you have questions, feel free to join the discord server: [discord.gg/yNNnmBVBpE](https://discord.gg/yNNnmBVBpE).
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
For detailed documentation, examples and guides, visit [basalt.madefor.cc](https://basalt.madefor.cc/)
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
If you need help or have questions:
|
||||||
|
- Check the [documentation](https://basalt.madefor.cc/)
|
||||||
|
- Join our [Discord](https://discord.gg/yNNnmBVBpE)
|
||||||
|
- Report issues on [GitHub](https://github.com/Pyroxenium/Basalt2/issues)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License.
|
||||||
|
|
||||||
|
## Demo
|
||||||
|
|
||||||
|

|
||||||
19
examples/benchmarks.lua
Normal file
19
examples/benchmarks.lua
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
local basalt = require("src")
|
||||||
|
local main = basalt.getMainFrame()
|
||||||
|
|
||||||
|
local btn = main:addButton()
|
||||||
|
:setText("Test")
|
||||||
|
:setX(40)
|
||||||
|
:setY(5)
|
||||||
|
:onMouseClick(function()
|
||||||
|
main:logContainerBenchmarks("render")
|
||||||
|
--main:stopChildrenBenchmark("render")
|
||||||
|
end)
|
||||||
|
|
||||||
|
local prog = main:addProgram()
|
||||||
|
:execute("../rom/programs/shell.lua")
|
||||||
|
|
||||||
|
local frame main:addFrame():setX(30):addButton()
|
||||||
|
|
||||||
|
main:benchmarkContainer("render")
|
||||||
|
basalt.run()
|
||||||
111
examples/states.lua
Normal file
111
examples/states.lua
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
local basalt = require("src")
|
||||||
|
|
||||||
|
-- Create main frame
|
||||||
|
local main = basalt.getMainFrame()
|
||||||
|
|
||||||
|
-- Create a complex form using states for managing its data
|
||||||
|
local form = main:addFrame()
|
||||||
|
:setSize("{parent.width - 4}", "{parent.height - 4}")
|
||||||
|
:setPosition(3, 3)
|
||||||
|
:setBackground(colors.lightGray)
|
||||||
|
-- Initialize form states with default values
|
||||||
|
:initializeState("username", "", true) -- true = triggers render update
|
||||||
|
:initializeState("email", "")
|
||||||
|
:initializeState("age", 0)
|
||||||
|
:initializeState("submitted", false)
|
||||||
|
-- Add computed state for form validation
|
||||||
|
:computed("isValid", function(self)
|
||||||
|
local username = self:getState("username")
|
||||||
|
local email = self:getState("email")
|
||||||
|
local age = self:getState("age")
|
||||||
|
return #username > 0 and email:match(".+@.+") and age > 0
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Create form title
|
||||||
|
form:addLabel()
|
||||||
|
:setText("Registration Form")
|
||||||
|
:setPosition(2, 2)
|
||||||
|
:setForeground(colors.black)
|
||||||
|
|
||||||
|
-- Username input with state binding
|
||||||
|
local usernameInput = form:addInput()
|
||||||
|
:setPosition(2, 4)
|
||||||
|
:setSize(20, 1)
|
||||||
|
:setBackground(colors.white)
|
||||||
|
:setForeground(colors.black)
|
||||||
|
-- Update username state when input changes
|
||||||
|
:onChange(function(self, value)
|
||||||
|
form:setState("username", value)
|
||||||
|
end)
|
||||||
|
|
||||||
|
form:addLabel()
|
||||||
|
:setText("Username:")
|
||||||
|
:setPosition(2, 3)
|
||||||
|
:setForeground(colors.black)
|
||||||
|
|
||||||
|
-- Email input
|
||||||
|
local emailInput = form:addInput()
|
||||||
|
:setPosition(2, 6)
|
||||||
|
:setSize(20, 1)
|
||||||
|
:setBackground(colors.white)
|
||||||
|
:setForeground(colors.black)
|
||||||
|
:onChange(function(self, value)
|
||||||
|
form:setState("email", value)
|
||||||
|
end)
|
||||||
|
|
||||||
|
form:addLabel()
|
||||||
|
:setText("Email:")
|
||||||
|
:setPosition(2, 5)
|
||||||
|
:setForeground(colors.black)
|
||||||
|
|
||||||
|
-- Age input
|
||||||
|
local ageInput = form:addInput()
|
||||||
|
:setPosition(2, 8)
|
||||||
|
:setSize(5, 1)
|
||||||
|
:setBackground(colors.white)
|
||||||
|
:setForeground(colors.black)
|
||||||
|
:onChange(function(self, value)
|
||||||
|
-- Convert to number and update state
|
||||||
|
form:setState("age", tonumber(value) or 0)
|
||||||
|
end)
|
||||||
|
|
||||||
|
form:addLabel()
|
||||||
|
:setText("Age:")
|
||||||
|
:setPosition(2, 7)
|
||||||
|
:setForeground(colors.black)
|
||||||
|
|
||||||
|
-- Submit button that reacts to form validity
|
||||||
|
local submitBtn = form:addButton()
|
||||||
|
:setPosition(2, 10)
|
||||||
|
:setSize(10, 1)
|
||||||
|
:setText("Submit")
|
||||||
|
-- Button color changes based on form validity
|
||||||
|
:onStateChange("isValid", function(self, isValid)
|
||||||
|
submitBtn:setBackground(isValid and colors.lime or colors.gray)
|
||||||
|
end)
|
||||||
|
:onMouseClick(function()
|
||||||
|
if form.computedStates.isValid() then
|
||||||
|
form:setState("submitted", true)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Status message that updates when form is submitted
|
||||||
|
local statusLabel = form:addLabel()
|
||||||
|
:setPosition(2, 12)
|
||||||
|
:setForeground(colors.black)
|
||||||
|
|
||||||
|
-- Listen for form submission
|
||||||
|
form:onStateChange("submitted", function(self, submitted)
|
||||||
|
if submitted then
|
||||||
|
local username = self:getState("username")
|
||||||
|
local email = self:getState("email")
|
||||||
|
local age = self:getState("age")
|
||||||
|
statusLabel:setText(string.format(
|
||||||
|
"Submitted: %s (%s) - Age: %d",
|
||||||
|
username, email, age
|
||||||
|
))
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Run basalt
|
||||||
|
basalt.run()
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
-- Will temporary exist while developing, maybe i will create a benchmark plugin in future
|
|
||||||
|
|
||||||
local log = require("log")
|
|
||||||
|
|
||||||
local Benchmark = {}
|
|
||||||
|
|
||||||
function Benchmark.start(name)
|
|
||||||
Benchmark[name] = {
|
|
||||||
startTime = os.epoch("local"),
|
|
||||||
updates = 0
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
function Benchmark.update(name)
|
|
||||||
if Benchmark[name] then
|
|
||||||
Benchmark[name].updates = Benchmark[name].updates + 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Benchmark.stop(name)
|
|
||||||
if Benchmark[name] then
|
|
||||||
local endTime = os.epoch("local")
|
|
||||||
local duration = endTime - Benchmark[name].startTime
|
|
||||||
local updates = Benchmark[name].updates
|
|
||||||
|
|
||||||
log.debug(string.format("[Benchmark] %s: %dms, %d updates, avg: %.2fms per update",
|
|
||||||
name, duration, updates, duration/updates))
|
|
||||||
|
|
||||||
return duration, updates
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return Benchmark
|
|
||||||
@@ -13,6 +13,7 @@ local main = format:gsub("path", dir)
|
|||||||
local ElementManager = {}
|
local ElementManager = {}
|
||||||
ElementManager._elements = {}
|
ElementManager._elements = {}
|
||||||
ElementManager._plugins = {}
|
ElementManager._plugins = {}
|
||||||
|
ElementManager._APIs = {}
|
||||||
local elementsDirectory = fs.combine(dir, "elements")
|
local elementsDirectory = fs.combine(dir, "elements")
|
||||||
local pluginsDirectory = fs.combine(dir, "plugins")
|
local pluginsDirectory = fs.combine(dir, "plugins")
|
||||||
|
|
||||||
@@ -40,10 +41,14 @@ if fs.exists(pluginsDirectory) then
|
|||||||
local plugin = require(fs.combine("plugins", name))
|
local plugin = require(fs.combine("plugins", name))
|
||||||
if type(plugin) == "table" then
|
if type(plugin) == "table" then
|
||||||
for k,v in pairs(plugin) do
|
for k,v in pairs(plugin) do
|
||||||
|
if(k ~= "API")then
|
||||||
if(ElementManager._plugins[k]==nil)then
|
if(ElementManager._plugins[k]==nil)then
|
||||||
ElementManager._plugins[k] = {}
|
ElementManager._plugins[k] = {}
|
||||||
end
|
end
|
||||||
table.insert(ElementManager._plugins[k], v)
|
table.insert(ElementManager._plugins[k], v)
|
||||||
|
else
|
||||||
|
ElementManager._APIs[name] = v
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -114,4 +119,8 @@ function ElementManager.getElementList()
|
|||||||
return ElementManager._elements
|
return ElementManager._elements
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function ElementManager.getAPI(name)
|
||||||
|
return ElementManager._APIs[name]
|
||||||
|
end
|
||||||
|
|
||||||
return ElementManager
|
return ElementManager
|
||||||
@@ -164,7 +164,7 @@ end
|
|||||||
--- @vararg any The arguments for the event
|
--- @vararg any The arguments for the event
|
||||||
--- @return boolean? handled Whether the event was handled
|
--- @return boolean? handled Whether the event was handled
|
||||||
function BaseElement:handleEvent(event, ...)
|
function BaseElement:handleEvent(event, ...)
|
||||||
return true
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
function BaseElement:getBaseFrame()
|
function BaseElement:getBaseFrame()
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ end})
|
|||||||
|
|
||||||
function BaseFrame.new(props, basalt)
|
function BaseFrame.new(props, basalt)
|
||||||
local self = setmetatable({}, BaseFrame):__init()
|
local self = setmetatable({}, BaseFrame):__init()
|
||||||
self:init(props, basalt)
|
|
||||||
self.set("term", term.current())
|
self.set("term", term.current())
|
||||||
self.set("background", colors.red)
|
self.set("background", colors.lightGray)
|
||||||
|
self:init(props, basalt)
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -11,13 +11,14 @@ Button.defineProperty(Button, "text", {default = "Button", type = "string", canT
|
|||||||
|
|
||||||
---@event mouse_click The event that is triggered when the button is clicked
|
---@event mouse_click The event that is triggered when the button is clicked
|
||||||
Button.listenTo(Button, "mouse_click")
|
Button.listenTo(Button, "mouse_click")
|
||||||
|
Button.listenTo(Button, "mouse_up")
|
||||||
|
|
||||||
function Button.new(props, basalt)
|
function Button.new(props, basalt)
|
||||||
local self = setmetatable({}, Button):__init()
|
local self = setmetatable({}, Button):__init()
|
||||||
self:init(props, basalt)
|
|
||||||
self.set("width", 10)
|
self.set("width", 10)
|
||||||
self.set("height", 3)
|
self.set("height", 3)
|
||||||
self.set("z", 5)
|
self.set("z", 5)
|
||||||
|
self:init(props, basalt)
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ Checkbox.listenTo(Checkbox, "mouse_click")
|
|||||||
|
|
||||||
function Checkbox.new(props, basalt)
|
function Checkbox.new(props, basalt)
|
||||||
local self = setmetatable({}, Checkbox):__init()
|
local self = setmetatable({}, Checkbox):__init()
|
||||||
self:init(props, basalt)
|
|
||||||
self.set("width", 1)
|
self.set("width", 1)
|
||||||
self.set("height", 1)
|
self.set("height", 1)
|
||||||
|
self:init(props, basalt)
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ local function sortAndFilterChildren(self, children)
|
|||||||
local visibleChildren = {}
|
local visibleChildren = {}
|
||||||
|
|
||||||
for _, child in ipairs(children) do
|
for _, child in ipairs(children) do
|
||||||
if self:isChildVisible(child) then
|
if self:isChildVisible(child) and child.get("visible") then
|
||||||
table.insert(visibleChildren, child)
|
table.insert(visibleChildren, child)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -103,7 +103,6 @@ local function sortAndFilterChildren(self, children)
|
|||||||
local current = visibleChildren[i]
|
local current = visibleChildren[i]
|
||||||
local currentZ = current.get("z")
|
local currentZ = current.get("z")
|
||||||
local j = i - 1
|
local j = i - 1
|
||||||
|
|
||||||
while j > 0 do
|
while j > 0 do
|
||||||
local compare = visibleChildren[j].get("z")
|
local compare = visibleChildren[j].get("z")
|
||||||
if compare > currentZ then
|
if compare > currentZ then
|
||||||
@@ -119,6 +118,15 @@ local function sortAndFilterChildren(self, children)
|
|||||||
return visibleChildren
|
return visibleChildren
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function Container:clear()
|
||||||
|
self.set("children", {})
|
||||||
|
self.set("childrenEvents", {})
|
||||||
|
self.set("visibleChildren", {})
|
||||||
|
self.set("visibleChildrenEvents", {})
|
||||||
|
self.set("childrenSorted", true)
|
||||||
|
self.set("childrenEventsSorted", true)
|
||||||
|
end
|
||||||
|
|
||||||
function Container:sortChildren()
|
function Container:sortChildren()
|
||||||
self.set("visibleChildren", sortAndFilterChildren(self, self._values.children))
|
self.set("visibleChildren", sortAndFilterChildren(self, self._values.children))
|
||||||
self.set("childrenSorted", true)
|
self.set("childrenSorted", true)
|
||||||
@@ -242,11 +250,11 @@ local function callChildrenEvents(self, visibleOnly, event, ...)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function Container:handleEvent(event, ...)
|
function Container:handleEvent(event, ...)
|
||||||
if(VisualElement.handleEvent(self, event, ...))then
|
VisualElement.handleEvent(self, event, ...)
|
||||||
local args = convertMousePosition(self, event, ...)
|
local args = convertMousePosition(self, event, ...)
|
||||||
return callChildrenEvents(self, false, event, table.unpack(args))
|
return callChildrenEvents(self, false, event, table.unpack(args))
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
function Container:mouse_click(button, x, y)
|
function Container:mouse_click(button, x, y)
|
||||||
if VisualElement.mouse_click(self, button, x, y) then
|
if VisualElement.mouse_click(self, button, x, y) then
|
||||||
@@ -260,6 +268,16 @@ function Container:mouse_click(button, x, y)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function Container:mouse_up(button, x, y)
|
||||||
|
if VisualElement.mouse_up(self, button, x, y) then
|
||||||
|
local args = convertMousePosition(self, "mouse_up", button, x, y)
|
||||||
|
local success, child = callChildrenEvents(self, true, "mouse_up", table.unpack(args))
|
||||||
|
if(success)then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
function Container:key(key)
|
function Container:key(key)
|
||||||
if self.get("focusedChild") then
|
if self.get("focusedChild") then
|
||||||
return self.get("focusedChild"):dispatchEvent("key", key)
|
return self.get("focusedChild"):dispatchEvent("key", key)
|
||||||
|
|||||||
122
src/elements/Dropdown.lua
Normal file
122
src/elements/Dropdown.lua
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
local VisualElement = require("elements/VisualElement")
|
||||||
|
local List = require("elements/List")
|
||||||
|
local tHex = require("libraries/colorHex")
|
||||||
|
|
||||||
|
---@class Dropdown : List
|
||||||
|
local Dropdown = setmetatable({}, List)
|
||||||
|
Dropdown.__index = Dropdown
|
||||||
|
|
||||||
|
Dropdown.defineProperty(Dropdown, "isOpen", {default = false, type = "boolean", canTriggerRender = true})
|
||||||
|
Dropdown.defineProperty(Dropdown, "dropdownHeight", {default = 5, type = "number"})
|
||||||
|
Dropdown.defineProperty(Dropdown, "selectedText", {default = "", type = "string"})
|
||||||
|
Dropdown.defineProperty(Dropdown, "dropSymbol", {default = "\31", type = "string"}) -- ▼ Symbol
|
||||||
|
|
||||||
|
function Dropdown.new(props, basalt)
|
||||||
|
local self = setmetatable({}, Dropdown):__init()
|
||||||
|
self.set("width", 16)
|
||||||
|
self.set("height", 1) -- Dropdown ist initial nur 1 Zeile hoch
|
||||||
|
self.set("z", 8)
|
||||||
|
self:init(props, basalt)
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function Dropdown:init(props, basalt)
|
||||||
|
List.init(self, props, basalt)
|
||||||
|
self.set("type", "Dropdown")
|
||||||
|
end
|
||||||
|
|
||||||
|
function Dropdown:mouse_click(button, x, y)
|
||||||
|
if not VisualElement.mouse_click(self, button, x, y) then return false end
|
||||||
|
|
||||||
|
local relX, relY = self:getRelativePosition(x, y)
|
||||||
|
|
||||||
|
if relY == 1 then -- Klick auf Header
|
||||||
|
self.set("isOpen", not self.get("isOpen"))
|
||||||
|
if not self.get("isOpen") then
|
||||||
|
self.set("height", 1)
|
||||||
|
else
|
||||||
|
self.set("height", 1 + math.min(self.get("dropdownHeight"), #self.get("items")))
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
elseif self.get("isOpen") and relY > 1 then
|
||||||
|
-- Offset für die Liste korrigieren (relY - 1 wegen Header)
|
||||||
|
local index = relY - 1 + self.get("offset")
|
||||||
|
local items = self.get("items")
|
||||||
|
|
||||||
|
if index <= #items then
|
||||||
|
local item = items[index]
|
||||||
|
if type(item) == "table" and item.separator then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
self.set("selectedIndex", index)
|
||||||
|
self.set("isOpen", false)
|
||||||
|
self.set("height", 1)
|
||||||
|
|
||||||
|
if type(item) == "table" and item.callback then
|
||||||
|
item.callback(self)
|
||||||
|
end
|
||||||
|
|
||||||
|
self:fireEvent("select", index, item)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function Dropdown:render()
|
||||||
|
VisualElement.render(self)
|
||||||
|
|
||||||
|
-- Header rendern
|
||||||
|
local text = self.get("selectedText")
|
||||||
|
if #text == 0 and self.get("selectedIndex") > 0 then
|
||||||
|
local item = self.get("items")[self.get("selectedIndex")]
|
||||||
|
text = type(item) == "table" and item.text or tostring(item)
|
||||||
|
end
|
||||||
|
|
||||||
|
self:blit(1, 1, text .. string.rep(" ", self.get("width") - #text - 1) .. (self.get("isOpen") and "\31" or "\17"),
|
||||||
|
string.rep(tHex[self.get("foreground")], self.get("width")),
|
||||||
|
string.rep(tHex[self.get("background")], self.get("width")))
|
||||||
|
|
||||||
|
-- Items nur rendern wenn offen
|
||||||
|
if self.get("isOpen") then
|
||||||
|
local items = self.get("items")
|
||||||
|
local offset = self.get("offset")
|
||||||
|
local selected = self.get("selectedIndex")
|
||||||
|
local width = self.get("width")
|
||||||
|
|
||||||
|
-- Liste ab Zeile 2 rendern (unterhalb des Headers)
|
||||||
|
for i = 2, self.get("height") do
|
||||||
|
local itemIndex = i - 1 + offset -- -1 wegen Header
|
||||||
|
local item = items[itemIndex]
|
||||||
|
|
||||||
|
if item then
|
||||||
|
if type(item) == "table" and item.separator then
|
||||||
|
local separatorChar = (item.text or "-"):sub(1,1)
|
||||||
|
local separatorText = string.rep(separatorChar, width)
|
||||||
|
local fg = item.foreground or self.get("foreground")
|
||||||
|
local bg = item.background or self.get("background")
|
||||||
|
|
||||||
|
self:textBg(1, i, string.rep(" ", width), bg)
|
||||||
|
self:textFg(1, i, separatorText, fg)
|
||||||
|
else
|
||||||
|
local itemText = type(item) == "table" and item.text or tostring(item)
|
||||||
|
local isSelected = itemIndex == selected
|
||||||
|
|
||||||
|
local bg = isSelected and
|
||||||
|
(item.selectedBackground or self.get("selectedColor")) or
|
||||||
|
(item.background or self.get("background"))
|
||||||
|
|
||||||
|
local fg = isSelected and
|
||||||
|
(item.selectedForeground or colors.white) or
|
||||||
|
(item.foreground or self.get("foreground"))
|
||||||
|
|
||||||
|
self:textBg(1, i, string.rep(" ", width), bg)
|
||||||
|
self:textFg(1, i, itemText, fg)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return Dropdown
|
||||||
@@ -232,13 +232,13 @@ end
|
|||||||
--- @usage local element = Flexbox.new("myId", basalt)
|
--- @usage local element = Flexbox.new("myId", basalt)
|
||||||
function Flexbox.new(props, basalt)
|
function Flexbox.new(props, basalt)
|
||||||
local self = setmetatable({}, Flexbox):__init()
|
local self = setmetatable({}, Flexbox):__init()
|
||||||
self:init(props, basalt)
|
|
||||||
self.set("width", 12)
|
self.set("width", 12)
|
||||||
self.set("height", 6)
|
self.set("height", 6)
|
||||||
self.set("background", colors.blue)
|
self.set("background", colors.blue)
|
||||||
self.set("z", 10)
|
self.set("z", 10)
|
||||||
self:observe("width", function() self.set("flexUpdateLayout", true) end)
|
self:observe("width", function() self.set("flexUpdateLayout", true) end)
|
||||||
self:observe("height", function() self.set("flexUpdateLayout", true) end)
|
self:observe("height", function() self.set("flexUpdateLayout", true) end)
|
||||||
|
self:init(props, basalt)
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -12,11 +12,11 @@ Frame.__index = Frame
|
|||||||
--- @usage local element = Frame.new("myId", basalt)
|
--- @usage local element = Frame.new("myId", basalt)
|
||||||
function Frame.new(props, basalt)
|
function Frame.new(props, basalt)
|
||||||
local self = setmetatable({}, Frame):__init()
|
local self = setmetatable({}, Frame):__init()
|
||||||
self:init(props, basalt)
|
|
||||||
self.set("width", 12)
|
self.set("width", 12)
|
||||||
self.set("height", 6)
|
self.set("height", 6)
|
||||||
self.set("background", colors.blue)
|
self.set("background", colors.gray)
|
||||||
self.set("z", 10)
|
self.set("z", 10)
|
||||||
|
self:init(props, basalt)
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -32,9 +32,9 @@ Input.listenTo(Input, "char")
|
|||||||
--- @usage local element = Input.new("myId", basalt)
|
--- @usage local element = Input.new("myId", basalt)
|
||||||
function Input.new(props, basalt)
|
function Input.new(props, basalt)
|
||||||
local self = setmetatable({}, Input):__init()
|
local self = setmetatable({}, Input):__init()
|
||||||
self:init(id, basalt)
|
|
||||||
self.set("width", 8)
|
self.set("width", 8)
|
||||||
self.set("z", 3)
|
self.set("z", 3)
|
||||||
|
self:init(id, basalt)
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ Label.__index = Label
|
|||||||
|
|
||||||
---@property text string Label Label text to be displayed
|
---@property text string Label Label text to be displayed
|
||||||
Label.defineProperty(Label, "text", {default = "Label", type = "string", setter = function(self, value)
|
Label.defineProperty(Label, "text", {default = "Label", type = "string", setter = function(self, value)
|
||||||
|
if(type(value)=="function")then value = value() end
|
||||||
self.set("width", #value)
|
self.set("width", #value)
|
||||||
return value
|
return value
|
||||||
end})
|
end})
|
||||||
@@ -18,9 +19,10 @@ end})
|
|||||||
--- @usage local element = Label.new("myId", basalt)
|
--- @usage local element = Label.new("myId", basalt)
|
||||||
function Label.new(props, basalt)
|
function Label.new(props, basalt)
|
||||||
local self = setmetatable({}, Label):__init()
|
local self = setmetatable({}, Label):__init()
|
||||||
self:init(props, basalt)
|
|
||||||
self.set("z", 3)
|
self.set("z", 3)
|
||||||
|
self.set("foreground", colors.black)
|
||||||
self.set("backgroundEnabled", false)
|
self.set("backgroundEnabled", false)
|
||||||
|
self:init(props, basalt)
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -20,9 +20,10 @@ List.listenTo(List, "mouse_scroll")
|
|||||||
|
|
||||||
function List.new(props, basalt)
|
function List.new(props, basalt)
|
||||||
local self = setmetatable({}, List):__init()
|
local self = setmetatable({}, List):__init()
|
||||||
self:init(props, basalt)
|
|
||||||
self.set("width", 16)
|
self.set("width", 16)
|
||||||
self.set("height", 8)
|
self.set("height", 8)
|
||||||
|
self.set("background", colors.gray)
|
||||||
|
self:init(props, basalt)
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -54,16 +55,25 @@ end
|
|||||||
|
|
||||||
function List:mouse_click(button, x, y)
|
function List:mouse_click(button, x, y)
|
||||||
if button == 1 and self:isInBounds(x, y) and self.get("selectable") then
|
if button == 1 and self:isInBounds(x, y) and self.get("selectable") then
|
||||||
local relY = self:getRelativePosition(x, y)
|
local _, index = self:getRelativePosition(x, y)
|
||||||
local index = relY + self.get("offset")
|
|
||||||
|
|
||||||
if index <= #self.get("items") then
|
local adjustedIndex = index + self.get("offset")
|
||||||
self.set("selectedIndex", index)
|
local items = self.get("items")
|
||||||
self:fireEvent("select", index, self.get("items")[index])
|
|
||||||
|
if adjustedIndex <= #items then
|
||||||
|
local item = items[adjustedIndex]
|
||||||
|
self.set("selectedIndex", adjustedIndex)
|
||||||
|
|
||||||
|
if type(item) == "table" and item.callback then
|
||||||
|
item.callback(self)
|
||||||
|
end
|
||||||
|
|
||||||
|
self:fireEvent("select", adjustedIndex, item)
|
||||||
self:updateRender()
|
self:updateRender()
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
function List:mouse_scroll(direction, x, y)
|
function List:mouse_scroll(direction, x, y)
|
||||||
@@ -77,6 +87,11 @@ function List:mouse_scroll(direction, x, y)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function List:onSelect(callback)
|
||||||
|
self:registerCallback("select", callback)
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
function List:render()
|
function List:render()
|
||||||
VisualElement.render(self)
|
VisualElement.render(self)
|
||||||
|
|
||||||
@@ -84,17 +99,35 @@ function List:render()
|
|||||||
local height = self.get("height")
|
local height = self.get("height")
|
||||||
local offset = self.get("offset")
|
local offset = self.get("offset")
|
||||||
local selected = self.get("selectedIndex")
|
local selected = self.get("selectedIndex")
|
||||||
|
local width = self.get("width")
|
||||||
|
|
||||||
for i = 1, height do
|
for i = 1, height do
|
||||||
local itemIndex = i + offset
|
local itemIndex = i + offset
|
||||||
local item = items[itemIndex]
|
local item = items[itemIndex]
|
||||||
|
|
||||||
if item then
|
if item then
|
||||||
if itemIndex == selected then
|
if type(item) == "table" and item.separator then
|
||||||
self:textBg(1, i, string.rep(" ", self.get("width")), self.get("selectedColor"))
|
local separatorChar = (item.text or "-"):sub(1,1)
|
||||||
self:textFg(1, i, item, colors.white)
|
local separatorText = string.rep(separatorChar, width)
|
||||||
|
local fg = item.foreground or self.get("foreground")
|
||||||
|
local bg = item.background or self.get("background")
|
||||||
|
|
||||||
|
self:textBg(1, i, string.rep(" ", width), bg)
|
||||||
|
self:textFg(1, i, separatorText, fg)
|
||||||
else
|
else
|
||||||
self:textFg(1, i, item, self.get("foreground"))
|
local text = type(item) == "table" and item.text or item
|
||||||
|
local isSelected = itemIndex == selected
|
||||||
|
|
||||||
|
local bg = isSelected and
|
||||||
|
(item.selectedBackground or self.get("selectedColor")) or
|
||||||
|
(item.background or self.get("background"))
|
||||||
|
|
||||||
|
local fg = isSelected and
|
||||||
|
(item.selectedForeground or colors.white) or
|
||||||
|
(item.foreground or self.get("foreground"))
|
||||||
|
|
||||||
|
self:textBg(1, i, string.rep(" ", width), bg)
|
||||||
|
self:textFg(1, i, text, fg)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
91
src/elements/Menu.lua
Normal file
91
src/elements/Menu.lua
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
local VisualElement = require("elements/VisualElement")
|
||||||
|
local List = require("elements/List")
|
||||||
|
local tHex = require("libraries/colorHex")
|
||||||
|
|
||||||
|
---@class Menu : List
|
||||||
|
local Menu = setmetatable({}, List)
|
||||||
|
Menu.__index = Menu
|
||||||
|
|
||||||
|
Menu.defineProperty(Menu, "separatorColor", {default = colors.gray, type = "number"})
|
||||||
|
|
||||||
|
function Menu.new(props, basalt)
|
||||||
|
local self = setmetatable({}, Menu):__init()
|
||||||
|
self.set("width", 30)
|
||||||
|
self.set("height", 1)
|
||||||
|
self.set("background", colors.gray)
|
||||||
|
self:init(props, basalt)
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function Menu:init(props, basalt)
|
||||||
|
List.init(self, props, basalt)
|
||||||
|
self.set("type", "Menu")
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function Menu:setItems(items)
|
||||||
|
local listItems = {}
|
||||||
|
local totalWidth = 0
|
||||||
|
for _, item in ipairs(items) do
|
||||||
|
if item.separator then
|
||||||
|
table.insert(listItems, {text = item.text or "|", selectable = false})
|
||||||
|
totalWidth = totalWidth + 1
|
||||||
|
else
|
||||||
|
local text = " " .. item.text .. " "
|
||||||
|
item.text = text
|
||||||
|
table.insert(listItems, item)
|
||||||
|
totalWidth = totalWidth + #text
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self.set("width", totalWidth)
|
||||||
|
return List.setItems(self, listItems)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Menu:render()
|
||||||
|
VisualElement.render(self)
|
||||||
|
local currentX = 1
|
||||||
|
|
||||||
|
for i, item in ipairs(self.get("items")) do
|
||||||
|
local isSelected = i == self.get("selectedIndex")
|
||||||
|
|
||||||
|
local fg = item.selectable == false and self.get("separatorColor") or
|
||||||
|
(isSelected and (item.selectedForeground or self.get("foreground")) or
|
||||||
|
(item.foreground or self.get("foreground")))
|
||||||
|
|
||||||
|
local bg = isSelected and
|
||||||
|
(item.selectedBackground or self.get("selectedColor")) or
|
||||||
|
(item.background or self.get("background"))
|
||||||
|
|
||||||
|
self:blit(currentX, 1, item.text,
|
||||||
|
string.rep(tHex[fg], #item.text),
|
||||||
|
string.rep(tHex[bg], #item.text))
|
||||||
|
|
||||||
|
currentX = currentX + #item.text
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Menu:mouse_click(button, x, y)
|
||||||
|
if not VisualElement.mouse_click(self, button, x, y) then return false end
|
||||||
|
if(self.get("selectable") == false) then return false end
|
||||||
|
local relX = select(1, self:getRelativePosition(x, y))
|
||||||
|
local currentX = 1
|
||||||
|
|
||||||
|
for i, item in ipairs(self.get("items")) do
|
||||||
|
if relX >= currentX and relX < currentX + #item.text then
|
||||||
|
if item.selectable ~= false then
|
||||||
|
self.set("selectedIndex", i)
|
||||||
|
if type(item) == "table" then
|
||||||
|
if item.callback then
|
||||||
|
item.callback(self)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self:fireEvent("select", i, item)
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
currentX = currentX + #item.text
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
return Menu
|
||||||
@@ -13,9 +13,9 @@ ProgressBar.defineProperty(ProgressBar, "progressColor", {default = colors.lime,
|
|||||||
|
|
||||||
function ProgressBar.new(props, basalt)
|
function ProgressBar.new(props, basalt)
|
||||||
local self = setmetatable({}, ProgressBar):__init()
|
local self = setmetatable({}, ProgressBar):__init()
|
||||||
self:init(props, basalt)
|
|
||||||
self.set("width", 10)
|
self.set("width", 10)
|
||||||
self.set("height", 1)
|
self.set("height", 1)
|
||||||
|
self:init(props, basalt)
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ Slider.listenTo(Slider, "mouse_up")
|
|||||||
|
|
||||||
function Slider.new(props, basalt)
|
function Slider.new(props, basalt)
|
||||||
local self = setmetatable({}, Slider):__init()
|
local self = setmetatable({}, Slider):__init()
|
||||||
self:init(props, basalt)
|
|
||||||
self.set("width", 8)
|
self.set("width", 8)
|
||||||
self.set("height", 1)
|
self.set("height", 1)
|
||||||
self.set("backgroundEnabled", false)
|
self.set("backgroundEnabled", false)
|
||||||
|
self:init(props, basalt)
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
182
src/elements/Table.lua
Normal file
182
src/elements/Table.lua
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
local VisualElement = require("elements/VisualElement")
|
||||||
|
local tHex = require("libraries/colorHex")
|
||||||
|
|
||||||
|
---@class Table : VisualElement
|
||||||
|
local Table = setmetatable({}, VisualElement)
|
||||||
|
Table.__index = Table
|
||||||
|
|
||||||
|
Table.defineProperty(Table, "columns", {default = {}, type = "table"})
|
||||||
|
Table.defineProperty(Table, "data", {default = {}, type = "table", canTriggerRender = true})
|
||||||
|
Table.defineProperty(Table, "selectedRow", {default = nil, type = "number", canTriggerRender = true})
|
||||||
|
Table.defineProperty(Table, "headerColor", {default = colors.blue, type = "number"})
|
||||||
|
Table.defineProperty(Table, "selectedColor", {default = colors.lightBlue, type = "number"})
|
||||||
|
Table.defineProperty(Table, "gridColor", {default = colors.gray, type = "number"})
|
||||||
|
Table.defineProperty(Table, "sortColumn", {default = nil, type = "number"})
|
||||||
|
Table.defineProperty(Table, "sortDirection", {default = "asc", type = "string"})
|
||||||
|
Table.defineProperty(Table, "scrollOffset", {default = 0, type = "number", canTriggerRender = true})
|
||||||
|
|
||||||
|
Table.listenTo(Table, "mouse_click")
|
||||||
|
Table.listenTo(Table, "mouse_scroll")
|
||||||
|
|
||||||
|
function Table.new(props, basalt)
|
||||||
|
local self = setmetatable({}, Table):__init()
|
||||||
|
self.set("width", 30)
|
||||||
|
self.set("height", 10)
|
||||||
|
self.set("z", 5)
|
||||||
|
self:init(props, basalt)
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function Table:init(props, basalt)
|
||||||
|
VisualElement.init(self, props, basalt)
|
||||||
|
self.set("type", "Table")
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function Table:setColumns(columns)
|
||||||
|
-- Columns Format: {{name="ID", width=4}, {name="Name", width=10}}
|
||||||
|
self.set("columns", columns)
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function Table:setData(data)
|
||||||
|
-- Data Format: {{"1", "Item One"}, {"2", "Item Two"}}
|
||||||
|
self.set("data", data)
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function Table:sortData(columnIndex)
|
||||||
|
local data = self.get("data")
|
||||||
|
local direction = self.get("sortDirection")
|
||||||
|
|
||||||
|
table.sort(data, function(a, b)
|
||||||
|
if direction == "asc" then
|
||||||
|
return a[columnIndex] < b[columnIndex]
|
||||||
|
else
|
||||||
|
return a[columnIndex] > b[columnIndex]
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
self.set("data", data)
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function Table:mouse_click(button, x, y)
|
||||||
|
if not VisualElement.mouse_click(self, button, x, y) then return false end
|
||||||
|
|
||||||
|
local relX, relY = self:getRelativePosition(x, y)
|
||||||
|
|
||||||
|
-- Header-Click für Sorting
|
||||||
|
if relY == 1 then
|
||||||
|
local currentX = 1
|
||||||
|
for i, col in ipairs(self.get("columns")) do
|
||||||
|
if relX >= currentX and relX < currentX + col.width then
|
||||||
|
if self.get("sortColumn") == i then
|
||||||
|
self.set("sortDirection", self.get("sortDirection") == "asc" and "desc" or "asc")
|
||||||
|
else
|
||||||
|
self.set("sortColumn", i)
|
||||||
|
self.set("sortDirection", "asc")
|
||||||
|
end
|
||||||
|
self:sortData(i)
|
||||||
|
break
|
||||||
|
end
|
||||||
|
currentX = currentX + col.width
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Row-Selection (berücksichtigt Scroll-Offset)
|
||||||
|
if relY > 1 then
|
||||||
|
local rowIndex = relY - 2 + self.get("scrollOffset")
|
||||||
|
if rowIndex >= 0 and rowIndex < #self.get("data") then
|
||||||
|
self.set("selectedRow", rowIndex + 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function Table:mouse_scroll(direction, x, y)
|
||||||
|
local data = self.get("data")
|
||||||
|
local height = self.get("height")
|
||||||
|
local visibleRows = height - 2
|
||||||
|
local maxScroll = math.max(0, #data - visibleRows + 1) -- +1 korrigiert den Scroll-Bereich
|
||||||
|
local newOffset = math.min(maxScroll, math.max(0, self.get("scrollOffset") + direction))
|
||||||
|
|
||||||
|
self.set("scrollOffset", newOffset)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function Table:render()
|
||||||
|
VisualElement.render(self)
|
||||||
|
|
||||||
|
local columns = self.get("columns")
|
||||||
|
local data = self.get("data")
|
||||||
|
local selected = self.get("selectedRow")
|
||||||
|
local sortCol = self.get("sortColumn")
|
||||||
|
local scrollOffset = self.get("scrollOffset")
|
||||||
|
local height = self.get("height")
|
||||||
|
|
||||||
|
local currentX = 1
|
||||||
|
for i, col in ipairs(columns) do
|
||||||
|
local text = col.name
|
||||||
|
if i == sortCol then
|
||||||
|
text = text .. (self.get("sortDirection") == "asc" and "\30" or "\31")
|
||||||
|
end
|
||||||
|
self:textFg(currentX, 1, text:sub(1, col.width), self.get("headerColor"))
|
||||||
|
currentX = currentX + col.width
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Angepasste Berechnung der sichtbaren Zeilen
|
||||||
|
local visibleRows = height - 2 -- Verfügbare Zeilen (minus Header)
|
||||||
|
for y = 2, height do
|
||||||
|
local rowIndex = y - 2 + scrollOffset
|
||||||
|
local rowData = data[rowIndex + 1]
|
||||||
|
|
||||||
|
-- Zeile nur rendern wenn es auch Daten dafür gibt
|
||||||
|
if rowData and (rowIndex + 1) <= #data then -- Korrigierte Bedingung
|
||||||
|
currentX = 1
|
||||||
|
local bg = (rowIndex + 1) == selected and self.get("selectedColor") or self.get("background")
|
||||||
|
|
||||||
|
for i, col in ipairs(columns) do
|
||||||
|
local cellText = rowData[i] or ""
|
||||||
|
local paddedText = cellText .. string.rep(" ", col.width - #cellText)
|
||||||
|
self:blit(currentX, y, paddedText,
|
||||||
|
string.rep(tHex[self.get("foreground")], col.width),
|
||||||
|
string.rep(tHex[bg], col.width))
|
||||||
|
currentX = currentX + col.width
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-- Leere Zeile füllen
|
||||||
|
self:blit(1, y, string.rep(" ", self.get("width")),
|
||||||
|
string.rep(tHex[self.get("foreground")], self.get("width")),
|
||||||
|
string.rep(tHex[self.get("background")], self.get("width")))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Scrollbar Berechnung überarbeitet
|
||||||
|
if #data > height - 2 then
|
||||||
|
local scrollbarHeight = height - 2
|
||||||
|
local thumbSize = math.max(1, math.floor(scrollbarHeight * (height - 2) / #data))
|
||||||
|
|
||||||
|
-- Thumb Position korrigiert
|
||||||
|
local maxScroll = #data - (height - 2) + 1 -- +1 für korrekte End-Position
|
||||||
|
local scrollPercent = scrollOffset / maxScroll
|
||||||
|
local thumbPos = 2 + math.floor(scrollPercent * (scrollbarHeight - thumbSize))
|
||||||
|
|
||||||
|
if scrollOffset >= maxScroll then
|
||||||
|
thumbPos = height - thumbSize -- Exakt am Ende
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Scrollbar Background
|
||||||
|
for y = 2, height do
|
||||||
|
self:blit(self.get("width"), y, "\127", tHex[colors.gray], tHex[colors.gray])
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Thumb zeichnen
|
||||||
|
for y = thumbPos, math.min(height, thumbPos + thumbSize - 1) do
|
||||||
|
self:blit(self.get("width"), y, "\127", tHex[colors.white], tHex[colors.white])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return Table
|
||||||
147
src/elements/Tree.lua
Normal file
147
src/elements/Tree.lua
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
local VisualElement = require("elements/VisualElement")
|
||||||
|
local tHex = require("libraries/colorHex")
|
||||||
|
|
||||||
|
---@class Tree : VisualElement
|
||||||
|
local Tree = setmetatable({}, VisualElement)
|
||||||
|
Tree.__index = Tree
|
||||||
|
|
||||||
|
Tree.defineProperty(Tree, "nodes", {default = {}, type = "table", canTriggerRender = true})
|
||||||
|
Tree.defineProperty(Tree, "selectedNode", {default = nil, type = "table", canTriggerRender = true})
|
||||||
|
Tree.defineProperty(Tree, "expandedNodes", {default = {}, type = "table", canTriggerRender = true})
|
||||||
|
Tree.defineProperty(Tree, "scrollOffset", {default = 0, type = "number", canTriggerRender = true})
|
||||||
|
Tree.defineProperty(Tree, "nodeColor", {default = colors.white, type = "number"})
|
||||||
|
Tree.defineProperty(Tree, "selectedColor", {default = colors.lightBlue, type = "number"})
|
||||||
|
|
||||||
|
Tree.listenTo(Tree, "mouse_click")
|
||||||
|
Tree.listenTo(Tree, "mouse_scroll")
|
||||||
|
|
||||||
|
function Tree.new(props, basalt)
|
||||||
|
local self = setmetatable({}, Tree):__init()
|
||||||
|
self.set("width", 30)
|
||||||
|
self.set("height", 10)
|
||||||
|
self.set("z", 5)
|
||||||
|
self:init(props, basalt)
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function Tree:init(props, basalt)
|
||||||
|
VisualElement.init(self, props, basalt)
|
||||||
|
self.set("type", "Tree")
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function Tree:setNodes(nodes)
|
||||||
|
self.set("nodes", nodes)
|
||||||
|
if #nodes > 0 then
|
||||||
|
self.get("expandedNodes")[nodes[1]] = true
|
||||||
|
end
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function Tree:expandNode(node)
|
||||||
|
self.get("expandedNodes")[node] = true
|
||||||
|
self:updateRender()
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function Tree:collapseNode(node)
|
||||||
|
self.get("expandedNodes")[node] = nil
|
||||||
|
self:updateRender()
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function Tree:toggleNode(node)
|
||||||
|
if self.get("expandedNodes")[node] then
|
||||||
|
self:collapseNode(node)
|
||||||
|
else
|
||||||
|
self:expandNode(node)
|
||||||
|
end
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
local function flattenTree(nodes, expandedNodes, level, result)
|
||||||
|
result = result or {}
|
||||||
|
level = level or 0
|
||||||
|
|
||||||
|
for _, node in ipairs(nodes) do
|
||||||
|
table.insert(result, {node = node, level = level})
|
||||||
|
if expandedNodes[node] and node.children then
|
||||||
|
flattenTree(node.children, expandedNodes, level + 1, result)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
function Tree:mouse_click(button, x, y)
|
||||||
|
if not VisualElement.mouse_click(self, button, x, y) then return false end
|
||||||
|
|
||||||
|
local relX, relY = self:getRelativePosition(x, y)
|
||||||
|
local flatNodes = flattenTree(self.get("nodes"), self.get("expandedNodes"))
|
||||||
|
local visibleIndex = relY + self.get("scrollOffset")
|
||||||
|
|
||||||
|
if flatNodes[visibleIndex] then
|
||||||
|
local nodeInfo = flatNodes[visibleIndex]
|
||||||
|
local node = nodeInfo.node
|
||||||
|
|
||||||
|
if relX <= nodeInfo.level * 2 + 2 then
|
||||||
|
self:toggleNode(node)
|
||||||
|
end
|
||||||
|
|
||||||
|
self.set("selectedNode", node)
|
||||||
|
self:fireEvent("node_select", node)
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function Tree:onSelect(callback)
|
||||||
|
self:registerCallback("node_select", callback)
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function Tree:mouse_scroll(direction)
|
||||||
|
local flatNodes = flattenTree(self.get("nodes"), self.get("expandedNodes"))
|
||||||
|
local maxScroll = math.max(0, #flatNodes - self.get("height"))
|
||||||
|
local newScroll = math.min(maxScroll, math.max(0, self.get("scrollOffset") + direction))
|
||||||
|
|
||||||
|
self.set("scrollOffset", newScroll)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function Tree:render()
|
||||||
|
VisualElement.render(self)
|
||||||
|
|
||||||
|
local flatNodes = flattenTree(self.get("nodes"), self.get("expandedNodes"))
|
||||||
|
local height = self.get("height")
|
||||||
|
local selectedNode = self.get("selectedNode")
|
||||||
|
local expandedNodes = self.get("expandedNodes")
|
||||||
|
local scrollOffset = self.get("scrollOffset")
|
||||||
|
|
||||||
|
for y = 1, height do
|
||||||
|
local nodeInfo = flatNodes[y + scrollOffset]
|
||||||
|
if nodeInfo then
|
||||||
|
local node = nodeInfo.node
|
||||||
|
local level = nodeInfo.level
|
||||||
|
local indent = string.rep(" ", level)
|
||||||
|
|
||||||
|
-- Expand/Collapse Symbol
|
||||||
|
local symbol = " "
|
||||||
|
if node.children and #node.children > 0 then
|
||||||
|
symbol = expandedNodes[node] and "\31" or "\16"
|
||||||
|
end
|
||||||
|
|
||||||
|
local bg = node == selectedNode and self.get("selectedColor") or self.get("background")
|
||||||
|
local text = indent .. symbol .." " .. (node.text or "Node")
|
||||||
|
|
||||||
|
self:blit(1, y, text .. string.rep(" ", self.get("width") - #text),
|
||||||
|
string.rep(tHex[self.get("nodeColor")], self.get("width")),
|
||||||
|
string.rep(tHex[bg], self.get("width")))
|
||||||
|
else
|
||||||
|
-- Leere Zeile
|
||||||
|
self:blit(1, y, string.rep(" ", self.get("width")),
|
||||||
|
string.rep(tHex[self.get("foreground")], self.get("width")),
|
||||||
|
string.rep(tHex[self.get("background")], self.get("width")))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return Tree
|
||||||
@@ -53,6 +53,18 @@ VisualElement.defineProperty(VisualElement, "focused", {default = false, type =
|
|||||||
return value
|
return value
|
||||||
end})
|
end})
|
||||||
|
|
||||||
|
VisualElement.defineProperty(VisualElement, "visible", {default = true, type = "boolean", canTriggerRender = true, setter=function(self, value)
|
||||||
|
if(self.parent~=nil)then
|
||||||
|
self.parent.set("childrenSorted", false)
|
||||||
|
self.parent.set("childrenEventsSorted", false)
|
||||||
|
end
|
||||||
|
return value
|
||||||
|
end})
|
||||||
|
|
||||||
|
VisualElement.combineProperties(VisualElement, "position", "x", "y")
|
||||||
|
VisualElement.combineProperties(VisualElement, "size", "width", "height")
|
||||||
|
VisualElement.combineProperties(VisualElement, "color", "foreground", "background")
|
||||||
|
|
||||||
VisualElement.listenTo(VisualElement, "focus")
|
VisualElement.listenTo(VisualElement, "focus")
|
||||||
VisualElement.listenTo(VisualElement, "blur")
|
VisualElement.listenTo(VisualElement, "blur")
|
||||||
|
|
||||||
@@ -66,7 +78,6 @@ local max, min = math.max, math.min
|
|||||||
function VisualElement.new(props, basalt)
|
function VisualElement.new(props, basalt)
|
||||||
local self = setmetatable({}, VisualElement):__init()
|
local self = setmetatable({}, VisualElement):__init()
|
||||||
self:init(props, basalt)
|
self:init(props, basalt)
|
||||||
self.set("type", "VisualElement")
|
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -139,8 +150,8 @@ function VisualElement:mouse_click(button, x, y)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function VisualElement:mouse_up(button, x, y)
|
function VisualElement:mouse_up(button, x, y)
|
||||||
if self:isInBounds(x, y) then
|
|
||||||
self.set("clicked", false)
|
self.set("clicked", false)
|
||||||
|
if self:isInBounds(x, y) then
|
||||||
self:fireEvent("mouse_up", button, x, y)
|
self:fireEvent("mouse_up", button, x, y)
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
local benchmark = require("benchmark")
|
|
||||||
benchmark.start("Basalt Initialization")
|
|
||||||
local elementManager = require("elementManager")
|
local elementManager = require("elementManager")
|
||||||
local errorManager = require("errorManager")
|
local errorManager = require("errorManager")
|
||||||
local propertySystem = require("propertySystem")
|
local propertySystem = require("propertySystem")
|
||||||
@@ -178,7 +176,6 @@ end
|
|||||||
--- @usage basalt.run()
|
--- @usage basalt.run()
|
||||||
--- @usage basalt.run(false)
|
--- @usage basalt.run(false)
|
||||||
function basalt.run(isActive)
|
function basalt.run(isActive)
|
||||||
benchmark.stop("Basalt Initialization")
|
|
||||||
updaterActive = isActive
|
updaterActive = isActive
|
||||||
if(isActive==nil)then updaterActive = true end
|
if(isActive==nil)then updaterActive = true end
|
||||||
local function f()
|
local function f()
|
||||||
@@ -197,4 +194,8 @@ function basalt.run(isActive)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function basalt.getAPI(name)
|
||||||
|
return elementManager.getAPI(name)
|
||||||
|
end
|
||||||
|
|
||||||
return basalt
|
return basalt
|
||||||
325
src/plugins/benchmark.lua
Normal file
325
src/plugins/benchmark.lua
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
local log = require("log")
|
||||||
|
|
||||||
|
local activeProfiles = setmetatable({}, {__mode = "k"})
|
||||||
|
|
||||||
|
local function createProfile()
|
||||||
|
return {
|
||||||
|
methods = {},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function wrapMethod(element, methodName)
|
||||||
|
local originalMethod = element[methodName]
|
||||||
|
|
||||||
|
if not activeProfiles[element] then
|
||||||
|
activeProfiles[element] = createProfile()
|
||||||
|
end
|
||||||
|
if not activeProfiles[element].methods[methodName] then
|
||||||
|
activeProfiles[element].methods[methodName] = {
|
||||||
|
calls = 0,
|
||||||
|
totalTime = 0,
|
||||||
|
minTime = math.huge,
|
||||||
|
maxTime = 0,
|
||||||
|
lastTime = 0,
|
||||||
|
startTime = 0,
|
||||||
|
path = {},
|
||||||
|
methodName = methodName,
|
||||||
|
originalMethod = originalMethod
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
element[methodName] = function(self, ...)
|
||||||
|
self:startProfile(methodName)
|
||||||
|
local result = originalMethod(self, ...)
|
||||||
|
self:endProfile(methodName)
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local BaseElement = {}
|
||||||
|
|
||||||
|
function BaseElement:startProfile(methodName)
|
||||||
|
local profile = activeProfiles[self]
|
||||||
|
if not profile then
|
||||||
|
profile = createProfile()
|
||||||
|
activeProfiles[self] = profile
|
||||||
|
end
|
||||||
|
|
||||||
|
if not profile.methods[methodName] then
|
||||||
|
profile.methods[methodName] = {
|
||||||
|
calls = 0,
|
||||||
|
totalTime = 0,
|
||||||
|
minTime = math.huge,
|
||||||
|
maxTime = 0,
|
||||||
|
lastTime = 0,
|
||||||
|
startTime = 0,
|
||||||
|
path = {},
|
||||||
|
methodName = methodName
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local methodProfile = profile.methods[methodName]
|
||||||
|
methodProfile.startTime = os.clock() * 1000
|
||||||
|
methodProfile.path = {}
|
||||||
|
|
||||||
|
local current = self
|
||||||
|
while current do
|
||||||
|
table.insert(methodProfile.path, 1, current.get("name") or current.get("id"))
|
||||||
|
current = current.parent
|
||||||
|
end
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function BaseElement:endProfile(methodName)
|
||||||
|
local profile = activeProfiles[self]
|
||||||
|
if not profile or not profile.methods[methodName] then return self end
|
||||||
|
|
||||||
|
local methodProfile = profile.methods[methodName]
|
||||||
|
local endTime = os.clock() * 1000
|
||||||
|
local duration = endTime - methodProfile.startTime
|
||||||
|
|
||||||
|
methodProfile.calls = methodProfile.calls + 1
|
||||||
|
methodProfile.totalTime = methodProfile.totalTime + duration
|
||||||
|
methodProfile.minTime = math.min(methodProfile.minTime, duration)
|
||||||
|
methodProfile.maxTime = math.max(methodProfile.maxTime, duration)
|
||||||
|
methodProfile.lastTime = duration
|
||||||
|
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function BaseElement:benchmark(methodName)
|
||||||
|
if not self[methodName] then
|
||||||
|
log.error("Method " .. methodName .. " does not exist")
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
activeProfiles[self] = createProfile()
|
||||||
|
activeProfiles[self].methodName = methodName
|
||||||
|
activeProfiles[self].isRunning = true
|
||||||
|
|
||||||
|
wrapMethod(self, methodName)
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function BaseElement:logBenchmark(methodName)
|
||||||
|
local profile = activeProfiles[self]
|
||||||
|
if not profile or not profile.methods[methodName] then return self end
|
||||||
|
|
||||||
|
local stats = profile.methods[methodName]
|
||||||
|
if stats then
|
||||||
|
local averageTime = stats.calls > 0 and (stats.totalTime / stats.calls) or 0
|
||||||
|
log.info(string.format(
|
||||||
|
"Benchmark results for %s.%s: " ..
|
||||||
|
"Path: %s " ..
|
||||||
|
"Calls: %d " ..
|
||||||
|
"Average time: %.2fms " ..
|
||||||
|
"Min time: %.2fms " ..
|
||||||
|
"Max time: %.2fms " ..
|
||||||
|
"Last time: %.2fms " ..
|
||||||
|
"Total time: %.2fms",
|
||||||
|
table.concat(stats.path, "."),
|
||||||
|
stats.methodName,
|
||||||
|
table.concat(stats.path, "/"),
|
||||||
|
stats.calls,
|
||||||
|
averageTime,
|
||||||
|
stats.minTime ~= math.huge and stats.minTime or 0,
|
||||||
|
stats.maxTime,
|
||||||
|
stats.lastTime,
|
||||||
|
stats.totalTime
|
||||||
|
))
|
||||||
|
end
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function BaseElement:stopBenchmark(methodName)
|
||||||
|
local profile = activeProfiles[self]
|
||||||
|
if not profile or not profile.methods[methodName] then return self end
|
||||||
|
|
||||||
|
local stats = profile.methods[methodName]
|
||||||
|
if stats and stats.originalMethod then
|
||||||
|
self[methodName] = stats.originalMethod
|
||||||
|
end
|
||||||
|
|
||||||
|
profile.methods[methodName] = nil
|
||||||
|
if not next(profile.methods) then
|
||||||
|
activeProfiles[self] = nil
|
||||||
|
end
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function BaseElement:getBenchmarkStats(methodName)
|
||||||
|
local profile = activeProfiles[self]
|
||||||
|
if not profile or not profile.methods[methodName] then return nil end
|
||||||
|
|
||||||
|
local stats = profile.methods[methodName]
|
||||||
|
return {
|
||||||
|
averageTime = stats.totalTime / stats.calls,
|
||||||
|
totalTime = stats.totalTime,
|
||||||
|
calls = stats.calls,
|
||||||
|
minTime = stats.minTime,
|
||||||
|
maxTime = stats.maxTime,
|
||||||
|
lastTime = stats.lastTime
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local Container = {}
|
||||||
|
|
||||||
|
function Container:benchmarkContainer(methodName)
|
||||||
|
self:benchmark(methodName)
|
||||||
|
|
||||||
|
for _, child in pairs(self.get("children")) do
|
||||||
|
child:benchmark(methodName)
|
||||||
|
|
||||||
|
if child:isType("Container") then
|
||||||
|
child:benchmarkContainer(methodName)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function Container:logContainerBenchmarks(methodName, depth)
|
||||||
|
depth = depth or 0
|
||||||
|
local indent = string.rep(" ", depth)
|
||||||
|
local childrenTotalTime = 0
|
||||||
|
local childrenStats = {}
|
||||||
|
|
||||||
|
for _, child in pairs(self.get("children")) do
|
||||||
|
local profile = activeProfiles[child]
|
||||||
|
if profile and profile.methods[methodName] then
|
||||||
|
local stats = profile.methods[methodName]
|
||||||
|
childrenTotalTime = childrenTotalTime + stats.totalTime
|
||||||
|
table.insert(childrenStats, {
|
||||||
|
element = child,
|
||||||
|
type = child.get("type"),
|
||||||
|
calls = stats.calls,
|
||||||
|
totalTime = stats.totalTime,
|
||||||
|
avgTime = stats.totalTime / stats.calls
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local profile = activeProfiles[self]
|
||||||
|
if profile and profile.methods[methodName] then
|
||||||
|
local stats = profile.methods[methodName]
|
||||||
|
local selfTime = stats.totalTime - childrenTotalTime
|
||||||
|
local avgSelfTime = selfTime / stats.calls
|
||||||
|
|
||||||
|
log.info(string.format(
|
||||||
|
"%sBenchmark %s (%s): " ..
|
||||||
|
"%.2fms/call (Self: %.2fms/call) " ..
|
||||||
|
"[Total: %dms, Calls: %d]",
|
||||||
|
indent,
|
||||||
|
self.get("type"),
|
||||||
|
methodName,
|
||||||
|
stats.totalTime / stats.calls,
|
||||||
|
avgSelfTime,
|
||||||
|
stats.totalTime,
|
||||||
|
stats.calls
|
||||||
|
))
|
||||||
|
|
||||||
|
if #childrenStats > 0 then
|
||||||
|
for _, childStat in ipairs(childrenStats) do
|
||||||
|
if childStat.element:isType("Container") then
|
||||||
|
childStat.element:logContainerBenchmarks(methodName, depth + 1)
|
||||||
|
else
|
||||||
|
log.info(string.format("%s> %s: %.2fms/call [Total: %dms, Calls: %d]",
|
||||||
|
indent .. " ",
|
||||||
|
childStat.type,
|
||||||
|
childStat.avgTime,
|
||||||
|
childStat.totalTime,
|
||||||
|
childStat.calls
|
||||||
|
))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function Container:stopContainerBenchmark(methodName)
|
||||||
|
for _, child in pairs(self.get("children")) do
|
||||||
|
if child:isType("Container") then
|
||||||
|
child:stopContainerBenchmark(methodName)
|
||||||
|
else
|
||||||
|
child:stopBenchmark(methodName)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
self:stopBenchmark(methodName)
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
local API = {
|
||||||
|
start = function(name, options)
|
||||||
|
options = options or {}
|
||||||
|
local profile = createProfile()
|
||||||
|
profile.name = name
|
||||||
|
profile.startTime = os.clock() * 1000
|
||||||
|
profile.custom = true
|
||||||
|
activeProfiles[name] = profile
|
||||||
|
end,
|
||||||
|
|
||||||
|
stop = function(name)
|
||||||
|
local profile = activeProfiles[name]
|
||||||
|
if not profile or not profile.custom then return end
|
||||||
|
|
||||||
|
local endTime = os.clock() * 1000
|
||||||
|
local duration = endTime - profile.startTime
|
||||||
|
|
||||||
|
profile.calls = profile.calls + 1
|
||||||
|
profile.totalTime = profile.totalTime + duration
|
||||||
|
profile.minTime = math.min(profile.minTime, duration)
|
||||||
|
profile.maxTime = math.max(profile.maxTime, duration)
|
||||||
|
profile.lastTime = duration
|
||||||
|
|
||||||
|
log.info(string.format(
|
||||||
|
"Custom Benchmark '%s': " ..
|
||||||
|
"Calls: %d " ..
|
||||||
|
"Average time: %.2fms " ..
|
||||||
|
"Min time: %.2fms " ..
|
||||||
|
"Max time: %.2fms " ..
|
||||||
|
"Last time: %.2fms " ..
|
||||||
|
"Total time: %.2fms",
|
||||||
|
name,
|
||||||
|
profile.calls,
|
||||||
|
profile.totalTime / profile.calls,
|
||||||
|
profile.minTime,
|
||||||
|
profile.maxTime,
|
||||||
|
profile.lastTime,
|
||||||
|
profile.totalTime
|
||||||
|
))
|
||||||
|
end,
|
||||||
|
|
||||||
|
getStats = function(name)
|
||||||
|
local profile = activeProfiles[name]
|
||||||
|
if not profile then return nil end
|
||||||
|
|
||||||
|
return {
|
||||||
|
averageTime = profile.totalTime / profile.calls,
|
||||||
|
totalTime = profile.totalTime,
|
||||||
|
calls = profile.calls,
|
||||||
|
minTime = profile.minTime,
|
||||||
|
maxTime = profile.maxTime,
|
||||||
|
lastTime = profile.lastTime
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
|
||||||
|
clear = function(name)
|
||||||
|
activeProfiles[name] = nil
|
||||||
|
end,
|
||||||
|
|
||||||
|
clearAll = function()
|
||||||
|
for k,v in pairs(activeProfiles) do
|
||||||
|
if v.custom then
|
||||||
|
activeProfiles[k] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
BaseElement = BaseElement,
|
||||||
|
Container = Container,
|
||||||
|
API = API
|
||||||
|
}
|
||||||
182
src/plugins/debug.lua
Normal file
182
src/plugins/debug.lua
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
local log = require("log")
|
||||||
|
local tHex = require("libraries/colorHex")
|
||||||
|
|
||||||
|
local maxLines = 10
|
||||||
|
local isVisible = false
|
||||||
|
|
||||||
|
local function createDebugger(element)
|
||||||
|
local elementInfo = {
|
||||||
|
renderCount = 0,
|
||||||
|
eventCount = {},
|
||||||
|
lastRender = os.epoch("utc"),
|
||||||
|
properties = {},
|
||||||
|
children = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
trackProperty = function(name, value)
|
||||||
|
elementInfo.properties[name] = value
|
||||||
|
end,
|
||||||
|
|
||||||
|
trackRender = function()
|
||||||
|
elementInfo.renderCount = elementInfo.renderCount + 1
|
||||||
|
elementInfo.lastRender = os.epoch("utc")
|
||||||
|
end,
|
||||||
|
|
||||||
|
trackEvent = function(event)
|
||||||
|
elementInfo.eventCount[event] = (elementInfo.eventCount[event] or 0) + 1
|
||||||
|
end,
|
||||||
|
|
||||||
|
dump = function()
|
||||||
|
return {
|
||||||
|
type = element.get("type"),
|
||||||
|
id = element.get("id"),
|
||||||
|
stats = elementInfo
|
||||||
|
}
|
||||||
|
end
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local BaseElement = {
|
||||||
|
debug = function(self, level)
|
||||||
|
self._debugger = createDebugger(self)
|
||||||
|
self._debugLevel = level or DEBUG_LEVELS.INFO
|
||||||
|
return self
|
||||||
|
end,
|
||||||
|
|
||||||
|
dumpDebug = function(self)
|
||||||
|
if not self._debugger then return end
|
||||||
|
return self._debugger.dump()
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
local BaseFrame = {
|
||||||
|
showDebugLog = function(self)
|
||||||
|
if not self._debugFrame then
|
||||||
|
local width = self.get("width")
|
||||||
|
local height = self.get("height")
|
||||||
|
self._debugFrame = self:addFrame()
|
||||||
|
:setWidth(width)
|
||||||
|
:setHeight(height)
|
||||||
|
:setBackground(colors.black)
|
||||||
|
:setZ(999)
|
||||||
|
:listenEvent("mouse_scroll", true)
|
||||||
|
|
||||||
|
self._debugFrame:addButton()
|
||||||
|
:setWidth(9)
|
||||||
|
:setHeight(1)
|
||||||
|
:setX(width - 8)
|
||||||
|
:setY(height)
|
||||||
|
:setText("Close")
|
||||||
|
:setBackground(colors.red)
|
||||||
|
:onMouseClick(function()
|
||||||
|
self:hideDebugLog()
|
||||||
|
end)
|
||||||
|
|
||||||
|
self._debugFrame._scrollOffset = 0
|
||||||
|
self._debugFrame._processedLogs = {}
|
||||||
|
|
||||||
|
local function wrapText(text, width)
|
||||||
|
local lines = {}
|
||||||
|
while #text > 0 do
|
||||||
|
local line = text:sub(1, width)
|
||||||
|
table.insert(lines, line)
|
||||||
|
text = text:sub(width + 1)
|
||||||
|
end
|
||||||
|
return lines
|
||||||
|
end
|
||||||
|
|
||||||
|
local function processLogs()
|
||||||
|
local processed = {}
|
||||||
|
local width = self._debugFrame.get("width")
|
||||||
|
|
||||||
|
for _, entry in ipairs(log._logs) do
|
||||||
|
local lines = wrapText(entry.message, width)
|
||||||
|
for _, line in ipairs(lines) do
|
||||||
|
table.insert(processed, {
|
||||||
|
text = line,
|
||||||
|
level = entry.level
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return processed
|
||||||
|
end
|
||||||
|
|
||||||
|
local totalLines = #processLogs() - self.get("height")
|
||||||
|
self._scrollOffset = totalLines
|
||||||
|
|
||||||
|
local originalRender = self._debugFrame.render
|
||||||
|
self._debugFrame.render = function(frame)
|
||||||
|
originalRender(frame)
|
||||||
|
frame._processedLogs = processLogs()
|
||||||
|
|
||||||
|
local height = frame.get("height")-2
|
||||||
|
local totalLines = #frame._processedLogs
|
||||||
|
local maxScroll = math.max(0, totalLines - height)
|
||||||
|
frame._scrollOffset = math.min(frame._scrollOffset, maxScroll)
|
||||||
|
|
||||||
|
for i = 1, height-2 do
|
||||||
|
local logIndex = i + frame._scrollOffset
|
||||||
|
local entry = frame._processedLogs[logIndex]
|
||||||
|
|
||||||
|
if entry then
|
||||||
|
local color = entry.level == log.LEVEL.ERROR and colors.red
|
||||||
|
or entry.level == log.LEVEL.WARN and colors.yellow
|
||||||
|
or entry.level == log.LEVEL.DEBUG and colors.lightGray
|
||||||
|
or colors.white
|
||||||
|
|
||||||
|
frame:textFg(2, i, entry.text, color)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local baseDispatchEvent = self._debugFrame.dispatchEvent
|
||||||
|
self._debugFrame.dispatchEvent = function(self, event, direction, ...)
|
||||||
|
if(event == "mouse_scroll") then
|
||||||
|
self._scrollOffset = math.max(0, self._scrollOffset + direction)
|
||||||
|
self:updateRender()
|
||||||
|
return true
|
||||||
|
else
|
||||||
|
baseDispatchEvent(self, event, direction, ...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self._debugFrame.set("visible", true)
|
||||||
|
return self
|
||||||
|
end,
|
||||||
|
|
||||||
|
hideDebugLog = function(self)
|
||||||
|
if self._debugFrame then
|
||||||
|
self._debugFrame.set("visible", false)
|
||||||
|
end
|
||||||
|
return self
|
||||||
|
end,
|
||||||
|
|
||||||
|
toggleDebugLog = function(self)
|
||||||
|
if self._debugFrame and self._debugFrame:isVisible() then
|
||||||
|
self:hideDebugLog()
|
||||||
|
else
|
||||||
|
self:showDebugLog()
|
||||||
|
end
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
local Container = {
|
||||||
|
debugChildren = function(self, level)
|
||||||
|
self:debug(level)
|
||||||
|
for _, child in pairs(self.get("children")) do
|
||||||
|
if child.debug then
|
||||||
|
child:debug(level)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
BaseElement = BaseElement,
|
||||||
|
Container = Container,
|
||||||
|
BaseFrame = BaseFrame,
|
||||||
|
}
|
||||||
@@ -18,14 +18,18 @@ local mathEnv = {
|
|||||||
end
|
end
|
||||||
}
|
}
|
||||||
|
|
||||||
local function parseExpression(expr, element)
|
local function parseExpression(expr, element, propName)
|
||||||
expr = expr:gsub("^{(.+)}$", "%1")
|
expr = expr:gsub("^{(.+)}$", "%1")
|
||||||
|
|
||||||
for k,v in pairs(colors) do
|
expr = expr:gsub("([%w_]+)%$([%w_]+)", function(obj, prop)
|
||||||
if type(k) == "string" then
|
if obj == "self" then
|
||||||
expr = expr:gsub("%f[%w]"..k.."%f[%W]", "colors."..k)
|
return string.format('__getState("%s")', prop)
|
||||||
end
|
elseif obj == "parent" then
|
||||||
|
return string.format('__getParentState("%s")', prop)
|
||||||
|
else
|
||||||
|
return string.format('__getElementState("%s", "%s")', obj, prop)
|
||||||
end
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
expr = expr:gsub("([%w_]+)%.([%w_]+)", function(obj, prop)
|
expr = expr:gsub("([%w_]+)%.([%w_]+)", function(obj, prop)
|
||||||
if protectedNames[obj] then
|
if protectedNames[obj] then
|
||||||
@@ -37,6 +41,23 @@ local function parseExpression(expr, element)
|
|||||||
local env = setmetatable({
|
local env = setmetatable({
|
||||||
colors = colors,
|
colors = colors,
|
||||||
math = math,
|
math = math,
|
||||||
|
tostring = tostring,
|
||||||
|
tonumber = tonumber,
|
||||||
|
__getState = function(prop)
|
||||||
|
return element:getState(prop)
|
||||||
|
end,
|
||||||
|
__getParentState = function(prop)
|
||||||
|
return element.parent:getState(prop)
|
||||||
|
end,
|
||||||
|
__getElementState = function(objName, prop)
|
||||||
|
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:getState(prop).value
|
||||||
|
end,
|
||||||
__getProperty = function(objName, propName)
|
__getProperty = function(objName, propName)
|
||||||
if objName == "self" then
|
if objName == "self" then
|
||||||
return element.get(propName)
|
return element.get(propName)
|
||||||
@@ -55,6 +76,12 @@ local function parseExpression(expr, element)
|
|||||||
end
|
end
|
||||||
}, { __index = mathEnv })
|
}, { __index = mathEnv })
|
||||||
|
|
||||||
|
if(element._properties[propName].type == "string")then
|
||||||
|
expr = "tostring(" .. expr .. ")"
|
||||||
|
elseif(element._properties[propName].type == "number")then
|
||||||
|
expr = "tonumber(" .. expr .. ")"
|
||||||
|
end
|
||||||
|
|
||||||
local func, err = load("return "..expr, "reactive", "t", env)
|
local func, err = load("return "..expr, "reactive", "t", env)
|
||||||
if not func then
|
if not func then
|
||||||
errorManager.header = "Reactive evaluation error"
|
errorManager.header = "Reactive evaluation error"
|
||||||
@@ -68,7 +95,8 @@ end
|
|||||||
local function validateReferences(expr, element)
|
local function validateReferences(expr, element)
|
||||||
for ref in expr:gmatch("([%w_]+)%.") do
|
for ref in expr:gmatch("([%w_]+)%.") do
|
||||||
if not protectedNames[ref] then
|
if not protectedNames[ref] then
|
||||||
if ref == "parent" then
|
if ref == "self" then
|
||||||
|
elseif ref == "parent" then
|
||||||
if not element.parent then
|
if not element.parent then
|
||||||
errorManager.header = "Reactive evaluation error"
|
errorManager.header = "Reactive evaluation error"
|
||||||
errorManager.error("No parent element available")
|
errorManager.error("No parent element available")
|
||||||
@@ -87,12 +115,19 @@ local function validateReferences(expr, element)
|
|||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
local functionCache = {}
|
local functionCache = setmetatable({}, {__mode = "k"})
|
||||||
local observerCache = setmetatable({}, {__mode = "k"})
|
|
||||||
|
|
||||||
local function setupObservers(element, expr)
|
local observerCache = setmetatable({}, {
|
||||||
if observerCache[element] then
|
__mode = "k",
|
||||||
for _, observer in ipairs(observerCache[element]) do
|
__index = function(t, k)
|
||||||
|
t[k] = {}
|
||||||
|
return t[k]
|
||||||
|
end
|
||||||
|
})
|
||||||
|
|
||||||
|
local function setupObservers(element, expr, propertyName)
|
||||||
|
if observerCache[element][propertyName] then
|
||||||
|
for _, observer in ipairs(observerCache[element][propertyName]) do
|
||||||
observer.target:removeObserver(observer.property, observer.callback)
|
observer.target:removeObserver(observer.property, observer.callback)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -123,7 +158,7 @@ local function setupObservers(element, expr)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
observerCache[element] = observers
|
observerCache[element][propertyName] = observers
|
||||||
end
|
end
|
||||||
|
|
||||||
PropertySystem.addSetterHook(function(element, propertyName, value, config)
|
PropertySystem.addSetterHook(function(element, propertyName, value, config)
|
||||||
@@ -133,15 +168,18 @@ PropertySystem.addSetterHook(function(element, propertyName, value, config)
|
|||||||
return config.default
|
return config.default
|
||||||
end
|
end
|
||||||
|
|
||||||
setupObservers(element, expr)
|
setupObservers(element, expr, propertyName)
|
||||||
|
|
||||||
if not functionCache[value] then
|
if not functionCache[element] then
|
||||||
local parsedFunc = parseExpression(value, element)
|
functionCache[element] = {}
|
||||||
functionCache[value] = parsedFunc
|
end
|
||||||
|
if not functionCache[element][value] then
|
||||||
|
local parsedFunc = parseExpression(value, element, propertyName)
|
||||||
|
functionCache[element][value] = parsedFunc
|
||||||
end
|
end
|
||||||
|
|
||||||
return function(self)
|
return function(self)
|
||||||
local success, result = pcall(functionCache[value])
|
local success, result = pcall(functionCache[element][value])
|
||||||
if not success then
|
if not success then
|
||||||
errorManager.header = "Reactive evaluation error"
|
errorManager.header = "Reactive evaluation error"
|
||||||
if type(result) == "string" then
|
if type(result) == "string" then
|
||||||
@@ -161,8 +199,10 @@ local BaseElement = {}
|
|||||||
BaseElement.hooks = {
|
BaseElement.hooks = {
|
||||||
destroy = function(self)
|
destroy = function(self)
|
||||||
if observerCache[self] then
|
if observerCache[self] then
|
||||||
for _, observer in ipairs(observerCache[self]) do
|
for propName, observers in pairs(observerCache[self]) do
|
||||||
observer.target:observe(observer.property, observer.callback)
|
for _, observer in ipairs(observers) do
|
||||||
|
observer.target:removeObserver(observer.property, observer.callback)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
observerCache[self] = nil
|
observerCache[self] = nil
|
||||||
end
|
end
|
||||||
|
|||||||
135
src/plugins/state.lua
Normal file
135
src/plugins/state.lua
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
local PropertySystem = require("propertySystem")
|
||||||
|
local errorManager = require("errorManager")
|
||||||
|
local BaseElement = {}
|
||||||
|
|
||||||
|
function BaseElement.setup(element)
|
||||||
|
element.defineProperty(element, "states", {default = {}, type = "table"})
|
||||||
|
element.defineProperty(element, "computedStates", {default = {}, type = "table"})
|
||||||
|
element.defineProperty(element, "stateUpdate", {
|
||||||
|
default = {key = "", value = nil, oldValue = nil},
|
||||||
|
type = "table"
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
function BaseElement:initializeState(name, default, canTriggerRender, persist, path)
|
||||||
|
local states = self.get("states")
|
||||||
|
|
||||||
|
if states[name] then
|
||||||
|
errorManager.error("State '" .. name .. "' already exists")
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
if persist then
|
||||||
|
local file = path or ("states/" .. self.get("name") .. "_" .. name .. ".state")
|
||||||
|
|
||||||
|
if fs.exists(file) then
|
||||||
|
local f = fs.open(file, "r")
|
||||||
|
states[name] = {
|
||||||
|
value = textutils.unserialize(f.readAll()),
|
||||||
|
persist = true,
|
||||||
|
file = file
|
||||||
|
}
|
||||||
|
f.close()
|
||||||
|
else
|
||||||
|
states[name] = {
|
||||||
|
value = default,
|
||||||
|
persist = true,
|
||||||
|
file = file,
|
||||||
|
canTriggerRender = canTriggerRender
|
||||||
|
}
|
||||||
|
end
|
||||||
|
else
|
||||||
|
states[name] = {
|
||||||
|
value = default,
|
||||||
|
canTriggerRender = canTriggerRender
|
||||||
|
}
|
||||||
|
end
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function BaseElement:setState(name, value)
|
||||||
|
local states = self.get("states")
|
||||||
|
if not states[name] then
|
||||||
|
error("State '"..name.."' not initialized")
|
||||||
|
end
|
||||||
|
|
||||||
|
local oldValue = states[name].value
|
||||||
|
states[name].value = value
|
||||||
|
|
||||||
|
if states[name].persist then
|
||||||
|
local dir = fs.getDir(states[name].file)
|
||||||
|
if not fs.exists(dir) then
|
||||||
|
fs.makeDir(dir)
|
||||||
|
end
|
||||||
|
local f = fs.open(states[name].file, "w")
|
||||||
|
f.write(textutils.serialize(value))
|
||||||
|
f.close()
|
||||||
|
end
|
||||||
|
|
||||||
|
if states[name].canTriggerRender then
|
||||||
|
self:updateRender()
|
||||||
|
end
|
||||||
|
|
||||||
|
self.set("stateUpdate", {
|
||||||
|
key = name,
|
||||||
|
value = value,
|
||||||
|
oldValue = oldValue
|
||||||
|
})
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function BaseElement:getState(name)
|
||||||
|
local states = self.get("states")
|
||||||
|
if not states[name] then
|
||||||
|
errorManager.error("State '"..name.."' not initialized")
|
||||||
|
end
|
||||||
|
return states[name].value
|
||||||
|
end
|
||||||
|
|
||||||
|
function BaseElement:computed(key, computeFn)
|
||||||
|
local computed = self.get("computedStates")
|
||||||
|
computed[key] = setmetatable({}, {
|
||||||
|
__call = function()
|
||||||
|
return computeFn(self)
|
||||||
|
end
|
||||||
|
})
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function BaseElement:shareState(stateKey, ...)
|
||||||
|
local value = self:getState(stateKey)
|
||||||
|
|
||||||
|
for _, element in ipairs({...}) do
|
||||||
|
if element.get("states")[stateKey] then
|
||||||
|
errorManager.error("Cannot share state '" .. stateKey .. "': Target element already has this state")
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
element:initializeState(stateKey, value)
|
||||||
|
|
||||||
|
self:observe("stateUpdate", function(self, update)
|
||||||
|
if update.key == stateKey then
|
||||||
|
element:setState(stateKey, update.value)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function BaseElement:onStateChange(stateName, callback)
|
||||||
|
if not self.get("states")[stateName] then
|
||||||
|
errorManager.error("Cannot observe state '" .. stateName .. "': State not initialized")
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
self:observe("stateUpdate", function(self, update)
|
||||||
|
if update.key == stateName then
|
||||||
|
callback(self, update.value, update.oldValue)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
BaseElement = BaseElement
|
||||||
|
}
|
||||||
@@ -1,33 +1,36 @@
|
|||||||
-- Has to be reworked
|
|
||||||
|
|
||||||
local Theme = {}
|
|
||||||
|
|
||||||
local defaultTheme = {
|
local defaultTheme = {
|
||||||
colors = {
|
default = {
|
||||||
primary = colors.blue,
|
background = colors.lightGray,
|
||||||
secondary = colors.cyan,
|
foreground = colors.black,
|
||||||
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",
|
|
||||||
},
|
},
|
||||||
|
BaseFrame = {
|
||||||
|
background = colors.white,
|
||||||
|
foreground = colors.black,
|
||||||
|
|
||||||
Frame = {
|
Frame = {
|
||||||
background = "background",
|
background = colors.gray,
|
||||||
foreground = "text"
|
|
||||||
|
Button = {
|
||||||
|
background = "{self.clicked and colors.black or colors.blue}",
|
||||||
|
foreground = "{self.clicked and colors.blue or colors.white}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
Button = {
|
||||||
|
background = "{self.clicked and colors.black or colors.cyan}",
|
||||||
|
foreground = "{self.clicked and colors.cyan or colors.black}"
|
||||||
|
},
|
||||||
|
|
||||||
|
Input = {
|
||||||
|
background = "{self.focused and colors.cyan or colors.lightGray}",
|
||||||
|
foreground = colors.black,
|
||||||
|
placeholderColor = colors.gray
|
||||||
|
},
|
||||||
|
|
||||||
|
List = {
|
||||||
|
background = colors.cyan,
|
||||||
|
foreground = colors.black,
|
||||||
|
selectedColor = colors.blue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,10 +41,6 @@ local themes = {
|
|||||||
|
|
||||||
local currentTheme = "default"
|
local currentTheme = "default"
|
||||||
|
|
||||||
function Theme.registerTheme(name, theme)
|
|
||||||
themes[name] = theme
|
|
||||||
end
|
|
||||||
|
|
||||||
local function resolveThemeValue(value, theme)
|
local function resolveThemeValue(value, theme)
|
||||||
if type(value) == "string" and theme.colors[value] then
|
if type(value) == "string" and theme.colors[value] then
|
||||||
return theme.colors[value]
|
return theme.colors[value]
|
||||||
@@ -49,10 +48,50 @@ local function resolveThemeValue(value, theme)
|
|||||||
return value
|
return value
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function getThemeForElement(element)
|
||||||
|
local path = {}
|
||||||
|
local current = element
|
||||||
|
|
||||||
|
while current do
|
||||||
|
table.insert(path, 1, current.get("type"))
|
||||||
|
current = current.parent
|
||||||
|
end
|
||||||
|
|
||||||
|
local result = {}
|
||||||
|
local current = defaultTheme
|
||||||
|
|
||||||
|
for _, elementType in ipairs(path) do
|
||||||
|
if current[elementType] then
|
||||||
|
for k,v in pairs(current[elementType]) do
|
||||||
|
result[k] = v
|
||||||
|
end
|
||||||
|
current = current[elementType]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
local function applyTheme(element, props)
|
||||||
|
|
||||||
|
local theme = getThemeForElement(element)
|
||||||
|
|
||||||
|
if props then
|
||||||
|
for k,v in pairs(props) do
|
||||||
|
theme[k] = v
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for k,v in pairs(theme) do
|
||||||
|
if element:getPropertyConfig(k) then
|
||||||
|
element.set(k, v)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local BaseElement = {
|
local BaseElement = {
|
||||||
hooks = {
|
hooks = {
|
||||||
init = function(self)
|
init = function(self)
|
||||||
-- Theme Properties für das Element registrieren
|
|
||||||
self.defineProperty(self, "theme", {
|
self.defineProperty(self, "theme", {
|
||||||
default = currentTheme,
|
default = currentTheme,
|
||||||
type = "string",
|
type = "string",
|
||||||
@@ -79,21 +118,67 @@ function BaseElement:applyTheme(themeName)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local BaseFrame = {
|
||||||
|
hooks = {
|
||||||
|
init = function(self)
|
||||||
|
applyTheme(self)
|
||||||
|
end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
local Container = {
|
local Container = {
|
||||||
hooks = {
|
hooks = {
|
||||||
addChild = function(self, child)
|
init = function(self)
|
||||||
if self.get("themeInherit") then
|
for k, _ in pairs(self.basalt.getElementManager().getElementList()) do
|
||||||
child.set("theme", self.get("theme"))
|
local capitalizedName = k:sub(1,1):upper() .. k:sub(2)
|
||||||
|
if capitalizedName ~= "BaseFrame" then
|
||||||
|
local methodName = "add"..capitalizedName
|
||||||
|
local original = self[methodName]
|
||||||
|
if original then
|
||||||
|
self[methodName] = function(self, name, props)
|
||||||
|
if type(name) == "table" then
|
||||||
|
props = name
|
||||||
|
name = nil
|
||||||
|
end
|
||||||
|
local element = original(self, name)
|
||||||
|
applyTheme(element, props)
|
||||||
|
return element
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function Container.setup(element)
|
local themeAPI = {
|
||||||
element.defineProperty(element, "themeInherit", {default = true, type = "boolean"})
|
setTheme = function(newTheme)
|
||||||
end
|
defaultTheme = newTheme
|
||||||
|
end,
|
||||||
|
|
||||||
|
getTheme = function()
|
||||||
|
return defaultTheme
|
||||||
|
end,
|
||||||
|
|
||||||
|
loadTheme = function(path)
|
||||||
|
local file = fs.open(path, "r")
|
||||||
|
if file then
|
||||||
|
local content = file.readAll()
|
||||||
|
file.close()
|
||||||
|
defaultTheme = textutils.unserializeJSON(content)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
local Theme = {
|
||||||
|
setup = function(basalt)
|
||||||
|
basalt.setTheme(defaultTheme)
|
||||||
|
end,
|
||||||
|
|
||||||
return {
|
|
||||||
BaseElement = BaseElement,
|
BaseElement = BaseElement,
|
||||||
|
BaseFrame = BaseFrame,
|
||||||
Container = Container,
|
Container = Container,
|
||||||
|
API = themeAPI
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return Theme
|
||||||
|
|||||||
@@ -16,6 +16,16 @@ function PropertySystem.addSetterHook(hook)
|
|||||||
table.insert(PropertySystem._setterHooks, hook)
|
table.insert(PropertySystem._setterHooks, hook)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function applyHooks(element, propertyName, value, config)
|
||||||
|
for _, hook in ipairs(PropertySystem._setterHooks) do
|
||||||
|
local newValue = hook(element, propertyName, value, config)
|
||||||
|
if newValue ~= nil then
|
||||||
|
value = newValue
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return value
|
||||||
|
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 = {}
|
||||||
@@ -42,16 +52,8 @@ function PropertySystem.defineProperty(class, name, config)
|
|||||||
|
|
||||||
class["set" .. capitalizedName] = function(self, value, ...)
|
class["set" .. capitalizedName] = function(self, value, ...)
|
||||||
expect(1, self, "element")
|
expect(1, self, "element")
|
||||||
|
value = applyHooks(self, name, value, config)
|
||||||
|
|
||||||
-- 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
|
if type(value) ~= "function" then
|
||||||
expect(2, value, config.type)
|
expect(2, value, config.type)
|
||||||
end
|
end
|
||||||
@@ -65,6 +67,32 @@ function PropertySystem.defineProperty(class, name, config)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function PropertySystem.combineProperties(class, name, ...)
|
||||||
|
local properties = {...}
|
||||||
|
for k,v in pairs(properties)do
|
||||||
|
if not class._properties[v] then errorManager.error("Property not found: "..v) end
|
||||||
|
end
|
||||||
|
local capitalizedName = name:sub(1,1):upper() .. name:sub(2)
|
||||||
|
|
||||||
|
class["get" .. capitalizedName] = function(self, ...)
|
||||||
|
expect(1, self, "element")
|
||||||
|
local value = {}
|
||||||
|
for _,v in pairs(properties)do
|
||||||
|
value[v] = self.get(v)
|
||||||
|
end
|
||||||
|
return table.unpack(value)
|
||||||
|
end
|
||||||
|
|
||||||
|
class["set" .. capitalizedName] = function(self, ...)
|
||||||
|
expect(1, self, "element")
|
||||||
|
local values = {...}
|
||||||
|
for i,v in pairs(properties)do
|
||||||
|
self.set(v, values[i])
|
||||||
|
end
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
--- Creates a blueprint of an element class with all its properties
|
--- Creates a blueprint of an element class with all its properties
|
||||||
--- @param elementClass table The element class to create a blueprint from
|
--- @param elementClass table The element class to create a blueprint from
|
||||||
--- @return table blueprint A table containing all property definitions
|
--- @return table blueprint A table containing all property definitions
|
||||||
@@ -195,7 +223,7 @@ function PropertySystem:__init()
|
|||||||
if config.canTriggerRender then
|
if config.canTriggerRender then
|
||||||
self:updateRender()
|
self:updateRender()
|
||||||
end
|
end
|
||||||
self._values[name] = value
|
self._values[name] = applyHooks(self, name, value, config)
|
||||||
if oldValue ~= value and self._observers[name] then
|
if oldValue ~= value and 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, value, oldValue)
|
||||||
@@ -249,10 +277,12 @@ function PropertySystem:__init()
|
|||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
__newindex = function(t, k, v)
|
__newindex = function(t, k, v)
|
||||||
if self._properties[k] then
|
local config = self._properties[k]
|
||||||
if self._properties[k].setter then
|
if config then
|
||||||
v = self._properties[k].setter(self, v)
|
if config.setter then
|
||||||
|
v = config.setter(self, v)
|
||||||
end
|
end
|
||||||
|
v = applyHooks(self, k, v, config)
|
||||||
self:_updateProperty(k, v)
|
self:_updateProperty(k, v)
|
||||||
else
|
else
|
||||||
rawset(t, k, v)
|
rawset(t, k, v)
|
||||||
@@ -278,13 +308,11 @@ end
|
|||||||
|
|
||||||
function PropertySystem:_updateProperty(name, value)
|
function PropertySystem:_updateProperty(name, value)
|
||||||
local oldValue = self._values[name]
|
local oldValue = self._values[name]
|
||||||
-- Wenn der alte Wert eine Funktion ist, müssen wir den tatsächlichen Wert holen
|
|
||||||
if type(oldValue) == "function" then
|
if type(oldValue) == "function" then
|
||||||
oldValue = oldValue(self)
|
oldValue = oldValue(self)
|
||||||
end
|
end
|
||||||
|
|
||||||
self._values[name] = value
|
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
|
local newValue = type(value) == "function" and value(self) or value
|
||||||
|
|
||||||
if oldValue ~= newValue then
|
if oldValue ~= newValue then
|
||||||
|
|||||||
@@ -144,8 +144,6 @@ function Render:clear(bg)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function Render:render()
|
function Render:render()
|
||||||
local benchmark = require("benchmark")
|
|
||||||
benchmark.start("render")
|
|
||||||
|
|
||||||
local mergedRects = {}
|
local mergedRects = {}
|
||||||
for _, rect in ipairs(self.buffer.dirtyRects) do
|
for _, rect in ipairs(self.buffer.dirtyRects) do
|
||||||
@@ -162,7 +160,6 @@ function Render:render()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Nur die Dirty Rectangles rendern
|
|
||||||
for _, rect in ipairs(mergedRects) do
|
for _, rect in ipairs(mergedRects) do
|
||||||
for y = rect.y, rect.y + rect.height - 1 do
|
for y = rect.y, rect.y + rect.height - 1 do
|
||||||
if y >= 1 and y <= self.height then
|
if y >= 1 and y <= self.height then
|
||||||
@@ -176,7 +173,6 @@ function Render:render()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
benchmark.update("render")
|
|
||||||
self.buffer.dirtyRects = {}
|
self.buffer.dirtyRects = {}
|
||||||
|
|
||||||
if self.blink then
|
if self.blink then
|
||||||
@@ -186,7 +182,6 @@ function Render:render()
|
|||||||
self.terminal.setCursorBlink(false)
|
self.terminal.setCursorBlink(false)
|
||||||
end
|
end
|
||||||
|
|
||||||
--benchmark.stop("render")
|
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user