diff --git a/config.lua b/config.lua index be7a4a4..0dc2999 100644 --- a/config.lua +++ b/config.lua @@ -1,115 +1,6086 @@ return { - ["required"] = { - ["src/init.lua"] = "/init.lua", - ["src/render.lua"] = "/render.lua", + ["files"] = { + ["libraries/expect.lua"] = "local errorManager = require("errorManager") + +-- Simple type checking without stack traces +local function expect(position, value, expectedType) + local valueType = type(value) + + if expectedType == "element" then + if valueType == "table" and value.get("type") ~= nil then + return true + end + end + + if expectedType == "color" then + if valueType == "number" and value >= 1 and value <= 32768 then + return true + end + if valueType == "string" and colors[value] then + return true + end + end + + if valueType ~= expectedType then + errorManager.header = "Basalt Type Error" + errorManager.error(string.format( + "Bad argument #%d: expected %s, got %s", + position, + expectedType, + valueType + )) + end + + return true +end + +return expect", + ["elements/BaseElement.lua"] = "local PropertySystem = require("propertySystem") +local uuid = require("/libraries/utils").uuid + +--- The base class for all UI elements in Basalt +--- @class BaseElement : PropertySystem +local BaseElement = setmetatable({}, PropertySystem) +BaseElement.__index = BaseElement +BaseElement._events = {} + +--- @property type string BaseElement The type identifier of the element +BaseElement.defineProperty(BaseElement, "type", {default = {"BaseElement"}, type = "string", setter=function(self, value) + if type(value) == "string" then + table.insert(self._values.type, 1, value) + return self._values.type + end + return value +end, getter = function(self, _, index) + if index~= nil and index < 1 then + return self._values.type + end + return self._values.type[index or 1] +end}) + +--- @property id string BaseElement The unique identifier for the element +BaseElement.defineProperty(BaseElement, "id", {default = "", type = "string", readonly = true}) + +--- @property name string BaseElement The name of the element +BaseElement.defineProperty(BaseElement, "name", {default = "", type = "string"}) + +--- @property eventCallbacks table {} Table containing all registered event callbacks +BaseElement.defineProperty(BaseElement, "eventCallbacks", {default = {}, type = "table"}) + +--- Registers an event that this class can listen to +--- @param class table The class to add the event to +--- @param eventName string The name of the event to register +--- @usage BaseElement.listenTo(MyClass, "mouse_click") +function BaseElement.listenTo(class, eventName) + if not class._events then + class._events = {} + end + class._events[eventName] = true +end + +--- Creates a new BaseElement instance +--- @param props table The properties to initialize the element with +--- @param basalt table The basalt instance +--- @return table The newly created BaseElement instance +--- @usage local element = BaseElement.new("myId", basalt) +function BaseElement.new() + local self = setmetatable({}, BaseElement):__init() + return self +end + +--- Initializes the BaseElement instance +--- @param props table The properties to initialize the element with +--- @param basalt table The basalt instance +--- @return table self The initialized instance +function BaseElement:init(props, basalt) + self._props = props + self._values.id = uuid() + self.basalt = basalt + self._registeredEvents = {} + if BaseElement._events then + for event in pairs(BaseElement._events) do + self._registeredEvents[event] = true + local handlerName = "on" .. event:gsub("_(%l)", function(c) + return c:upper() + end):gsub("^%l", string.upper) + self[handlerName] = function(self, ...) + self:registerCallback(event, ...) + return self + end + end + end + return self +end + +--- Post initialization hook +--- @return table self The BaseElement instance +function BaseElement:postInit() + if(self._props)then + for k,v in pairs(self._props)do + self.set(k, v) + end + end + self._props = nil + return self +end + +--- Checks if the element is a specific type +--- @param type string The type to check for +--- @return boolean Whether the element is of the specified type +function BaseElement:isType(type) + for _, t in ipairs(self._values.type) do + if t == type then + return true + end + end + return false +end + +--- Enables or disables event listening for a specific event +--- @param eventName string The name of the event to listen for +--- @param enable? boolean Whether to enable or disable the event (default: true) +--- @return table self The BaseElement instance +--- @usage element:listenEvent("mouse_click", true) +function BaseElement:listenEvent(eventName, enable) + enable = enable ~= false + if enable ~= (self._registeredEvents[eventName] or false) then + if enable then + self._registeredEvents[eventName] = true + if self.parent then + self.parent:registerChildEvent(self, eventName) + end + else + self._registeredEvents[eventName] = nil + if self.parent then + self.parent:unregisterChildEvent(self, eventName) + end + end + end + return self +end + +--- Registers a callback function for an event +--- @param event string The event to register the callback for +--- @param callback function The callback function to register +--- @return table self The BaseElement instance +--- @usage element:registerCallback("mouse_click", function(self, ...) end) +function BaseElement:registerCallback(event, callback) + if not self._registeredEvents[event] then + self:listenEvent(event, true) + end + + if not self._values.eventCallbacks[event] then + self._values.eventCallbacks[event] = {} + end + + table.insert(self._values.eventCallbacks[event], callback) + return self +end + +--- Triggers an event and calls all registered callbacks +--- @param event string The event to fire +--- @param ... any Additional arguments to pass to the callbacks +--- @return table self The BaseElement instance +--- @usage element:fireEvent("mouse_click", 1, 2) +function BaseElement:fireEvent(event, ...) + if self._values.eventCallbacks[event] then + for _, callback in ipairs(self._values.eventCallbacks[event]) do + local result = callback(self, ...) + return result + end + end + return self +end + +--- Handles all events +--- @param event string The event to handle +--- @vararg any The arguments for the event +--- @return boolean? handled Whether the event was handled +function BaseElement:dispatchEvent(event, ...) + if self[event] then + return self[event](self, ...) + end + return self:handleEvent(event, ...) +end + +--- The default event handler for all events +--- @param event string The event to handle +--- @vararg any The arguments for the event +--- @return boolean? handled Whether the event was handled +function BaseElement:handleEvent(event, ...) + return false +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() + if(self.parent) then + self.parent:updateRender() + else + self._renderUpdate = true + end +end + +return BaseElement", + ["plugins/pluginTemplate.lua"] = "-- Will temporary exist so that we don't lose track of how the plugin system works + +local VisualElement = {hooks={init={}}} + +-- Called on Class level to define properties and setup before instance is created +function VisualElement.setup(element) + element.defineProperty(element, "testProp", {default = 5, type = "number"}) +end + +-- Hooks into existing methods (you can also use init.pre or init.post) +function VisualElement.hooks.init(self) + --self.basalt.LOGGER.debug("VisualElement initialized") +end + +-- Adds a new method to the class +function VisualElement:testFunc() + --self.basalt.LOGGER.debug("Hello World", self.get("testProp")) +end + +return { + VisualElement = VisualElement +} + +", + ["elements/VisualElement.lua"] = "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 + +---@property x number 1 x position of the element +VisualElement.defineProperty(VisualElement, "x", {default = 1, type = "number", canTriggerRender = true}) +---@property y number 1 y position of the element +VisualElement.defineProperty(VisualElement, "y", {default = 1, type = "number", canTriggerRender = true}) +---@property z number 1 z position of the element +VisualElement.defineProperty(VisualElement, "z", {default = 1, type = "number", canTriggerRender = true, setter = function(self, value) + if self.parent then + self.parent:sortChildren() + end + return value +end}) + +---@property width number 1 width of the element +VisualElement.defineProperty(VisualElement, "width", {default = 1, type = "number", canTriggerRender = true}) +---@property height number 1 height of the element +VisualElement.defineProperty(VisualElement, "height", {default = 1, type = "number", canTriggerRender = true}) +---@property background color black background color of the element +VisualElement.defineProperty(VisualElement, "background", {default = colors.black, type = "number", canTriggerRender = true}) +---@property foreground color white foreground color of the element +VisualElement.defineProperty(VisualElement, "foreground", {default = colors.white, type = "number", canTriggerRender = true}) +---@property clicked boole an false element is currently clicked +VisualElement.defineProperty(VisualElement, "clicked", {default = false, type = "boolean"}) +---@property backgroundEnabled boolean true whether the background is enabled +VisualElement.defineProperty(VisualElement, "backgroundEnabled", {default = true, type = "boolean", canTriggerRender = true}) +---@property focused boolean false whether the element is focused +VisualElement.defineProperty(VisualElement, "focused", {default = false, type = "boolean", setter = function(self, value, internal) + local curValue = self.get("focused") + if value == curValue then return value end + + if value then + self:focus() + else + self:blur() + end + + if not internal and self.parent then + if value then + self.parent:setFocusedChild(self) + else + self.parent:setFocusedChild(nil) + end + end + return value +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, "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 +--- @return VisualElement object The newly created VisualElement instance +--- @usage local element = VisualElement.new("myId", basalt) +function VisualElement.new() + local self = setmetatable({}, VisualElement):__init() + return self +end + +function VisualElement:init(props, basalt) + BaseElement.init(self, props, basalt) + self.set("type", "VisualElement") +end + +--- Draws a text character/fg/bg at the specified position with a certain size, used in the rendering system +--- @param x number The x position to draw +--- @param y number The y position to draw +--- @param width number The width of the element +--- @param height number The height of the element +--- @param text string The text char to draw +--- @param fg color The foreground color +--- @param bg color The background color +function VisualElement:multiBlit(x, y, width, height, text, fg, bg) + x = x + self.get("x") - 1 + y = y + self.get("y") - 1 + self.parent:multiBlit(x, y, width, height, text, fg, bg) +end + +--- Draws a text character at the specified position, used in the rendering system +--- @param x number The x position to draw +--- @param y number The y position to draw +--- @param text string The text char to draw +--- @param fg color The foreground color +function VisualElement:textFg(x, y, text, fg) + x = x + self.get("x") - 1 + y = y + self.get("y") - 1 + 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 +--- @return boolean isInBounds Whether the coordinates are within the bounds of the element +function VisualElement:isInBounds(x, y) + local xPos, yPos = self.get("x"), self.get("y") + local width, height = self.get("width"), self.get("height") + + return x >= xPos and x <= xPos + width - 1 and + y >= yPos and y <= yPos + height - 1 +end + +--- Handles a mouse click event +--- @param button number The button that was clicked +--- @param x number The x position of the click +--- @param y number The y position of the click +--- @return boolean clicked Whether the element was clicked +function VisualElement:mouse_click(button, x, y) + if self:isInBounds(x, y) then + self.set("clicked", true) + self:fireEvent("mouse_click", button, self:getRelativePosition(x, y)) + return true + end + return false +end + +function VisualElement:mouse_up(button, x, y) + self.set("clicked", false) + if self:isInBounds(x, y) then + self:fireEvent("mouse_up", button, x, y) + return true + end + self:fireEvent("mouse_release", button, self:getRelativePosition(x, y)) +end + +function VisualElement:mouse_release() + self.set("clicked", false) +end + +function VisualElement:focus() + self:fireEvent("focus") +end + +function VisualElement:blur() + self:fireEvent("blur") + self:setCursor(1,1, false) +end + +--- Returns the absolute position of the element or the given coordinates. +---@param x? number x position +---@param y? number y position +function VisualElement:getAbsolutePosition(x, y) + local xPos, yPos = self.get("x"), self.get("y") + if(x ~= nil) then + xPos = xPos + x - 1 + end + if(y ~= nil) then + yPos = yPos + y - 1 + end + + local parent = self.parent + while parent do + local px, py = parent.get("x"), parent.get("y") + xPos = xPos + px - 1 + yPos = yPos + py - 1 + parent = parent.parent + end + + return xPos, yPos +end + +--- Returns the relative position of the element or the given coordinates. +---@param x? number x position +---@param y? number y position +---@return number, number +function VisualElement:getRelativePosition(x, y) + if (x == nil) or (y == nil) then + x, y = self.get("x"), self.get("y") + end + + local parentX, parentY = 1, 1 + if self.parent then + parentX, parentY = self.parent:getRelativePosition() + end + + local elementX = self.get("x") + local elementY = self.get("y") + + return x - (elementX - 1) - (parentX - 1), + y - (elementY - 1) - (parentY - 1) +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 + +--- Renders the element +--- @usage element:render() +function VisualElement:render() + if(not self.get("backgroundEnabled"))then + return + end + local width, height = self.get("width"), self.get("height") + self:multiBlit(1, 1, width, height, " ", tHex[self.get("foreground")], tHex[self.get("background")]) +end + +return VisualElement", + ["log.lua"] = "local Log = {} +Log._logs = {} +Log._enabled = true +Log._logToFile = true +Log._logFile = "basalt.log" + +fs.delete(Log._logFile) + +-- Log levels +Log.LEVEL = { + DEBUG = 1, + INFO = 2, + WARN = 3, + ERROR = 4 +} + +local levelMessages = { + [Log.LEVEL.DEBUG] = "Debug", + [Log.LEVEL.INFO] = "Info", + [Log.LEVEL.WARN] = "Warn", + [Log.LEVEL.ERROR] = "Error" +} + +local levelColors = { + [Log.LEVEL.DEBUG] = colors.lightGray, + [Log.LEVEL.INFO] = colors.white, + [Log.LEVEL.WARN] = colors.yellow, + [Log.LEVEL.ERROR] = colors.red +} + +function Log.setLogToFile(enable) + Log._logToFile = enable +end + +function Log.setEnabled(enable) + Log._enabled = enable +end + +local function writeToFile(message) + if Log._logToFile then + local file = io.open(Log._logFile, "a") + if file then + file:write(message.."\n") + file:close() + end + end +end + +local function log(level, ...) + if not Log._enabled then return end + + local timeStr = os.date("%H:%M:%S") + + -- Get caller info (skip log function and Log.debug/info/etc functions) + local info = debug.getinfo(3, "Sl") + local source = info.source:match("@?(.*)") + local line = info.currentline + local levelStr = string.format("[%s:%d]", source:match("([^/\\]+)%.lua$"), line) + + local levelMsg = "[" .. levelMessages[level] .. "]" + + local message = "" + for i, v in ipairs(table.pack(...)) do + if i > 1 then + message = message .. " " + end + message = message .. tostring(v) + end + + local fullMessage = string.format("%s %s%s %s", timeStr, levelStr, levelMsg, message) + + -- File output + writeToFile(fullMessage) + -- Store in memory + table.insert(Log._logs, { + time = timeStr, + level = level, + message = message + }) +end + +function Log.debug(...) log(Log.LEVEL.DEBUG, ...) end +function Log.info(...) log(Log.LEVEL.INFO, ...) end +function Log.warn(...) log(Log.LEVEL.WARN, ...) end +function Log.error(...) log(Log.LEVEL.ERROR, ...) end + +Log.info("Logger initialized") + +return Log", + ["elements/Program.lua"] = "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 +--- @return Program object The newly created Program instance +--- @usage local element = Program.new("myId", basalt) +function Program.new() + local self = setmetatable({}, Program):__init() + self.set("z", 5) + self.set("width", 30) + self.set("height", 12) + 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", + ["plugins/state.lua"] = "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 +} +", + ["elements/Table.lua"] = "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() + local self = setmetatable({}, Table):__init() + self.set("width", 30) + self.set("height", 10) + self.set("z", 5) + 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", + ["init.lua"] = "local args = {...} +local basaltPath = fs.getDir(args[2]) + +local defaultPath = package.path +local format = "path;/path/?.lua;/path/?/init.lua;" + +local main = format:gsub("path", basaltPath) +package.path = main.."rom/?" + +local function errorHandler(err) + local errorManager = require("errorManager") + errorManager.header = "Basalt Loading Error" + errorManager.error(err) +end + +local ok, result = pcall(require, "main") + +package.path = defaultPath +if not ok then + errorHandler(result) +else + return result +end", + ["elements/Slider.lua"] = "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() + local self = setmetatable({}, Slider):__init() + 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", + ["errorManager.lua"] = "local LOGGER = require("log") + +local errorHandler = { + tracebackEnabled = true, + header = "Basalt Error" +} + +local function coloredPrint(message, color) + term.setTextColor(color) + print(message) + term.setTextColor(colors.white) +end + +function errorHandler.error(errMsg) + if errorHandler.errorHandled then + error() + end + term.setBackgroundColor(colors.black) + + term.clear() + term.setCursorPos(1, 1) + + coloredPrint(errorHandler.header..":", colors.red) + print() + + local level = 2 + local topInfo + while true do + local info = debug.getinfo(level, "Sl") + if not info then break end + topInfo = info + level = level + 1 + end + local info = topInfo or debug.getinfo(2, "Sl") + local fileName = info.source:sub(2) + local lineNumber = info.currentline + local errorMessage = errMsg + + if(errorHandler.tracebackEnabled)then + local stackTrace = debug.traceback() + if stackTrace then + --coloredPrint("Stack traceback:", colors.gray) + for line in stackTrace:gmatch("[^\r\n]+") do + local fileNameInTraceback, lineNumberInTraceback = line:match("([^:]+):(%d+):") + if fileNameInTraceback and lineNumberInTraceback then + term.setTextColor(colors.lightGray) + term.write(fileNameInTraceback) + term.setTextColor(colors.gray) + term.write(":") + term.setTextColor(colors.lightBlue) + term.write(lineNumberInTraceback) + term.setTextColor(colors.gray) + line = line:gsub(fileNameInTraceback .. ":" .. lineNumberInTraceback, "") + end + coloredPrint(line, colors.gray) + end + print() + end + end + + if fileName and lineNumber then + term.setTextColor(colors.red) + term.write("Error in ") + term.setTextColor(colors.white) + term.write(fileName) + term.setTextColor(colors.red) + term.write(":") + term.setTextColor(colors.lightBlue) + term.write(lineNumber) + term.setTextColor(colors.red) + term.write(": ") + + + if errorMessage then + errorMessage = string.gsub(errorMessage, "stack traceback:.*", "") + if errorMessage ~= "" then + coloredPrint(errorMessage, colors.red) + else + coloredPrint("Error message not available", colors.gray) + end + else + coloredPrint("Error message not available", colors.gray) + end + + local file = fs.open(fileName, "r") + if file then + local lineContent = "" + local currentLineNumber = 1 + repeat + lineContent = file.readLine() + if currentLineNumber == tonumber(lineNumber) then + coloredPrint("\149Line " .. lineNumber, colors.cyan) + coloredPrint(lineContent, colors.lightGray) + break + end + currentLineNumber = currentLineNumber + 1 + until not lineContent + file.close() + end + end + + term.setBackgroundColor(colors.black) + LOGGER.error(errMsg) + errorHandler.errorHandled = true + error() +end + +return errorHandler", + ["elements/Container.lua"] = "local elementManager = require("elementManager") +local VisualElement = elementManager.getElement("VisualElement") +local expect = require("libraries/expect") +local split = require("libraries/utils").split + +local max = math.max + +---@class Container : VisualElement +local Container = setmetatable({}, VisualElement) +Container.__index = Container + +Container.defineProperty(Container, "children", {default = {}, type = "table"}) +Container.defineProperty(Container, "childrenSorted", {default = true, type = "boolean"}) +Container.defineProperty(Container, "childrenEventsSorted", {default = true, type = "boolean"}) +Container.defineProperty(Container, "childrenEvents", {default = {}, type = "table"}) +Container.defineProperty(Container, "eventListenerCount", {default = {}, type = "table"}) +Container.defineProperty(Container, "focusedChild", {default = nil, type = "table", setter = function(self, value, internal) + local oldChild = self._values.focusedChild + + if value == oldChild then return value end + + if oldChild then + if oldChild:isType("Container") then + oldChild.set("focusedChild", nil, true) + end + oldChild.set("focused", false, true) + end + + if value and not internal then + value.set("focused", true, true) + if self.parent then + self.parent:setFocusedChild(self) + end + end + + return value +end}) + +Container.defineProperty(Container, "visibleChildren", {default = {}, type = "table"}) +Container.defineProperty(Container, "visibleChildrenEvents", {default = {}, type = "table"}) + +function Container:isChildVisible(child) + local childX, childY = child.get("x"), child.get("y") + local childW, childH = child.get("width"), child.get("height") + local containerW, containerH = self.get("width"), self.get("height") + + return childX <= containerW and + childY <= containerH and + childX + childW > 0 and + childY + childH > 0 +end + +for k, _ in pairs(elementManager:getElementList()) do + local capitalizedName = k:sub(1,1):upper() .. k:sub(2) + if capitalizedName ~= "BaseFrame" then + Container["add"..capitalizedName] = function(self, ...) + expect(1, self, "table") + local element = self.basalt.create(k, ...) + self:addChild(element) + element:postInit() + return element + end + Container["addDelayed"..capitalizedName] = function(self, prop) + expect(1, self, "table") + local element = self.basalt.create(k, prop, true, self) + return element + end + end +end + +function Container.new() + local self = setmetatable({}, Container):__init() + return self +end + +function Container:init(props, basalt) + VisualElement.init(self, props, basalt) + self.set("type", "Container") +end + +function Container:addChild(child) + if child == self then + error("Cannot add container to itself") + end + + table.insert(self._values.children, child) + child.parent = self + self.set("childrenSorted", false) + self:registerChildrenEvents(child) + return self +end + +local function sortAndFilterChildren(self, children) + local visibleChildren = {} + + for _, child in ipairs(children) do + if self:isChildVisible(child) and child.get("visible") then + table.insert(visibleChildren, child) + end + end + + for i = 2, #visibleChildren do + local current = visibleChildren[i] + local currentZ = current.get("z") + local j = i - 1 + while j > 0 do + local compare = visibleChildren[j].get("z") + if compare > currentZ then + visibleChildren[j + 1] = visibleChildren[j] + j = j - 1 + else + break + end + end + visibleChildren[j + 1] = current + end + + return visibleChildren +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() + self.set("visibleChildren", sortAndFilterChildren(self, self._values.children)) + self.set("childrenSorted", true) +end + +function Container:sortChildrenEvents(eventName) + if self._values.childrenEvents[eventName] then + self._values.visibleChildrenEvents[eventName] = sortAndFilterChildren(self, self._values.childrenEvents[eventName]) + end + self.set("childrenEventsSorted", true) +end + +function Container:registerChildrenEvents(child) + if(child._registeredEvents == nil)then return end + for event in pairs(child._registeredEvents) do + self:registerChildEvent(child, event) + end +end + +function Container:registerChildEvent(child, eventName) + if not self._values.childrenEvents[eventName] then + self._values.childrenEvents[eventName] = {} + self._values.eventListenerCount[eventName] = 0 + + if self.parent then + self.parent:registerChildEvent(self, eventName) + end + end + + for _, registeredChild in ipairs(self._values.childrenEvents[eventName]) do + if registeredChild == child then + return + end + end + + self.set("childrenEventsSorted", false) + table.insert(self._values.childrenEvents[eventName], child) + self._values.eventListenerCount[eventName] = self._values.eventListenerCount[eventName] + 1 +end + +function Container:removeChildrenEvents(child) + if(child._registeredEvents == nil)then return end + for event in pairs(child._registeredEvents) do + self:unregisterChildEvent(child, event) + end +end + +function Container:unregisterChildEvent(child, eventName) + if self._values.childrenEvents[eventName] then + for i, listener in ipairs(self._values.childrenEvents[eventName]) do + if listener == child then + table.remove(self._values.childrenEvents[eventName], i) + self._values.eventListenerCount[eventName] = self._values.eventListenerCount[eventName] - 1 + + if self._values.eventListenerCount[eventName] <= 0 then + self._values.childrenEvents[eventName] = nil + self._values.eventListenerCount[eventName] = nil + + if self.parent then + self.parent:unregisterChildEvent(self, eventName) + end + end + break + end + end + end +end + +function Container:removeChild(child) + for i,v in ipairs(self._values.children) do + if v == child then + table.remove(self._values.children, i) + child.parent = nil + break + end + end + self:removeChildrenEvents(child) + return self +end + +function Container:getChild(path) + if type(path) == "string" then + local parts = split(path, "/") + for _,v in pairs(self._values.children) do + if v.get("name") == parts[1] then + if #parts == 1 then + return v + else + if(v:isType("Container"))then + return v:find(table.concat(parts, "/", 2)) + end + end + end + end + end + return nil +end + +local function convertMousePosition(self, event, ...) + local args = {...} + if event:find("mouse_") then + local button, absX, absY = ... + local relX, relY = self:getRelativePosition(absX, absY) + args = {button, relX, relY} + end + return args +end + +local function callChildrenEvents(self, visibleOnly, event, ...) + local children = visibleOnly and self.get("visibleChildrenEvents") or self.get("childrenEvents") + if children[event] then + local events = children[event] + for i = #events, 1, -1 do + local child = events[i] + if(child:dispatchEvent(event, ...))then + return true, child + end + end + end + return false +end + +function Container:handleEvent(event, ...) + VisualElement.handleEvent(self, event, ...) + local args = convertMousePosition(self, event, ...) + return callChildrenEvents(self, false, event, table.unpack(args)) +end + + +function Container:mouse_click(button, x, y) + if VisualElement.mouse_click(self, button, x, y) then + local args = convertMousePosition(self, "mouse_click", button, x, y) + local success, child = callChildrenEvents(self, true, "mouse_click", table.unpack(args)) + if(success)then + self.set("focusedChild", child) + return true + end + self.set("focusedChild", nil) + 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) + if self.get("focusedChild") then + return self.get("focusedChild"):dispatchEvent("key", key) + end + return true +end + +function Container:char(char) + if self.get("focusedChild") then + return self.get("focusedChild"):dispatchEvent("char", char) + end + return true +end + +function Container:key_up(key) + if self.get("focusedChild") then + return self.get("focusedChild"):dispatchEvent("key_up", key) + end + return true +end + +function Container:multiBlit(x, y, width, height, text, fg, bg) + local w, h = self.get("width"), self.get("height") + + width = x < 1 and math.min(width + x - 1, w) or math.min(width, math.max(0, w - x + 1)) + height = y < 1 and math.min(height + y - 1, h) or math.min(height, math.max(0, h - y + 1)) + + if width <= 0 or height <= 0 then return end + + VisualElement.multiBlit(self, math.max(1, x), math.max(1, y), width, height, text, fg, bg) +end + +function Container:textFg(x, y, text, fg) + 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) + + if textLen <= 0 then return end + + 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 + self:sortChildren() + end + if not self.get("childrenEventsSorted")then + for event in pairs(self._values.childrenEvents) do + self:sortChildrenEvents(event) + end + end + for _, child in ipairs(self.get("visibleChildren")) do + if child == self then + self.basalt.LOGGER.error("CIRCULAR REFERENCE DETECTED!") + return + end + child:render() + end +end + +return Container", + ["libraries/colorHex.lua"] = "local colorHex = {} + +for i = 0, 15 do + colorHex[2^i] = ("%x"):format(i) + colorHex[("%x"):format(i)] = 2^i +end + +return colorHex", + ["elements/Flexbox.lua"] = "local elementManager = require("elementManager") +local Container = elementManager.getElement("Container") + +---@class Flexbox : Container +local Flexbox = setmetatable({}, Container) +Flexbox.__index = Flexbox + +Flexbox.defineProperty(Flexbox, "flexDirection", {default = "row", type = "string"}) +Flexbox.defineProperty(Flexbox, "flexSpacing", {default = 1, type = "number"}) +Flexbox.defineProperty(Flexbox, "flexJustifyContent", { + default = "flex-start", + type = "string", + setter = function(self, value) + if not value:match("^flex%-") then + value = "flex-" .. value + end + return value + end +}) +Flexbox.defineProperty(Flexbox, "flexWrap", {default = false, type = "boolean"}) +Flexbox.defineProperty(Flexbox, "flexUpdateLayout", {default = false, type = "boolean"}) + +local lineBreakElement = { + getHeight = function(self) return 0 end, + getWidth = function(self) return 0 end, + getZ = function(self) return 1 end, + getPosition = function(self) return 0, 0 end, + getSize = function(self) return 0, 0 end, + isType = function(self) return false end, + getType = function(self) return "lineBreak" end, + getName = function(self) return "lineBreak" end, + setPosition = function(self) end, + setParent = function(self) end, + setSize = function(self) end, + getFlexGrow = function(self) return 0 end, + getFlexShrink = function(self) return 0 end, + getFlexBasis = function(self) return 0 end, + init = function(self) end, + getVisible = function(self) return true end, +} + + +local function sortElements(self, direction, spacing, wrap) + local elements = self.get("children") + local sortedElements = {} + if not(wrap)then + local index = 1 + local lineSize = 1 + local lineOffset = 1 + for _,v in pairs(elements)do + if(sortedElements[index]==nil)then sortedElements[index]={offset=1} end + + local childHeight = direction == "row" and v.get("height") or v.get("width") + if childHeight > lineSize then + lineSize = childHeight + end + if(v == lineBreakElement)then + lineOffset = lineOffset + lineSize + spacing + lineSize = 1 + index = index + 1 + sortedElements[index] = {offset=lineOffset} + else + table.insert(sortedElements[index], v) + end + end + elseif(wrap)then + local lineSize = 1 + local lineOffset = 1 + + local maxSize = direction == "row" and self.get("width") or self.get("height") + local usedSize = 0 + local index = 1 + + for _,v in pairs(elements) do + if(sortedElements[index]==nil) then sortedElements[index]={offset=1} end + + if v:getType() == "lineBreak" then + lineOffset = lineOffset + lineSize + spacing + usedSize = 0 + lineSize = 1 + index = index + 1 + sortedElements[index] = {offset=lineOffset} + else + local objSize = direction == "row" and v.get("width") or v.get("height") + if(objSize+usedSize<=maxSize) then + table.insert(sortedElements[index], v) + usedSize = usedSize + objSize + spacing + else + lineOffset = lineOffset + lineSize + spacing + lineSize = direction == "row" and v.get("height") or v.get("width") + index = index + 1 + usedSize = objSize + spacing + sortedElements[index] = {offset=lineOffset, v} + end + + local childHeight = direction == "row" and v.get("height") or v.get("width") + if childHeight > lineSize then + lineSize = childHeight + end + end + end + end + return sortedElements +end + +local function calculateRow(self, children, spacing, justifyContent) + local containerWidth = self.get("width") + + local usedSpace = spacing * (#children - 1) + local totalFlexGrow = 0 + + for _, child in ipairs(children) do + if child ~= lineBreakElement then + usedSpace = usedSpace + child.get("width") + totalFlexGrow = totalFlexGrow + child.get("flexGrow") + end + end + + local remainingSpace = containerWidth - usedSpace + local extraSpacePerUnit = totalFlexGrow > 0 and (remainingSpace / totalFlexGrow) or 0 + local distributedSpace = 0 + + local currentX = 1 + for i, child in ipairs(children) do + if child ~= lineBreakElement then + local childWidth = child.get("width") + + if child.get("flexGrow") > 0 then + + if i == #children then + local extraSpace = remainingSpace - distributedSpace + childWidth = childWidth + extraSpace + else + local extraSpace = math.floor(extraSpacePerUnit * child.get("flexGrow")) + childWidth = childWidth + extraSpace + distributedSpace = distributedSpace + extraSpace + end + end + + child.set("x", currentX) + child.set("y", children.offset or 1) + child.set("width", childWidth) + currentX = currentX + childWidth + spacing + end + end + + if justifyContent == "flex-end" then + local offset = containerWidth - (currentX - spacing - 1) + for _, child in ipairs(children) do + child.set("x", child.get("x") + offset) + end + elseif justifyContent == "flex-center" or justifyContent == "center" then -- Akzeptiere beide Formate + local offset = math.floor((containerWidth - (currentX - spacing - 1)) / 2) + for _, child in ipairs(children) do + child.set("x", child.get("x") + offset) + end + end +end + +local function calculateColumn(self, children, spacing, justifyContent) + local containerHeight = self.get("height") + + local usedSpace = spacing * (#children - 1) + local totalFlexGrow = 0 + + for _, child in ipairs(children) do + if child ~= lineBreakElement then + usedSpace = usedSpace + child.get("height") + totalFlexGrow = totalFlexGrow + child.get("flexGrow") + end + end + + local remainingSpace = containerHeight - usedSpace + local extraSpacePerUnit = totalFlexGrow > 0 and (remainingSpace / totalFlexGrow) or 0 + local distributedSpace = 0 + + local currentY = 1 + for i, child in ipairs(children) do + if child ~= lineBreakElement then + local childHeight = child.get("height") + + if child.get("flexGrow") > 0 then + + if i == #children then + local extraSpace = remainingSpace - distributedSpace + childHeight = childHeight + extraSpace + else + local extraSpace = math.floor(extraSpacePerUnit * child.get("flexGrow")) + childHeight = childHeight + extraSpace + distributedSpace = distributedSpace + extraSpace + end + end + + child.set("x", children.offset or 1) + child.set("y", currentY) + child.set("height", childHeight) + currentY = currentY + childHeight + spacing + end + end + + if justifyContent == "flex-end" then + local offset = containerHeight - (currentY - spacing - 1) + for _, child in ipairs(children) do + child.set("y", child.get("y") + offset) + end + elseif justifyContent == "flex-center" or justifyContent == "center" then -- Akzeptiere beide Formate + local offset = math.floor((containerHeight - (currentY - spacing - 1)) / 2) + for _, child in ipairs(children) do + child.set("y", child.get("y") + offset) + end + end +end + +local function updateLayout(self, direction, spacing, justifyContent, wrap) + local elements = sortElements(self, direction, spacing, wrap) + if direction == "row" then + for _,v in pairs(elements)do + calculateRow(self, v, spacing, justifyContent) + end + else + for _,v in pairs(elements)do + calculateColumn(self, v, spacing, justifyContent) + end + end + self.set("flexUpdateLayout", false) +end + +--- Creates a new Flexbox instance +--- @return Flexbox object The newly created Flexbox instance +--- @usage local element = Flexbox.new("myId", basalt) +function Flexbox.new() + local self = setmetatable({}, Flexbox):__init() + self.set("width", 12) + self.set("height", 6) + self.set("background", colors.blue) + self.set("z", 10) + self:observe("width", function() self.set("flexUpdateLayout", true) end) + self:observe("height", function() self.set("flexUpdateLayout", true) end) + return self +end + +function Flexbox:init(props, basalt) + Container.init(self, props, basalt) + self.set("type", "Flexbox") +end + +function Flexbox:addChild(element) + Container.addChild(self, element) + + if(element~=lineBreakElement)then + element:instanceProperty("flexGrow", {default = 0, type = "number"}) + element:instanceProperty("flexShrink", {default = 0, type = "number"}) + element:instanceProperty("flexBasis", {default = 0, type = "number"}) + end + + self.set("flexUpdateLayout", true) + return self +end + +function Flexbox:removeChild(element) + Container.removeChild(self, element) + + if(element~=lineBreakElement)then + element.setFlexGrow = nil + element.setFlexShrink = nil + element.setFlexBasis = nil + element.getFlexGrow = nil + element.getFlexShrink = nil + element.getFlexBasis = nil + element.set("flexGrow", nil) + element.set("flexShrink", nil) + element.set("flexBasis", nil) + end + + self.set("flexUpdateLayout", true) + return self +end + +--- Adds a new line break to the flexbox. +---@param self Flexbox The element itself +---@return Flexbox +function Flexbox:addLineBreak() + self:addChild(lineBreakElement) + return self +end + +function Flexbox:render() + if(self.get("flexUpdateLayout"))then + updateLayout(self, self.get("flexDirection"), self.get("flexSpacing"), self.get("flexJustifyContent"), self.get("flexWrap")) + end + Container.render(self) +end + +return Flexbox", + ["elements/Label.lua"] = "local elementManager = require("elementManager") +local VisualElement = elementManager.getElement("VisualElement") + +---@class Label : VisualElement +local Label = setmetatable({}, VisualElement) +Label.__index = Label + +---@property text string Label Label text to be displayed +Label.defineProperty(Label, "text", {default = "Label", type = "string", setter = function(self, value) + if(type(value)=="function")then value = value() end + self.set("width", #value) + return value +end}) + +--- Creates a new Label instance +--- @return Label object The newly created Label instance +--- @usage local element = Label.new("myId", basalt) +function Label.new() + local self = setmetatable({}, Label):__init() + self.set("z", 3) + self.set("foreground", colors.black) + self.set("backgroundEnabled", false) + return self +end + +function Label:init(props, basalt) + VisualElement.init(self, props, basalt) + self.set("type", "Label") +end + +function Label:render() + VisualElement.render(self) + local text = self.get("text") + self:textFg(1, 1, text, self.get("foreground")) +end + +return Label", + ["plugins/theme.lua"] = "local defaultTheme = { + default = { + background = colors.lightGray, + foreground = colors.black, + }, + BaseFrame = { + background = colors.white, + foreground = colors.black, + + Frame = { + background = colors.black, + names = { + basaltDebugLogClose = { + background = colors.blue, + foreground = colors.white + } + }, + }, + Button = { + background = "{self.clicked and colors.black or colors.cyan}", + foreground = "{self.clicked and colors.cyan or colors.black}", + }, + + names = { + basaltDebugLog = { + background = colors.red, + foreground = colors.white + }, + test = { + background = "{self.clicked and colors.black or colors.green}", + foreground = "{self.clicked and colors.green or colors.black}" + } + }, + } +} + +local themes = { + default = defaultTheme +} + +local currentTheme = "default" + +local BaseElement = { + hooks = { + postInit = { + pre = function(self) + self:applyTheme() + end} + } +} + +function BaseElement.____getElementPath(self, types) + if types then + table.insert(types, 1, self._values.type) + else + types = {self._values.type} + end + local parent = self.parent + if parent then + return parent.____getElementPath(parent, types) + else + return types + end +end + +local function lookUpTemplate(theme, path) + local current = theme + + for i = 1, #path do + local found = false + local types = path[i] + + for _, elementType in ipairs(types) do + if current[elementType] then + current = current[elementType] + found = true + break + end + end + + if not found then + return nil + end + end + + return current +end + +local function getDefaultProperties(theme, elementType) + local result = {} + if theme.default then + for k,v in pairs(theme.default) do + if type(v) ~= "table" then + result[k] = v + end + end + + if theme.default[elementType] then + for k,v in pairs(theme.default[elementType]) do + if type(v) ~= "table" then + result[k] = v + end + end + end + end + return result +end + +local function applyNamedStyles(result, theme, elementType, elementName, themeTable) + if theme.default and theme.default.names and theme.default.names[elementName] then + for k,v in pairs(theme.default.names[elementName]) do + if type(v) ~= "table" then result[k] = v end + end + end + + if theme.default and theme.default[elementType] and theme.default[elementType].names + and theme.default[elementType].names[elementName] then + for k,v in pairs(theme.default[elementType].names[elementName]) do + if type(v) ~= "table" then result[k] = v end + end + end + + if themeTable and themeTable.names and themeTable.names[elementName] then + for k,v in pairs(themeTable.names[elementName]) do + if type(v) ~= "table" then result[k] = v end + end + end +end + +local function collectThemeProps(theme, path, elementType, elementName) + local result = {} + local themeTable = lookUpTemplate(theme, path) + if themeTable then + for k,v in pairs(themeTable) do + if type(v) ~= "table" then + result[k] = v + end + end + end + + if next(result) == nil then + result = getDefaultProperties(theme, elementType) + end + + applyNamedStyles(result, theme, elementType, elementName, themeTable) + + return result +end + + function BaseElement:applyTheme() + local styles = self:getTheme() + if(styles ~= nil) then + for prop, value in pairs(styles) do + self.set(prop, value) + end + end +end + +function BaseElement:getTheme() + local path = self:____getElementPath() + local elementType = self.get("type") + local elementName = self.get("name") + + return collectThemeProps(themes[currentTheme], path, elementType, elementName) +end + +local themeAPI = { + setTheme = function(newTheme) + 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 = { + BaseElement = BaseElement, + API = themeAPI +} + +return Theme +", + ["plugins/animation.lua"] = "local Animation = {} +Animation.__index = Animation + +local registeredAnimations = {} + +function Animation.registerAnimation(name, handlers) + registeredAnimations[name] = handlers + + Animation[name] = function(self, ...) + local args = {...} + local easing = "linear" + if(type(args[#args]) == "string") then + easing = table.remove(args, #args) + end + local duration = table.remove(args, #args) + return self:addAnimation(name, args, duration, easing) + end +end + +local easings = { + linear = function(progress) + return progress + end, + + easeInQuad = function(progress) + return progress * progress + end, + + easeOutQuad = function(progress) + return 1 - (1 - progress) * (1 - progress) + end, + + easeInOutQuad = function(progress) + if progress < 0.5 then + return 2 * progress * progress + end + return 1 - (-2 * progress + 2)^2 / 2 + end +} + +function Animation.registerEasing(name, func) + easings[name] = func +end + +local AnimationInstance = {} +AnimationInstance.__index = AnimationInstance + +function AnimationInstance.new(element, animType, args, duration, easing) + local self = setmetatable({}, AnimationInstance) + self.element = element + self.type = animType + self.args = args + self.duration = duration + self.startTime = 0 + self.isPaused = false + self.handlers = registeredAnimations[animType] + self.easing = easing + return self +end + +function AnimationInstance:start() + self.startTime = os.epoch("local") / 1000 + if self.handlers.start then + self.handlers.start(self) + end +end + +function AnimationInstance:update(elapsed) + local rawProgress = math.min(1, elapsed / self.duration) + local progress = easings[self.easing](rawProgress) + return self.handlers.update(self, progress) +end + +function AnimationInstance:complete() + if self.handlers.complete then + self.handlers.complete(self) + end +end + +function Animation.new(element) + local self = {} + self.element = element + self.sequences = {{}} + self.sequenceCallbacks = {} + self.currentSequence = 1 + self.timer = nil + setmetatable(self, Animation) + return self +end + +function Animation:sequence() + table.insert(self.sequences, {}) + self.currentSequence = #self.sequences + self.sequenceCallbacks[self.currentSequence] = { + start = nil, + update = nil, + complete = nil + } + return self +end + +function Animation:onStart(callback) + if not self.sequenceCallbacks[self.currentSequence] then + self.sequenceCallbacks[self.currentSequence] = {} + end + self.sequenceCallbacks[self.currentSequence].start = callback + return self +end + +function Animation:onUpdate(callback) + if not self.sequenceCallbacks[self.currentSequence] then + self.sequenceCallbacks[self.currentSequence] = {} + end + self.sequenceCallbacks[self.currentSequence].update = callback + return self +end + +function Animation:onComplete(callback) + if not self.sequenceCallbacks[self.currentSequence] then + self.sequenceCallbacks[self.currentSequence] = {} + end + self.sequenceCallbacks[self.currentSequence].complete = callback + return self +end + +function Animation:addAnimation(type, args, duration, easing) + local anim = AnimationInstance.new(self.element, type, args, duration, easing) + table.insert(self.sequences[self.currentSequence], anim) + return self +end + +function Animation:start() + + self.currentSequence = 1 + if(self.sequenceCallbacks[self.currentSequence])then + if(self.sequenceCallbacks[self.currentSequence].start) then + self.sequenceCallbacks[self.currentSequence].start(self.element) + end + end + if #self.sequences[self.currentSequence] > 0 then + self.timer = os.startTimer(0.05) + for _, anim in ipairs(self.sequences[self.currentSequence]) do + anim:start() + end + end + return self +end + +function Animation:event(event, timerId) + if event == "timer" and timerId == self.timer then + local currentTime = os.epoch("local") / 1000 + local sequenceFinished = true + local remaining = {} + local callbacks = self.sequenceCallbacks[self.currentSequence] + + for _, anim in ipairs(self.sequences[self.currentSequence]) do + local elapsed = currentTime - anim.startTime + local progress = elapsed / anim.duration + local finished = anim:update(elapsed) + + if callbacks and callbacks.update then + callbacks.update(self.element, progress) + end + + if not finished then + table.insert(remaining, anim) + sequenceFinished = false + else + anim:complete() + end + end + + if sequenceFinished then + if callbacks and callbacks.complete then + callbacks.complete(self.element) + end + + if self.currentSequence < #self.sequences then + self.currentSequence = self.currentSequence + 1 + remaining = {} + + local nextCallbacks = self.sequenceCallbacks[self.currentSequence] + if nextCallbacks and nextCallbacks.start then + nextCallbacks.start(self.element) + end + + for _, anim in ipairs(self.sequences[self.currentSequence]) do + anim:start() + table.insert(remaining, anim) + end + end + end + + if #remaining > 0 then + self.timer = os.startTimer(0.05) + end + end +end + +Animation.registerAnimation("move", { + start = function(anim) + anim.startX = anim.element.get("x") + anim.startY = anim.element.get("y") + end, + + update = function(anim, progress) + local x = anim.startX + (anim.args[1] - anim.startX) * progress + local y = anim.startY + (anim.args[2] - anim.startY) * progress + anim.element.set("x", math.floor(x)) + anim.element.set("y", math.floor(y)) + return progress >= 1 + end, + + complete = function(anim) + anim.element.set("x", anim.args[1]) + anim.element.set("y", anim.args[2]) + end +}) + +Animation.registerAnimation("morphText", { + start = function(anim) + local startText = anim.element.get(anim.args[1]) + local targetText = anim.args[2] + local maxLength = math.max(#startText, #targetText) + local startSpace = string.rep(" ", math.floor(maxLength - #startText)/2) + anim.startText = startSpace .. startText .. startSpace + anim.targetText = targetText .. string.rep(" ", maxLength - #targetText) + anim.length = maxLength + end, + + update = function(anim, progress) + local currentText = "" + + for i = 1, anim.length do + local startChar = anim.startText:sub(i,i) + local targetChar = anim.targetText:sub(i,i) + + if progress < 0.5 then + currentText = currentText .. (math.random() > progress*2 and startChar or " ") + else + currentText = currentText .. (math.random() > (progress-0.5)*2 and " " or targetChar) + end + end + + anim.element.set(anim.args[1], currentText) + return progress >= 1 + end, + + complete = function(anim) + anim.element.set(anim.args[1], anim.targetText:gsub("%s+$", "")) -- Entferne trailing spaces + end +}) + +Animation.registerAnimation("typewrite", { + start = function(anim) + anim.targetText = anim.args[2] + anim.element.set(anim.args[1], "") + end, + + update = function(anim, progress) + local length = math.floor(#anim.targetText * progress) + anim.element.set(anim.args[1], anim.targetText:sub(1, length)) + return progress >= 1 + end +}) + +Animation.registerAnimation("fadeText", { + start = function(anim) + anim.chars = {} + for i=1, #anim.args[2] do + anim.chars[i] = {char = anim.args[2]:sub(i,i), visible = false} + end + end, + + update = function(anim, progress) + local text = "" + for i, charData in ipairs(anim.chars) do + if math.random() < progress then + charData.visible = true + end + text = text .. (charData.visible and charData.char or " ") + end + anim.element.set(anim.args[1], text) + return progress >= 1 + end +}) + +Animation.registerAnimation("scrollText", { + start = function(anim) + anim.width = anim.element.get("width") + anim.targetText = anim.args[2] + anim.element.set(anim.args[1], "") + end, + + update = function(anim, progress) + local offset = math.floor(anim.width * (1-progress)) + local spaces = string.rep(" ", offset) + anim.element.set(anim.args[1], spaces .. anim.targetText) + return progress >= 1 + end +}) + +local VisualElement = {hooks={}} + +function VisualElement.hooks.dispatchEvent(self, event, ...) + if event == "timer" then + local animation = self.get("animation") + if animation then + animation:event(event, ...) + end + end +end + +function VisualElement.setup(element) + VisualElementBaseDispatchEvent = element.dispatchEvent + element.defineProperty(element, "animation", {default = nil, type = "table"}) + element.listenTo(element, "timer") +end + +function VisualElement:animate() + local animation = Animation.new(self) + self.set("animation", animation) + return animation +end + +return { + VisualElement = VisualElement +}", + ["plugins/debug.lua"] = "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("basaltDebugLog") + :setWidth(width) + :setHeight(height) + :setZ(999) + :listenEvent("mouse_scroll", true) + self.basalt.LOGGER.debug("Created debug log frame " .. self._debugFrame.get("name")) + + self._debugFrame:addButton("basaltDebugLogClose") + :setWidth(9) + :setHeight(1) + :setX(width - 8) + :setY(height) + :setText("Close") + :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, +} +", + ["elements/Frame.lua"] = "local elementManager = require("elementManager") +local Container = elementManager.getElement("Container") + +---@class Frame : Container +local Frame = setmetatable({}, Container) +Frame.__index = Frame + +--- Creates a new Frame instance +--- @return Frame object The newly created Frame instance +--- @usage local element = Frame.new("myId", basalt) +function Frame.new() + local self = setmetatable({}, Frame):__init() + self.set("width", 12) + self.set("height", 6) + self.set("background", colors.gray) + self.set("z", 10) + return self +end + +function Frame:init(props, basalt) + Container.init(self, props, basalt) + self.set("type", "Frame") +end + +return Frame", + ["render.lua"] = "local Render = {} +Render.__index = Render +local colorChars = require("libraries/colorHex") + +function Render.new(terminal) + local self = setmetatable({}, Render) + self.terminal = terminal + self.width, self.height = terminal.getSize() + + self.buffer = { + text = {}, + fg = {}, + bg = {}, + dirtyRects = {} + } + + for y=1, self.height do + self.buffer.text[y] = string.rep(" ", self.width) + self.buffer.fg[y] = string.rep("0", self.width) + self.buffer.bg[y] = string.rep("f", self.width) + end + + return self +end + +function Render:addDirtyRect(x, y, width, height) + table.insert(self.buffer.dirtyRects, { + x = x, + y = y, + width = width, + height = height + }) +end + +function Render:blit(x, y, text, fg, bg) + if y < 1 or y > self.height then return self end + if(#text ~= #fg or #text ~= #bg)then + error("Text, fg, and bg must be the same length") + end + + self.buffer.text[y] = self.buffer.text[y]:sub(1,x-1) .. text .. self.buffer.text[y]:sub(x+#text) + self.buffer.fg[y] = self.buffer.fg[y]:sub(1,x-1) .. fg .. self.buffer.fg[y]:sub(x+#fg) + self.buffer.bg[y] = self.buffer.bg[y]:sub(1,x-1) .. bg .. self.buffer.bg[y]:sub(x+#bg) + self:addDirtyRect(x, y, #text, 1) + + return self +end + +function Render:multiBlit(x, y, width, height, text, fg, bg) + if y < 1 or y > self.height then return self end + if(#text ~= #fg or #text ~= #bg)then + error("Text, fg, and bg must be the same length") + end + + text = text:rep(width) + fg = fg:rep(width) + bg = bg:rep(width) + + for dy=0, height-1 do + local cy = y + dy + if cy >= 1 and cy <= self.height then + self.buffer.text[cy] = self.buffer.text[cy]:sub(1,x-1) .. text .. self.buffer.text[cy]:sub(x+#text) + self.buffer.fg[cy] = self.buffer.fg[cy]:sub(1,x-1) .. fg .. self.buffer.fg[cy]:sub(x+#fg) + self.buffer.bg[cy] = self.buffer.bg[cy]:sub(1,x-1) .. bg .. self.buffer.bg[cy]:sub(x+#bg) + end + end + + self:addDirtyRect(x, y, width, height) + return self +end + +function Render:textFg(x, y, text, fg) + if y < 1 or y > self.height then return self end + fg = colorChars[fg] or "0" + + self.buffer.text[y] = self.buffer.text[y]:sub(1,x-1) .. text .. self.buffer.text[y]:sub(x+#text) + self.buffer.fg[y] = self.buffer.fg[y]:sub(1,x-1) .. fg:rep(#text) .. self.buffer.fg[y]:sub(x+#text) + self:addDirtyRect(x, y, #text, 1) + + return self +end + +function Render:textBg(x, y, text, bg) + if y < 1 or y > self.height then return self end + bg = colorChars[bg] or "f" + + self.buffer.text[y] = self.buffer.text[y]:sub(1,x-1) .. text .. self.buffer.text[y]:sub(x+#text) + self.buffer.bg[y] = self.buffer.bg[y]:sub(1,x-1) .. bg:rep(#text) .. self.buffer.bg[y]:sub(x+#text) + self:addDirtyRect(x, y, #text, 1) + + return self +end + +function Render:text(x, y, text) + if y < 1 or y > self.height then return self end + + self.buffer.text[y] = self.buffer.text[y]:sub(1,x-1) .. text .. self.buffer.text[y]:sub(x+#text) + self:addDirtyRect(x, y, #text, 1) + + return self +end + +function Render:fg(x, y, fg) + if y < 1 or y > self.height then return self end + + self.buffer.fg[y] = self.buffer.fg[y]:sub(1,x-1) .. fg .. self.buffer.fg[y]:sub(x+#fg) + self:addDirtyRect(x, y, #fg, 1) + + return self +end + +function Render:bg(x, y, bg) + if y < 1 or y > self.height then return self end + + self.buffer.bg[y] = self.buffer.bg[y]:sub(1,x-1) .. bg .. self.buffer.bg[y]:sub(x+#bg) + self:addDirtyRect(x, y, #bg, 1) + + return self +end + +function Render:blit(x, y, text, fg, bg) + if y < 1 or y > self.height then return self end + if(#text ~= #fg or #text ~= #bg)then + error("Text, fg, and bg must be the same length") + end + + self.buffer.text[y] = self.buffer.text[y]:sub(1,x-1) .. text .. self.buffer.text[y]:sub(x+#text) + self.buffer.fg[y] = self.buffer.fg[y]:sub(1,x-1) .. fg .. self.buffer.fg[y]:sub(x+#fg) + self.buffer.bg[y] = self.buffer.bg[y]:sub(1,x-1) .. bg .. self.buffer.bg[y]:sub(x+#bg) + self:addDirtyRect(x, y, #text, 1) + + return self +end + +function Render:clear(bg) + local bgChar = colorChars[bg] or "f" + for y=1, self.height do + self.buffer.text[y] = string.rep(" ", self.width) + self.buffer.fg[y] = string.rep("0", self.width) + self.buffer.bg[y] = string.rep(bgChar, self.width) + self:addDirtyRect(1, y, self.width, 1) + end + return self +end + +function Render:render() + + local mergedRects = {} + for _, rect in ipairs(self.buffer.dirtyRects) do + local merged = false + for _, existing in ipairs(mergedRects) do + if self:rectOverlaps(rect, existing) then + self:mergeRects(existing, rect) + merged = true + break + end + end + if not merged then + table.insert(mergedRects, rect) + end + end + + for _, rect in ipairs(mergedRects) do + for y = rect.y, rect.y + rect.height - 1 do + if y >= 1 and y <= self.height then + self.terminal.setCursorPos(rect.x, y) + self.terminal.blit( + self.buffer.text[y]:sub(rect.x, rect.x + rect.width - 1), + self.buffer.fg[y]:sub(rect.x, rect.x + rect.width - 1), + self.buffer.bg[y]:sub(rect.x, rect.x + rect.width - 1) + ) + end + end + end + + self.buffer.dirtyRects = {} + + if self.blink then + self.terminal.setCursorPos(self.xCursor, self.yCursor) + self.terminal.setCursorBlink(true) + else + self.terminal.setCursorBlink(false) + end + + return self +end + +function Render:rectOverlaps(r1, r2) + return not (r1.x + r1.width <= r2.x or + r2.x + r2.width <= r1.x or + r1.y + r1.height <= r2.y or + r2.y + r2.height <= r1.y) +end + +function Render:mergeRects(target, source) + local x1 = math.min(target.x, source.x) + local y1 = math.min(target.y, source.y) + local x2 = math.max(target.x + target.width, source.x + source.width) + local y2 = math.max(target.y + target.height, source.y + source.height) + + target.x = x1 + target.y = y1 + target.width = x2 - x1 + target.height = y2 - y1 +end + +function Render:setCursor(x, y, blink) + self.terminal.setCursorPos(x, y) + self.terminal.setCursorBlink(blink) + self.xCursor = x + self.yCursor = y + self.blink = blink + return self +end + +function Render:clearArea(x, y, width, height, bg) + local bgChar = colorChars[bg] or "f" + for dy=0, height-1 do + local cy = y + dy + if cy >= 1 and cy <= self.height then + local text = string.rep(" ", width) + local color = string.rep(bgChar, width) + self:blit(x, cy, text, "0", bgChar) + end + end + return self +end + +function Render:getSize() + return self.width, self.height +end + +return Render", + ["propertySystem.lua"] = "local deepCopy = require("libraries/utils").deepCopy +local expect = require("libraries/expect") +local errorManager = require("errorManager") +local log = require("log") + +--- @class PropertySystem +local PropertySystem = {} +PropertySystem.__index = PropertySystem + +PropertySystem._properties = {} +local blueprintTemplates = {} + +PropertySystem._setterHooks = {} + +function PropertySystem.addSetterHook(hook) + table.insert(PropertySystem._setterHooks, hook) +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) + if not rawget(class, '_properties') then + class._properties = {} + end + + class._properties[name] = { + type = config.type, + default = config.default, + canTriggerRender = config.canTriggerRender, + getter = config.getter, + setter = config.setter, + } + + local capitalizedName = name:sub(1,1):upper() .. name:sub(2) + + class["get" .. capitalizedName] = function(self, ...) + expect(1, self, "element") + local value = self._values[name] + if type(value) == "function" and config.type ~= "function" then + value = value(self) + end + return config.getter and config.getter(self, value, ...) or value + end + + class["set" .. capitalizedName] = function(self, value, ...) + expect(1, self, "element") + value = applyHooks(self, name, value, config) + + if type(value) ~= "function" then + expect(2, value, config.type) + end + + if config.setter then + value = config.setter(self, value, ...) + end + + self:_updateProperty(name, value) + return self + end +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 +--- @param elementClass table The element class to create a blueprint from +--- @return table blueprint A table containing all property definitions +function PropertySystem.blueprint(elementClass, properties, basalt, parent) + if not blueprintTemplates[elementClass] then + local template = { + basalt = basalt, + __isBlueprint = true, + _values = properties or {}, + _events = {}, + render = function() end, + dispatchEvent = function() end, + init = function() end, + } + + template.loaded = function(self, callback) + self.loadedCallback = callback + return template + end + + template.create = function(self) + local element = elementClass.new() + element:init({}, self.basalt) + for name, value in pairs(self._values) do + element._values[name] = value + end + for name, callbacks in pairs(self._events) do + for _, callback in ipairs(callbacks) do + element[name](element, callback) + end + end + if(parent~=nil)then + parent:addChild(element) + end + element:updateRender() + self.loadedCallback(element) + element:postInit() + return element + end + + local currentClass = elementClass + while currentClass do + if rawget(currentClass, '_properties') then + for name, config in pairs(currentClass._properties) do + if type(config.default) == "table" then + template._values[name] = deepCopy(config.default) + else + template._values[name] = config.default + end + end + end + currentClass = getmetatable(currentClass) and rawget(getmetatable(currentClass), '__index') + end + + blueprintTemplates[elementClass] = template + end + + local blueprint = { + _values = {}, + _events = {}, + loadedCallback = function() end, + } + + blueprint.get = function(name) + local value = blueprint._values[name] + local config = elementClass._properties[name] + if type(value) == "function" and config.type ~= "function" then + value = value(blueprint) + end + return value + end + blueprint.set = function(name, value) + blueprint._values[name] = value + return blueprint + end + + setmetatable(blueprint, { + __index = function(self, k) + if k:match("^on%u") then + return function(_, callback) + self._events[k] = self._events[k] or {} + table.insert(self._events[k], callback) + return self + end + end + if k:match("^get%u") then + local propName = k:sub(4,4):lower() .. k:sub(5) + return function() + return self._values[propName] + end + end + if k:match("^set%u") then + local propName = k:sub(4,4):lower() .. k:sub(5) + return function(_, value) + self._values[propName] = value + return self + end + end + return blueprintTemplates[elementClass][k] + end + }) + + return blueprint +end + +function PropertySystem.createFromBlueprint(elementClass, blueprint, basalt) + local element = elementClass.new({}, basalt) + for name, value in pairs(blueprint._values) do + if type(value) == "table" then + element._values[name] = deepCopy(value) + else + element._values[name] = value + end + end + + return element +end + +function PropertySystem:__init() + self._values = {} + self._observers = {} + + self.set = function(name, value, ...) + local oldValue = self._values[name] + local config = self._properties[name] + if(config~=nil)then + if(config.setter) then + value = config.setter(self, value, ...) + end + if config.canTriggerRender then + self:updateRender() + end + self._values[name] = applyHooks(self, name, value, config) + if oldValue ~= value and self._observers[name] then + for _, callback in ipairs(self._observers[name]) do + callback(self, value, oldValue) + end + end + end + end + + self.get = function(name, ...) + local value = self._values[name] + local config = self._properties[name] + if(config==nil)then errorManager.error("Property not found: "..name) return end + if type(value) == "function" and config.type ~= "function" then + value = value(self) + end + return config.getter and config.getter(self, value, ...) or value + end + + local properties = {} + local currentClass = getmetatable(self).__index + + while currentClass do + if rawget(currentClass, '_properties') then + for name, config in pairs(currentClass._properties) do + if not properties[name] then + properties[name] = config + end + end + end + currentClass = getmetatable(currentClass) and rawget(getmetatable(currentClass), '__index') + end + + self._properties = properties + + local originalMT = getmetatable(self) + local originalIndex = originalMT.__index + setmetatable(self, { + __index = function(t, k) + local config = self._properties[k] + if config then + local value = self._values[k] + if type(value) == "function" and config.type ~= "function" then + value = value(self) + end + return value + end + if type(originalIndex) == "function" then + return originalIndex(t, k) + else + return originalIndex[k] + end + end, + __newindex = function(t, k, v) + local config = self._properties[k] + if config then + if config.setter then + v = config.setter(self, v) + end + v = applyHooks(self, k, v, config) + self:_updateProperty(k, v) + else + rawset(t, k, v) + end + end, + __tostring = function(self) + return string.format("Object: %s (id: %s)", self._values.type, self.id) + end + }) + + for name, config in pairs(properties) do + if self._values[name] == nil then + if type(config.default) == "table" then + self._values[name] = deepCopy(config.default) + else + self._values[name] = config.default + end + end + end + + return self +end + +function PropertySystem:_updateProperty(name, value) + local oldValue = self._values[name] + if type(oldValue) == "function" then + oldValue = oldValue(self) + end + + self._values[name] = value + local newValue = type(value) == "function" and value(self) or value + + if oldValue ~= newValue then + if self._properties[name].canTriggerRender then + self:updateRender() + end + if self._observers[name] then + for _, callback in ipairs(self._observers[name]) do + callback(self, newValue, oldValue) + end + end + end +end + +function PropertySystem:observe(name, callback) + self._observers[name] = self._observers[name] or {} + table.insert(self._observers[name], callback) + return self +end + +function PropertySystem:removeObserver(name, callback) + if self._observers[name] then + for i, cb in ipairs(self._observers[name]) do + if cb == callback then + table.remove(self._observers[name], i) + if #self._observers[name] == 0 then + self._observers[name] = nil + end + break + end + end + end + return self +end + +function PropertySystem:removeAllObservers(name) + if name then + self._observers[name] = nil + else + self._observers = {} + end + return self +end + +function PropertySystem:instanceProperty(name, config) + PropertySystem.defineProperty(self, name, config) + self._values[name] = config.default + return self +end + +function PropertySystem:removeProperty(name) + self._values[name] = nil + self._properties[name] = nil + self._observers[name] = nil + + local capitalizedName = name:sub(1,1):upper() .. name:sub(2) + self["get" .. capitalizedName] = nil + self["set" .. capitalizedName] = nil + return self +end + +function PropertySystem:getPropertyConfig(name) + return self._properties[name] +end + +return PropertySystem", + ["elements/Dropdown.lua"] = "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() + local self = setmetatable({}, Dropdown):__init() + self.set("width", 16) + self.set("height", 1) + self.set("z", 8) + 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 +", + ["elements/Input.lua"] = "local VisualElement = require("elements/VisualElement") +local tHex = require("libraries/colorHex") + +---@class Input : VisualElement +local Input = setmetatable({}, VisualElement) +Input.__index = Input + +---@property text string Input - text to be displayed +Input.defineProperty(Input, "text", {default = "", type = "string", canTriggerRender = true}) + +---@property cursorPos number Input - current cursor position +Input.defineProperty(Input, "cursorPos", {default = 1, 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 +--- @return Input object The newly created Input instance +--- @usage local element = Input.new("myId", basalt) +function Input.new() + local self = setmetatable({}, Input):__init() + self.set("width", 8) + self.set("z", 3) + return self +end + +function Input:init(props, basalt) + VisualElement.init(self, props, basalt) + self.set("type", "Input") +end + +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 + +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 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 + + local relativePos = self.get("cursorPos") - self.get("viewOffset") + self:setCursor(relativePos, 1, true) +end + +function Input:focus() + VisualElement.focus(self) + self:updateRender() +end + +function Input:blur() + VisualElement.blur(self) + 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 cursorPos = self.get("cursorPos") + local viewOffset = self.get("viewOffset") + 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 + + if viewOffset > textLength - width then + self.set("viewOffset", math.max(0, textLength - width)) + end +end + +function Input:render() + local text = self.get("text") + local viewOffset = self.get("viewOffset") + local width = self.get("width") + 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")) +end + +return Input", + ["plugins/benchmark.lua"] = "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 +}", + ["elements/Menu.lua"] = "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() + local self = setmetatable({}, Menu):__init() + self.set("width", 30) + self.set("height", 1) + self.set("background", colors.gray) + 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 +", + ["elements/BaseFrame.lua"] = "local elementManager = require("elementManager") +local Container = elementManager.getElement("Container") +local Render = require("render") + +---@class BaseFrame : Container +local BaseFrame = setmetatable({}, Container) +BaseFrame.__index = BaseFrame + +---@property text term term nil text +BaseFrame.defineProperty(BaseFrame, "term", {default = nil, type = "table", setter = function(self, value) + if value == nil or value.setCursorPos == nil then + return value + end + self._render = Render.new(value) + self._renderUpdate = true + local width, height = value.getSize() + self.set("width", width) + self.set("height", height) + return value +end}) + +function BaseFrame.new() + local self = setmetatable({}, BaseFrame):__init() + self.set("term", term.current()) + self.set("background", colors.lightGray) + return self +end + +function BaseFrame:init(props, basalt) + Container.init(self, props, basalt) + self.set("type", "BaseFrame") +end + +function BaseFrame:multiBlit(x, y, width, height, text, fg, bg) + self._render:multiBlit(x, y, width, height, text, fg, bg) +end + +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) +end + +function BaseFrame:render() + if(self._renderUpdate) then + if self._render ~= nil then + Container.render(self) + self._render:render() + self._renderUpdate = false + end + end +end + +return BaseFrame", + ["LuaLS.lua"] = "---@class BaseFrame +---@field text term +local BaseFrame = {} + +--- Gets the nil text +---@generic Element: BaseFrame +---@param self Element +---@return term +function BaseFrame:getText() + return self.text +end + +--- Sets the nil text +---@generic Element: BaseFrame +---@param self Element +---@param text term +---@return Element +function BaseFrame:setText(text) + self.text = text + return self +end + + +---@class Button +---@field text string +local Button = {} + +--- Gets the Button text +---@generic Element: Button +---@param self Element +---@return string +function Button:getText() + return self.text +end + +--- Sets the Button text +---@generic Element: Button +---@param self Element +---@param text string +---@return Element +function Button:setText(text) + self.text = text + return self +end + +--- The event that is triggered when the button is clicked +---@generic Element: Button +---@param self Element +---@param callback function +---@return Element +function Button:onMouseClick(callback) + return self +end + + +---@class Checkbox +---@field checked boolean +---@field text string +---@field symbol string +local Checkbox = {} + +--- Gets the checkbox is checked +---@generic Element: Checkbox +---@param self Element +---@return boolean +function Checkbox:getChecked() + return self.checked +end + +--- Sets the checkbox is checked +---@generic Element: Checkbox +---@param self Element +---@param checked boolean +---@return Element +function Checkbox:setChecked(checked) + self.checked = checked + return self +end + +--- Gets the text +---@generic Element: Checkbox +---@param self Element +---@return string +function Checkbox:getText() + return self.text +end + +--- Sets the text +---@generic Element: Checkbox +---@param self Element +---@param text string +---@return Element +function Checkbox:setText(text) + self.text = text + return self +end + +--- Gets the symbol +---@generic Element: Checkbox +---@param self Element +---@return string +function Checkbox:getSymbol() + return self.symbol +end + +--- Sets the symbol +---@generic Element: Checkbox +---@param self Element +---@param symbol string +---@return Element +function Checkbox:setSymbol(symbol) + self.symbol = symbol + return self +end + + +---@class Container +local Container = {} + +--- Adds a new Button to the container +---@generic Element: Container +---@param self Element +---@return Button +function Container:addButton() + return self +end + +--- Adds a new Checkbox to the container +---@generic Element: Container +---@param self Element +---@return Checkbox +function Container:addCheckbox() + return self +end + +--- Adds a new Container to the container +---@generic Element: Container +---@param self Element +---@return Container +function Container:addContainer() + return self +end + +--- Adds a new Dropdown to the container +---@generic Element: Container +---@param self Element +---@return Dropdown +function Container:addDropdown() + return self +end + +--- Adds a new Flexbox to the container +---@generic Element: Container +---@param self Element +---@return Flexbox +function Container:addFlexbox() + return self +end + +--- Adds a new Frame to the container +---@generic Element: Container +---@param self Element +---@return Frame +function Container:addFrame() + return self +end + +--- Adds a new Input to the container +---@generic Element: Container +---@param self Element +---@return Input +function Container:addInput() + return self +end + +--- Adds a new Label to the container +---@generic Element: Container +---@param self Element +---@return Label +function Container:addLabel() + return self +end + +--- Adds a new List to the container +---@generic Element: Container +---@param self Element +---@return List +function Container:addList() + return self +end + +--- Adds a new Menu to the container +---@generic Element: Container +---@param self Element +---@return Menu +function Container:addMenu() + return self +end + +--- Adds a new Program to the container +---@generic Element: Container +---@param self Element +---@return Program +function Container:addProgram() + return self +end + +--- Adds a new ProgressBar to the container +---@generic Element: Container +---@param self Element +---@return ProgressBar +function Container:addProgressBar() + return self +end + +--- Adds a new Slider to the container +---@generic Element: Container +---@param self Element +---@return Slider +function Container:addSlider() + return self +end + +--- Adds a new Table to the container +---@generic Element: Container +---@param self Element +---@return Table +function Container:addTable() + return self +end + +--- Adds a new Tree to the container +---@generic Element: Container +---@param self Element +---@return Tree +function Container:addTree() + return self +end + +--- Adds a new VisualElement to the container +---@generic Element: Container +---@param self Element +---@return VisualElement +function Container:addVisualElement() + return self +end + + +---@class Input +---@field text string +---@field cursorPos number +---@field viewOffset number +local Input = {} + +--- Gets the - text to be displayed +---@generic Element: Input +---@param self Element +---@return string +function Input:getText() + return self.text +end + +--- Sets the - text to be displayed +---@generic Element: Input +---@param self Element +---@param text string +---@return Element +function Input:setText(text) + self.text = text + return self +end + +--- Gets the - current cursor position +---@generic Element: Input +---@param self Element +---@return number +function Input:getCursorPos() + return self.cursorPos +end + +--- Sets the - current cursor position +---@generic Element: Input +---@param self Element +---@param cursorPos number +---@return Element +function Input:setCursorPos(cursorPos) + self.cursorPos = cursorPos + return self +end + +--- Gets the - offset of view +---@generic Element: Input +---@param self Element +---@return number +function Input:getViewOffset() + return self.viewOffset +end + +--- Sets the - offset of view +---@generic Element: Input +---@param self Element +---@param viewOffset number +---@return Element +function Input:setViewOffset(viewOffset) + self.viewOffset = viewOffset + return self +end + + +---@class Label +---@field text string +local Label = {} + +--- Gets the Label text to be displayed +---@generic Element: Label +---@param self Element +---@return string +function Label:getText() + return self.text +end + +--- Sets the Label text to be displayed +---@generic Element: Label +---@param self Element +---@param text string +---@return Element +function Label:setText(text) + self.text = text + return self +end + + +---@class List +---@field items table +---@field selectedIndex number +---@field selectable boolean +---@field offset number +---@field selectedColor color +local List = {} + +--- Gets the of items to display +---@generic Element: List +---@param self Element +---@return table +function List:getItems() + return self.items +end + +--- Sets the of items to display +---@generic Element: List +---@param self Element +---@param items table +---@return Element +function List:setItems(items) + self.items = items + return self +end + +--- Gets the selected item index +---@generic Element: List +---@param self Element +---@return number +function List:getSelectedIndex() + return self.selectedIndex +end + +--- Sets the selected item index +---@generic Element: List +---@param self Element +---@param selectedIndex number +---@return Element +function List:setSelectedIndex(selectedIndex) + self.selectedIndex = selectedIndex + return self +end + +--- Gets the items can be selected +---@generic Element: List +---@param self Element +---@return boolean +function List:getSelectable() + return self.selectable +end + +--- Sets the items can be selected +---@generic Element: List +---@param self Element +---@param selectable boolean +---@return Element +function List:setSelectable(selectable) + self.selectable = selectable + return self +end + +--- Gets the offset +---@generic Element: List +---@param self Element +---@return number +function List:getOffset() + return self.offset +end + +--- Sets the offset +---@generic Element: List +---@param self Element +---@param offset number +---@return Element +function List:setOffset(offset) + self.offset = offset + return self +end + +--- Gets the for selected item +---@generic Element: List +---@param self Element +---@return color +function List:getSelectedColor() + return self.selectedColor +end + +--- Sets the for selected item +---@generic Element: List +---@param self Element +---@param selectedColor color +---@return Element +function List:setSelectedColor(selectedColor) + self.selectedColor = selectedColor + return self +end + + +---@class ProgressBar +---@field progress number +---@field showPercentage boolean +---@field progressColor color +local ProgressBar = {} + +--- Gets the progress (0-100) +---@generic Element: ProgressBar +---@param self Element +---@return number +function ProgressBar:getProgress() + return self.progress +end + +--- Sets the progress (0-100) +---@generic Element: ProgressBar +---@param self Element +---@param progress number +---@return Element +function ProgressBar:setProgress(progress) + self.progress = progress + return self +end + +--- Gets the percentage text +---@generic Element: ProgressBar +---@param self Element +---@return boolean +function ProgressBar:getShowPercentage() + return self.showPercentage +end + +--- Sets the percentage text +---@generic Element: ProgressBar +---@param self Element +---@param showPercentage boolean +---@return Element +function ProgressBar:setShowPercentage(showPercentage) + self.showPercentage = showPercentage + return self +end + +--- Gets the bar color +---@generic Element: ProgressBar +---@param self Element +---@return color +function ProgressBar:getProgressColor() + return self.progressColor +end + +--- Sets the bar color +---@generic Element: ProgressBar +---@param self Element +---@param progressColor color +---@return Element +function ProgressBar:setProgressColor(progressColor) + self.progressColor = progressColor + return self +end + + +---@class Slider +---@field step number +---@field max number +---@field horizontal boolean +---@field barColor color +---@field sliderColor color +local Slider = {} + +--- Gets the Current step position (1 to width/height) +---@generic Element: Slider +---@param self Element +---@return number +function Slider:getStep() + return self.step +end + +--- Sets the Current step position (1 to width/height) +---@generic Element: Slider +---@param self Element +---@param step number +---@return Element +function Slider:setStep(step) + self.step = step + return self +end + +--- Gets the Maximum value for value conversion +---@generic Element: Slider +---@param self Element +---@return number +function Slider:getMax() + return self.max +end + +--- Sets the Maximum value for value conversion +---@generic Element: Slider +---@param self Element +---@param max number +---@return Element +function Slider:setMax(max) + self.max = max + return self +end + +--- Gets the Whether the slider is horizontal +---@generic Element: Slider +---@param self Element +---@return boolean +function Slider:getHorizontal() + return self.horizontal +end + +--- Sets the Whether the slider is horizontal +---@generic Element: Slider +---@param self Element +---@param horizontal boolean +---@return Element +function Slider:setHorizontal(horizontal) + self.horizontal = horizontal + return self +end + +--- Gets the Colors for the slider bar +---@generic Element: Slider +---@param self Element +---@return color +function Slider:getBarColor() + return self.barColor +end + +--- Sets the Colors for the slider bar +---@generic Element: Slider +---@param self Element +---@param barColor color +---@return Element +function Slider:setBarColor(barColor) + self.barColor = barColor + return self +end + +--- Gets the color of the slider handle +---@generic Element: Slider +---@param self Element +---@return color +function Slider:getSliderColor() + return self.sliderColor +end + +--- Sets the color of the slider handle +---@generic Element: Slider +---@param self Element +---@param sliderColor color +---@return Element +function Slider:setSliderColor(sliderColor) + self.sliderColor = sliderColor + return self +end + + +---@class VisualElement +---@field x number +---@field y number +---@field z number +---@field width number +---@field height number +---@field background color +---@field foreground color +---@field clicked boole +---@field backgroundEnabled boolean +---@field focused boolean +local VisualElement = {} + +--- Gets the x position of the element +---@generic Element: VisualElement +---@param self Element +---@return number +function VisualElement:getX() + return self.x +end + +--- Sets the x position of the element +---@generic Element: VisualElement +---@param self Element +---@param x number +---@return Element +function VisualElement:setX(x) + self.x = x + return self +end + +--- Gets the y position of the element +---@generic Element: VisualElement +---@param self Element +---@return number +function VisualElement:getY() + return self.y +end + +--- Sets the y position of the element +---@generic Element: VisualElement +---@param self Element +---@param y number +---@return Element +function VisualElement:setY(y) + self.y = y + return self +end + +--- Gets the z position of the element +---@generic Element: VisualElement +---@param self Element +---@return number +function VisualElement:getZ() + return self.z +end + +--- Sets the z position of the element +---@generic Element: VisualElement +---@param self Element +---@param z number +---@return Element +function VisualElement:setZ(z) + self.z = z + return self +end + +--- Gets the width of the element +---@generic Element: VisualElement +---@param self Element +---@return number +function VisualElement:getWidth() + return self.width +end + +--- Sets the width of the element +---@generic Element: VisualElement +---@param self Element +---@param width number +---@return Element +function VisualElement:setWidth(width) + self.width = width + return self +end + +--- Gets the height of the element +---@generic Element: VisualElement +---@param self Element +---@return number +function VisualElement:getHeight() + return self.height +end + +--- Sets the height of the element +---@generic Element: VisualElement +---@param self Element +---@param height number +---@return Element +function VisualElement:setHeight(height) + self.height = height + return self +end + +--- Gets the background color of the element +---@generic Element: VisualElement +---@param self Element +---@return color +function VisualElement:getBackground() + return self.background +end + +--- Sets the background color of the element +---@generic Element: VisualElement +---@param self Element +---@param background color +---@return Element +function VisualElement:setBackground(background) + self.background = background + return self +end + +--- Gets the foreground color of the element +---@generic Element: VisualElement +---@param self Element +---@return color +function VisualElement:getForeground() + return self.foreground +end + +--- Sets the foreground color of the element +---@generic Element: VisualElement +---@param self Element +---@param foreground color +---@return Element +function VisualElement:setForeground(foreground) + self.foreground = foreground + return self +end + +--- Gets the false element is currently clicked +---@generic Element: VisualElement +---@param self Element +---@return boole +function VisualElement:getClicked() + return self.clicked +end + +--- Sets the false element is currently clicked +---@generic Element: VisualElement +---@param self Element +---@param clicked boole +---@return Element +function VisualElement:setClicked(clicked) + self.clicked = clicked + return self +end + +--- Gets the whether the background is enabled +---@generic Element: VisualElement +---@param self Element +---@return boolean +function VisualElement:getBackgroundEnabled() + return self.backgroundEnabled +end + +--- Sets the whether the background is enabled +---@generic Element: VisualElement +---@param self Element +---@param backgroundEnabled boolean +---@return Element +function VisualElement:setBackgroundEnabled(backgroundEnabled) + self.backgroundEnabled = backgroundEnabled + return self +end + +--- Gets the whether the element is focused +---@generic Element: VisualElement +---@param self Element +---@return boolean +function VisualElement:getFocused() + return self.focused +end + +--- Sets the whether the element is focused +---@generic Element: VisualElement +---@param self Element +---@param focused boolean +---@return Element +function VisualElement:setFocused(focused) + self.focused = focused + return self +end +", + ["elements/Tree.lua"] = "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() + local self = setmetatable({}, Tree):__init() + self.set("width", 30) + self.set("height", 10) + self.set("z", 5) + 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 +", + ["plugins/xml.lua"] = "local errorManager = require("errorManager") + +local function parseTag(str) + local tag = { + attributes = {} + } + tag.name = str:match("<(%w+)") + for k,v in str:gmatch('%s(%w+)="([^"]-)"') do + tag.attributes[k] = v + end + return tag +end + +local function parseXML(self, xmlString) + local stack = {} + local root = {children = {}} + local current = root + local inCDATA = false + local cdataContent = "" + + for line in xmlString:gmatch("[^\r\n]+") do + line = line:match("^%s*(.-)%s*$") + self.basalt.LOGGER.debug("Parsing line: " .. line) + + if line:match("^$") and inCDATA then + inCDATA = false + current.content = cdataContent + elseif inCDATA then + cdataContent = cdataContent .. line .. "\n" + elseif line:match("^<[^/]") then + local tag = parseTag(line) + tag.children = {} + tag.content = "" + table.insert(current.children, tag) + + if not line:match("/>$") then + table.insert(stack, current) + current = tag + end + elseif line:match("^ 0 then + createElements(node, element, scope) + end + end + end + end + + createElements(tree, self, scope) + return self +end + +return { + BaseElement = BaseElement, + Container = Container +} + +", + ["elements/List.lua"] = "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() + local self = setmetatable({}, List):__init() + self.set("width", 16) + self.set("height", 8) + self.set("background", colors.gray) + 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 _, index = self:getRelativePosition(x, y) + + local adjustedIndex = index + self.get("offset") + local items = self.get("items") + + 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() + return true + end + end + return false +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:onSelect(callback) + self:registerCallback("select", callback) + return self +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") + local width = self.get("width") + + for i = 1, height do + local itemIndex = i + offset + 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 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 + +return List +", + ["libraries/utils.lua"] = "local floor, len = math.floor, string.len + +local utils = {} + +function utils.getCenteredPosition(text, totalWidth, totalHeight) + local textLength = len(text) + + local x = floor((totalWidth - textLength+1) / 2 + 0.5) + local y = floor(totalHeight / 2 + 0.5) + + return x, y +end + +function utils.deepCopy(obj) + if type(obj) ~= "table" then + return obj + end + + local copy = {} + for k, v in pairs(obj) do + copy[utils.deepCopy(k)] = utils.deepCopy(v) + end + + return copy +end + +function utils.copy(obj) + local new = {} + for k,v in pairs(obj)do + new[k] = v + end + return new +end + +function utils.reverse(t) + local reversed = {} + for i = #t, 1, -1 do + table.insert(reversed, t[i]) + end + return reversed +end + +function utils.uuid() + return string.format('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', + math.random(0, 0xffff), math.random(0, 0xffff), math.random(0, 0xffff), + math.random(0, 0x0fff) + 0x4000, math.random(0, 0x3fff) + 0x8000, + math.random(0, 0xffff), math.random(0, 0xffff), math.random(0, 0xffff)) +end + +function utils.split(str, sep) + local parts = {} + local start = 1 + local len = len(str) + local splitIndex = 1 + + while true do + local index = str:find(sep, start, true) + if not index then + parts[splitIndex] = str:sub(start, len) + break + end + + parts[splitIndex] = str:sub(start, index - 1) + start = index + 1 + splitIndex = splitIndex + 1 + end + + return parts +end + +return utils", + ["elements/ProgressBar.lua"] = "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() + local self = setmetatable({}, ProgressBar):__init() + 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", + ["elementManager.lua"] = "local args = table.pack(...) +local dir = fs.getDir(args[2] or "basalt") +local subDir = args[1] +if(dir==nil)then + error("Unable to find directory "..args[2].." please report this bug to our discord.") +end + +local log = require("log") +local defaultPath = package.path +local format = "path;/path/?.lua;/path/?/init.lua;" +local main = format:gsub("path", dir) + +local ElementManager = {} +ElementManager._elements = {} +ElementManager._plugins = {} +ElementManager._APIs = {} +local elementsDirectory = fs.combine(dir, "elements") +local pluginsDirectory = fs.combine(dir, "plugins") + +log.info("Loading elements from "..elementsDirectory) +if fs.exists(elementsDirectory) then + for _, file in ipairs(fs.list(elementsDirectory)) do + local name = file:match("(.+).lua") + if name then + log.debug("Found element: "..name) + ElementManager._elements[name] = { + class = nil, + plugins = {}, + loaded = false + } + end + end +end + +log.info("Loading plugins from "..pluginsDirectory) +if fs.exists(pluginsDirectory) then + for _, file in ipairs(fs.list(pluginsDirectory)) do + local name = file:match("(.+).lua") + if name then + log.debug("Found plugin: "..name) + local plugin = require(fs.combine("plugins", name)) + if type(plugin) == "table" then + for k,v in pairs(plugin) do + if(k ~= "API")then + if(ElementManager._plugins[k]==nil)then + ElementManager._plugins[k] = {} + end + table.insert(ElementManager._plugins[k], v) + else + ElementManager._APIs[name] = v + end + end + end + end + end +end + +function ElementManager.loadElement(name) + if not ElementManager._elements[name].loaded then + package.path = main.."rom/?" + local element = require(fs.combine("elements", name)) + package.path = defaultPath + ElementManager._elements[name] = { + class = element, + plugins = element.plugins, + loaded = true + } + log.debug("Loaded element: "..name) + + if(ElementManager._plugins[name]~=nil)then + for _, plugin in pairs(ElementManager._plugins[name]) do + if(plugin.setup)then + plugin.setup(element) + end + + if(plugin.hooks)then + for methodName, hooks in pairs(plugin.hooks) do + local original = element[methodName] + if(type(original)~="function")then + error("Element "..name.." does not have a method "..methodName) + end + if(type(hooks)=="function")then + element[methodName] = function(self, ...) + local result = original(self, ...) + local hookResult = hooks(self, ...) + return hookResult == nil and result or hookResult + end + elseif(type(hooks)=="table")then + element[methodName] = function(self, ...) + if hooks.pre then hooks.pre(self, ...) end + local result = original(self, ...) + if hooks.post then hooks.post(self, ...) end + return result + end + end + end + end + + for funcName, func in pairs(plugin) do + if funcName ~= "setup" and funcName ~= "hooks" then + element[funcName] = func + end + end + end + end + end +end + +function ElementManager.getElement(name) + if not ElementManager._elements[name].loaded then + ElementManager.loadElement(name) + end + return ElementManager._elements[name].class +end + +function ElementManager.getElementList() + return ElementManager._elements +end + +function ElementManager.getAPI(name) + return ElementManager._APIs[name] +end + +return ElementManager", + ["elements/Checkbox.lua"] = "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() + local self = setmetatable({}, Checkbox):__init() + 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", + ["plugins/reactive.lua"] = "local errorManager = require("errorManager") +local PropertySystem = require("propertySystem") +local log = require("log") + +local protectedNames = { + colors = true, + math = true, + clamp = true, + round = true +} + +local mathEnv = { + clamp = function(val, min, max) + return math.min(math.max(val, min), max) + end, + round = function(val) + return math.floor(val + 0.5) + end +} + +local function parseExpression(expr, element, propName) + expr = expr:gsub("^{(.+)}$", "%1") + + expr = expr:gsub("([%w_]+)%$([%w_]+)", function(obj, prop) + if obj == "self" then + return string.format('__getState("%s")', prop) + elseif obj == "parent" then + return string.format('__getParentState("%s")', prop) + else + return string.format('__getElementState("%s", "%s")', obj, prop) + end + end) + + expr = expr:gsub("([%w_]+)%.([%w_]+)", function(obj, prop) + if protectedNames[obj] then + return obj.."."..prop + end + return string.format('__getProperty("%s", "%s")', obj, prop) + end) + + local env = setmetatable({ + colors = colors, + math = math, + 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) + if objName == "self" then + return element.get(propName) + elseif objName == "parent" then + return element.parent.get(propName) + else + local target = element:getBaseFrame():getChild(objName) + if not target then + errorManager.header = "Reactive evaluation error" + errorManager.error("Could not find element: " .. objName) + return nil + end + + return target.get(propName) + end + end + }, { __index = mathEnv }) + + 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) + if not func then + errorManager.header = "Reactive evaluation error" + errorManager.error("Invalid expression: " .. err) + return function() return nil end + end + + return func +end + +local function validateReferences(expr, element) + for ref in expr:gmatch("([%w_]+)%.") do + if not protectedNames[ref] then + if ref == "self" then + elseif ref == "parent" then + if not element.parent then + errorManager.header = "Reactive evaluation error" + errorManager.error("No parent element available") + return false + end + else + local target = element:getBaseFrame():getChild(ref) + if not target then + errorManager.header = "Reactive evaluation error" + errorManager.error("Referenced element not found: " .. ref) + return false + end + end + end + end + return true +end + +local functionCache = setmetatable({}, {__mode = "k"}) + +local observerCache = setmetatable({}, { + __mode = "k", + __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) + end + end + + local observers = {} + for ref, prop in expr:gmatch("([%w_]+)%.([%w_]+)") do + if not protectedNames[ref] then + local target + if ref == "self" then + target = element + elseif ref == "parent" then + target = element.parent + else + target = element:getBaseFrame():getChild(ref) + end + + if target then + local observer = { + target = target, + property = prop, + callback = function() + element:updateRender() + end + } + target:observe(prop, observer.callback) + table.insert(observers, observer) + end + end + end + + observerCache[element][propertyName] = observers +end + +PropertySystem.addSetterHook(function(element, propertyName, value, config) + if type(value) == "string" and value:match("^{.+}$") then + local expr = value:gsub("^{(.+)}$", "%1") + if not validateReferences(expr, element) then + return config.default + end + + setupObservers(element, expr, propertyName) + + if not functionCache[element] then + functionCache[element] = {} + end + if not functionCache[element][value] then + local parsedFunc = parseExpression(value, element, propertyName) + functionCache[element][value] = parsedFunc + end + + return function(self) + local success, result = pcall(functionCache[element][value]) + if not success then + errorManager.header = "Reactive evaluation error" + if type(result) == "string" then + errorManager.error("Error evaluating expression: " .. result) + else + errorManager.error("Error evaluating expression") + end + return config.default + end + return result + end + end +end) + +local BaseElement = {} + +BaseElement.hooks = { + destroy = function(self) + if observerCache[self] then + for propName, observers in pairs(observerCache[self]) do + for _, observer in ipairs(observers) do + observer.target:removeObserver(observer.property, observer.callback) + end + end + observerCache[self] = nil + end + end +} + +return { + BaseElement = BaseElement +} +", + ["elements/Button.lua"] = "local elementManager = require("elementManager") +local VisualElement = elementManager.getElement("VisualElement") +local getCenteredPosition = require("libraries/utils").getCenteredPosition + +---@class Button : VisualElement +local Button = setmetatable({}, VisualElement) +Button.__index = Button + +---@property text string Button Button text +Button.defineProperty(Button, "text", {default = "Button", type = "string", canTriggerRender = true}) + +---@event mouse_click The event that is triggered when the button is clicked +Button.listenTo(Button, "mouse_click") +Button.listenTo(Button, "mouse_up") + +function Button.new() + local self = setmetatable({}, Button):__init() + self.set("width", 10) + self.set("height", 3) + self.set("z", 5) + return self +end + +function Button:init(props, basalt) + VisualElement.init(self, props, basalt) + self.set("type", "Button") +end + +function Button:render() + VisualElement.render(self) + local text = self.get("text") + local xO, yO = getCenteredPosition(text, self.get("width"), self.get("height")) + self:textFg(xO, yO, text, self.get("foreground")) +end + +return Button", + ["main.lua"] = "local elementManager = require("elementManager") +local errorManager = require("errorManager") +local propertySystem = require("propertySystem") + + +--- This is the UI Manager and the starting point for your project. The following functions allow you to influence the default behavior of Basalt. +--- +--- Before you can access Basalt, you need to add the following code on top of your file: +--- @usage local basalt = require("basalt") +--- What this code does is it loads basalt into the project, and you can access it by using the variable defined as "basalt". +-- @module Basalt +local basalt = {} +basalt.traceback = true +basalt._events = {} +basalt._schedule = {} +basalt._plugins = {} +basalt.LOGGER = require("log") +basalt.path = fs.getDir(select(2, ...)) + +local mainFrame = nil +local updaterActive = false +local _type = type + +local lazyElements = {} +local lazyElementCount = 10 +local lazyElementsTimer = 0 +local isLazyElementsTimerActive = false + + +local function queueLazyElements() + if(isLazyElementsTimerActive)then return end + lazyElementsTimer = os.startTimer(0.2) + isLazyElementsTimerActive = true +end + +local function loadLazyElements(count) + for _=1,count do + local blueprint = lazyElements[1] + if(blueprint)then + blueprint:create() + end + table.remove(lazyElements, 1) + end +end + +local function lazyElementsEventHandler(event, timerId) + if(event=="timer")then + if(timerId==lazyElementsTimer)then + loadLazyElements(lazyElementCount) + isLazyElementsTimerActive = false + lazyElementsTimer = 0 + if(#lazyElements>0)then + queueLazyElements() + end + return true + end + end +end + +--- Creates and returns a new UI element of the specified type. +--- @shortDescription Creates a new UI element +--- @param type string The type of element to create (e.g. "Button", "Label", "BaseFrame") +--- @param properties? string|table Optional name for the element or a table with properties to initialize the element with +--- @return table element The created element instance +--- @usage local button = basalt.create("Button") +function basalt.create(type, properties, lazyLoading, parent) + if(_type(properties)=="string")then properties = {name=properties} end + if(properties == nil)then properties = {name = type} end + local elementClass = elementManager.getElement(type) + if(lazyLoading)then + local blueprint = propertySystem.blueprint(elementClass, properties, basalt, parent) + table.insert(lazyElements, blueprint) + queueLazyElements() + return blueprint + else + local element = elementClass.new() + element:init(properties, basalt) + return element + end +end + +--- Creates and returns a new frame +--- @return table BaseFrame The created frame instance +--- @usage local mainFrame = basalt.createFrame() +function basalt.createFrame() + local frame = basalt.create("BaseFrame") + frame:postInit() + mainFrame = frame + return frame +end + +--- Returns the element manager instance +--- @return table ElementManager The element manager +--- @usage local manager = basalt.getElementManager() +function basalt.getElementManager() + return elementManager +end + +--- Gets or creates the main frame +--- @return BaseFrame table The main frame instance +--- @usage local frame = basalt.getMainFrame() +function basalt.getMainFrame() + if(mainFrame == nil)then + mainFrame = basalt.createFrame() + end + return mainFrame +end + +--- Sets the active frame +--- @param frame table The frame to set as active +--- @usage basalt.setActiveFrame(myFrame) +function basalt.setActiveFrame(frame) + mainFrame = frame +end + +--- Schedules a function to be updated +--- @function scheduleUpdate +--- @param func function The function to schedule +--- @return number Id The schedule ID +--- @usage local id = basalt.scheduleUpdate(myFunction) +function basalt.scheduleUpdate(func) + table.insert(basalt._schedule, func) + return #basalt._schedule +end + +--- Removes a scheduled update +--- @function removeSchedule +--- @param id number The schedule ID to remove +--- @usage basalt.removeSchedule(scheduleId) +function basalt.removeSchedule(id) + basalt._schedule[id] = nil +end + +--- @local Internal event handler +local function updateEvent(event, ...) + if(event=="terminate")then basalt.stop() end + if lazyElementsEventHandler(event, ...) then return end + + if(mainFrame)then + if(mainFrame:dispatchEvent(event, ...))then + return + end + end + + if basalt._events[event] then + for _, callback in ipairs(basalt._events[event]) do + callback(...) + end + end +end + +--- @local Internal render function +local function renderFrames() + if(mainFrame)then + mainFrame:render() + end +end + +--- Updates all scheduled functions +--- @usage basalt.update() +function basalt.update() + for k,v in pairs(basalt._schedule) do + if type(v)=="function" then + v() + end + end +end + +--- Stops the Basalt runtime +--- @usage basalt.stop() +function basalt.stop() + term.clear() + term.setCursorPos(1,1) + updaterActive = false +end + +--- Starts the Basalt runtime +--- @param isActive? boolean Whether to start active (default: true) +--- @usage basalt.run() +--- @usage basalt.run(false) +function basalt.run(isActive) + updaterActive = isActive + if(isActive==nil)then updaterActive = true end + local function f() + renderFrames() + while updaterActive do + updateEvent(os.pullEventRaw()) + renderFrames() + end + end + while updaterActive do + local ok, err = pcall(f) + if not(ok)then + errorManager.header = "Basalt Runtime Error" + errorManager.error(err) + end + end +end + +function basalt.getAPI(name) + return elementManager.getAPI(name) +end + + +return basalt", }, - ["elements"] = { - ["src/elements/Program.lua"] = { - ["name"] = "Program", - ["path"] = "/elements/Program.lua", - }, - ["src/elements/List.lua"] = { - ["name"] = "List", - ["path"] = "/elements/List.lua", - }, - ["src/elements/Button.lua"] = { - ["name"] = "Button", - ["path"] = "/elements/Button.lua", - }, - ["src/elements/ProgressBar.lua"] = { - ["name"] = "ProgressBar", - ["path"] = "/elements/ProgressBar.lua", - }, - ["src/elements/Frame.lua"] = { - ["name"] = "Frame", - ["path"] = "/elements/Frame.lua", - }, - ["src/elements/BaseFrame.lua"] = { - ["name"] = "BaseFrame", - ["path"] = "/elements/BaseFrame.lua", - }, - ["src/elements/Checkbox.lua"] = { - ["name"] = "Checkbox", - ["path"] = "/elements/Checkbox.lua", - }, - ["src/elements/Tree.lua"] = { - ["name"] = "Tree", - ["path"] = "/elements/Tree.lua", - }, - ["src/elements/Dropdown.lua"] = { - ["name"] = "Dropdown", - ["path"] = "/elements/Dropdown.lua", - }, - ["src/elements/Table.lua"] = { - ["name"] = "Table", - ["path"] = "/elements/Table.lua", - }, - ["src/elements/Container.lua"] = { - ["name"] = "Container", - ["path"] = "/elements/Container.lua", - }, - ["src/elements/Menu.lua"] = { - ["name"] = "Menu", - ["path"] = "/elements/Menu.lua", - }, - ["src/elements/Input.lua"] = { - ["name"] = "Input", - ["path"] = "/elements/Input.lua", - }, - ["src/elements/Slider.lua"] = { - ["name"] = "Slider", - ["path"] = "/elements/Slider.lua", - }, - ["src/elements/Flexbox.lua"] = { - ["name"] = "Flexbox", - ["path"] = "/elements/Flexbox.lua", - }, - ["src/elements/VisualElement.lua"] = { - ["name"] = "VisualElement", - ["path"] = "/elements/VisualElement.lua", - }, - ["src/elements/Label.lua"] = { - ["name"] = "Label", - ["path"] = "/elements/Label.lua", - }, - ["src/elements/BaseElement.lua"] = { - ["name"] = "BaseElement", - ["path"] = "/elements/BaseElement.lua", - }, - }, - ["plugins"] = { - ["src/plugins/debug.lua"] = { - ["name"] = "debug", - ["path"] = "/plugins/debug.lua", - }, - ["src/plugins/state.lua"] = { - ["name"] = "state", - ["path"] = "/plugins/state.lua", - }, - ["src/plugins/benchmark.lua"] = { - ["name"] = "benchmark", - ["path"] = "/plugins/benchmark.lua", - }, - ["src/plugins/xml.lua"] = { - ["name"] = "xml", - ["path"] = "/plugins/xml.lua", - }, - ["src/plugins/pluginTemplate.lua"] = { - ["name"] = "pluginTemplate", - ["path"] = "/plugins/pluginTemplate.lua", - }, - ["src/plugins/reactive.lua"] = { - ["name"] = "reactive", - ["path"] = "/plugins/reactive.lua", - }, - ["src/plugins/theme.lua"] = { - ["name"] = "theme", - ["path"] = "/plugins/theme.lua", - }, - ["src/plugins/animation.lua"] = { - ["name"] = "animation", - ["path"] = "/plugins/animation.lua", - }, - }, - ["repo"] = "https://raw.githubusercontent.com/Pyroxenium/Basalt2/master", } \ No newline at end of file