- added List

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

View File

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

View File

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

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

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

View File

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

View File

@@ -1,4 +1,5 @@
local VisualElement = require("elements/VisualElement")
local tHex = require("libraries/colorHex")
---@class Input : VisualElement
local Input = setmetatable({}, VisualElement)
@@ -10,19 +11,26 @@ Input.defineProperty(Input, "text", {default = "", type = "string", canTriggerRe
---@property cursorPos number Input - current cursor position
Input.defineProperty(Input, "cursorPos", {default = 1, type = "number"})
---@property viewOffset number Input - offset für Text-Viewport
Input.defineProperty(Input, "viewOffset", {default = 0, type = "number"})
---@property viewOffset number Input - offset of view
Input.defineProperty(Input, "viewOffset", {default = 0, type = "number", canTriggerRender = true})
-- Neue Properties
Input.defineProperty(Input, "maxLength", {default = nil, type = "number"})
Input.defineProperty(Input, "placeholder", {default = "asd", type = "string"})
Input.defineProperty(Input, "placeholderColor", {default = colors.gray, type = "number"})
Input.defineProperty(Input, "focusedColor", {default = colors.blue, type = "number"})
Input.defineProperty(Input, "pattern", {default = nil, type = "string"})
Input.listenTo(Input, "mouse_click")
Input.listenTo(Input, "key")
Input.listenTo(Input, "char")
--- Creates a new Input instance
--- @param id string The unique identifier for this element
--- @param props table The properties to initialize the element with
--- @param basalt table The basalt instance
--- @return Input object The newly created Input instance
--- @usage local element = Input.new("myId", basalt)
function Input.new(id, basalt)
function Input.new(props, basalt)
local self = setmetatable({}, Input):__init()
self:init(id, basalt)
self.set("width", 8)
@@ -30,8 +38,8 @@ function Input.new(id, basalt)
return self
end
function Input:init(id, basalt)
VisualElement.init(self, id, basalt)
function Input:init(props, basalt)
VisualElement.init(self, props, basalt)
self.set("type", "Input")
end
@@ -39,8 +47,15 @@ function Input:char(char)
if not self.get("focused") then return end
local text = self.get("text")
local pos = self.get("cursorPos")
local maxLength = self.get("maxLength")
local pattern = self.get("pattern")
if maxLength and #text >= maxLength then return end
if pattern and not char:match(pattern) then return end
self.set("text", text:sub(1, pos-1) .. char .. text:sub(pos))
self.set("cursorPos", pos + 1)
self:updateRender()
self:updateViewport()
end
@@ -48,64 +63,91 @@ function Input:key(key)
if not self.get("focused") then return end
local pos = self.get("cursorPos")
local text = self.get("text")
local viewOffset = self.get("viewOffset")
local width = self.get("width")
if key == keys.left and pos > 1 then
self.set("cursorPos", pos - 1)
self:setCursor(pos - 1,1, true)
elseif key == keys.right and pos <= #text then
self.set("cursorPos", pos + 1)
self:setCursor(pos + 1,1, true)
elseif key == keys.backspace and pos > 1 then
self.set("text", text:sub(1, pos-2) .. text:sub(pos))
self.set("cursorPos", pos - 1)
self:setCursor(pos - 1,1, true)
if key == keys.left then
if pos > 1 then
self.set("cursorPos", pos - 1)
if pos - 1 <= viewOffset then
self.set("viewOffset", math.max(0, pos - 2))
end
end
elseif key == keys.right then
if pos <= #text then
self.set("cursorPos", pos + 1)
if pos - viewOffset >= width then
self.set("viewOffset", pos - width + 1)
end
end
elseif key == keys.backspace then
if pos > 1 then
self.set("text", text:sub(1, pos-2) .. text:sub(pos))
self.set("cursorPos", pos - 1)
self:updateRender()
self:updateViewport()
end
end
self:updateViewport()
local relativePos = self.get("cursorPos") - self.get("viewOffset")
self:setCursor(relativePos, 1, true)
end
function Input:focus()
VisualElement.focus(self)
self.set("background", colors.blue)
self:setCursor(1,1, true)
self:updateRender()
end
function Input:blur()
VisualElement.blur(self)
self.set("background", colors.green)
self:updateRender()
end
function Input:mouse_click(button, x, y)
if VisualElement.mouse_click(self, button, x, y) then
local relX, relY = self:getRelativePosition(x, y)
local text = self.get("text")
self:setCursor(math.min(relX, #text + 1), relY, true)
self:set("cursorPos", relX + self.get("viewOffset"))
return true
end
end
function Input:updateViewport()
local width = self.get("width")
local text = self.get("text")
local cursorPos = self.get("cursorPos")
local viewOffset = self.get("viewOffset")
-- Wenn Cursor außerhalb des sichtbaren Bereichs nach rechts
local textLength = #self.get("text")
if cursorPos - viewOffset > width then
self.set("viewOffset", cursorPos - width)
elseif cursorPos <= viewOffset then
self.set("viewOffset", math.max(0, cursorPos - 1))
end
-- Wenn Cursor außerhalb des sichtbaren Bereichs nach links
if cursorPos <= viewOffset then
self.set("viewOffset", cursorPos - 1)
if viewOffset > textLength - width then
self.set("viewOffset", math.max(0, textLength - width))
end
end
function Input:render()
VisualElement.render(self)
local text = self.get("text")
local viewOffset = self.get("viewOffset")
local width = self.get("width")
-- Nur den sichtbaren Teil des Textes rendern
local placeholder = self.get("placeholder")
local focusedColor = self.get("focusedColor")
local focused = self.get("focused")
local width, height = self.get("width"), self.get("height")
self:multiBlit(1, 1, width, height, " ", tHex[self.get("foreground")], tHex[focused and focusedColor or self.get("background")])
if #text == 0 and #placeholder ~= 0 and self.get("focused") == false then
self:textFg(1, 1, placeholder:sub(1, width), self.get("placeholderColor"))
return
end
local visibleText = text:sub(viewOffset + 1, viewOffset + width)
self:textFg(1, 1, visibleText, self.get("foreground"))
if self.get("focused") then
local cursorPos = self.get("cursorPos")
-- Cursor relativ zum Viewport positionieren
self:setCursor(cursorPos - viewOffset, 1, true)
end
end
return Input

View File

@@ -12,7 +12,7 @@ Label.defineProperty(Label, "text", {default = "Label", type = "string", setter
end})
--- Creates a new Label instance
--- @param name table The properties to initialize the element with
--- @param props table The properties to initialize the element with
--- @param basalt table The basalt instance
--- @return Label object The newly created Label instance
--- @usage local element = Label.new("myId", basalt)

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

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

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

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

View File

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

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

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

View File

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