- Created Plugin loading system
- Added lazy loading system for elements (optional feature) - Improved rendering performance - Added ID system which is separated from Eement Names - Added Focussystem for container - Improved container performance by only rendering and handling events from visible childrens instead of all - Added label and input - Added animation and xml
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1 +1,4 @@
|
||||
test.lua
|
||||
test.lua
|
||||
test2.lua
|
||||
lua-ls-cc-tweaked-main
|
||||
test.xml
|
||||
222
src/LuaLS.lua
222
src/LuaLS.lua
@@ -1,222 +0,0 @@
|
||||
---@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 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 Container to the container
|
||||
---@generic Element: Container
|
||||
---@param self Element
|
||||
---@return Container
|
||||
function Container:addContainer()
|
||||
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 VisualElement to the container
|
||||
---@generic Element: Container
|
||||
---@param self Element
|
||||
---@return VisualElement
|
||||
function Container:addVisualElement()
|
||||
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 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 element is currently clicked
|
||||
---@generic Element: VisualElement
|
||||
---@param self Element
|
||||
---@return boolean
|
||||
function VisualElement:getClicked()
|
||||
return self.clicked
|
||||
end
|
||||
|
||||
--- Sets the element is currently clicked
|
||||
---@generic Element: VisualElement
|
||||
---@param self Element
|
||||
---@param clicked boolean
|
||||
---@return Element
|
||||
function VisualElement:setClicked(clicked)
|
||||
self.clicked = clicked
|
||||
return self
|
||||
end
|
||||
33
src/benchmark.lua
Normal file
33
src/benchmark.lua
Normal file
@@ -0,0 +1,33 @@
|
||||
-- Will temporary exist while developing, maybe i will create a benchmark plugin in future
|
||||
|
||||
local log = require("log")
|
||||
|
||||
local Benchmark = {}
|
||||
|
||||
function Benchmark.start(name)
|
||||
Benchmark[name] = {
|
||||
startTime = os.epoch("local"),
|
||||
updates = 0
|
||||
}
|
||||
end
|
||||
|
||||
function Benchmark.update(name)
|
||||
if Benchmark[name] then
|
||||
Benchmark[name].updates = Benchmark[name].updates + 1
|
||||
end
|
||||
end
|
||||
|
||||
function Benchmark.stop(name)
|
||||
if Benchmark[name] then
|
||||
local endTime = os.epoch("local")
|
||||
local duration = endTime - Benchmark[name].startTime
|
||||
local updates = Benchmark[name].updates
|
||||
|
||||
log.debug(string.format("[Benchmark] %s: %dms, %d updates, avg: %.2fms per update",
|
||||
name, duration, updates, duration/updates))
|
||||
|
||||
return duration, updates
|
||||
end
|
||||
end
|
||||
|
||||
return Benchmark
|
||||
@@ -1,10 +1,14 @@
|
||||
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 = {}
|
||||
@@ -27,55 +31,19 @@ if fs.exists(elementsDirectory) then
|
||||
end
|
||||
end
|
||||
|
||||
function ElementManager.extendMethod(element, methodName, newMethod, originalMethod)
|
||||
if not originalMethod then
|
||||
element[methodName] = newMethod
|
||||
return
|
||||
end
|
||||
element[methodName] = function(self, ...)
|
||||
if newMethod.before then
|
||||
newMethod.before(self, ...)
|
||||
end
|
||||
|
||||
local results
|
||||
if newMethod.override then
|
||||
results = {newMethod.override(self, originalMethod, ...)}
|
||||
else
|
||||
results = {originalMethod(self, ...)}
|
||||
end
|
||||
|
||||
if newMethod.after then
|
||||
newMethod.after(self, ...)
|
||||
end
|
||||
|
||||
return table.unpack(results)
|
||||
end
|
||||
end
|
||||
|
||||
function ElementManager.loadPlugin(name)
|
||||
local plugin = require("plugins/"..name)
|
||||
|
||||
-- Apply plugin to each targeted element
|
||||
for elementName, pluginData in pairs(plugin) do
|
||||
local element = ElementManager._elements[elementName]
|
||||
if element then
|
||||
-- Register properties
|
||||
if pluginData.properties then
|
||||
element.class.initialize(elementName.."Plugin")
|
||||
for propName, config in pairs(pluginData.properties) do
|
||||
element.class.registerProperty(propName, config)
|
||||
end
|
||||
end
|
||||
|
||||
-- Register/extend methods
|
||||
if pluginData.methods then
|
||||
for methodName, methodData in pairs(pluginData.methods) do
|
||||
ElementManager.extendMethod(
|
||||
element.class,
|
||||
methodName,
|
||||
methodData,
|
||||
element.class[methodName]
|
||||
)
|
||||
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(ElementManager._plugins[k]==nil)then
|
||||
ElementManager._plugins[k] = {}
|
||||
end
|
||||
table.insert(ElementManager._plugins[k], v)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -84,7 +52,9 @@ end
|
||||
|
||||
function ElementManager.loadElement(name)
|
||||
if not ElementManager._elements[name].loaded then
|
||||
local element = require("elements/"..name)
|
||||
package.path = main.."rom/?"
|
||||
local element = require(fs.combine("elements", name))
|
||||
package.path = defaultPath
|
||||
ElementManager._elements[name] = {
|
||||
class = element,
|
||||
plugins = element.plugins,
|
||||
@@ -92,22 +62,46 @@ function ElementManager.loadElement(name)
|
||||
}
|
||||
log.debug("Loaded element: "..name)
|
||||
|
||||
-- Load element's required plugins
|
||||
if element.requires then
|
||||
for pluginName, _ in pairs(element.requires) do
|
||||
--ElementManager.loadPlugin(pluginName)
|
||||
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, ...)
|
||||
original(self, ...)
|
||||
return hooks(self, ...)
|
||||
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] = function(self, ...)
|
||||
return func(self, ...)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ElementManager.registerPlugin(name, plugin)
|
||||
if not plugin.provides then
|
||||
error("Plugin must specify what it provides")
|
||||
end
|
||||
ElementManager._plugins[name] = plugin
|
||||
end
|
||||
|
||||
function ElementManager.getElement(name)
|
||||
if not ElementManager._elements[name].loaded then
|
||||
ElementManager.loadElement(name)
|
||||
@@ -119,11 +113,4 @@ function ElementManager.getElementList()
|
||||
return ElementManager._elements
|
||||
end
|
||||
|
||||
function ElementManager.generateId()
|
||||
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
|
||||
|
||||
return ElementManager
|
||||
@@ -1,4 +1,5 @@
|
||||
local PropertySystem = require("propertySystem")
|
||||
local uuid = require("/libraries/utils").uuid
|
||||
|
||||
--- The base class for all UI elements in Basalt
|
||||
--- @class BaseElement : PropertySystem
|
||||
@@ -7,45 +8,28 @@ BaseElement.__index = BaseElement
|
||||
BaseElement._events = {}
|
||||
|
||||
--- @property type string BaseElement The type identifier of the element
|
||||
BaseElement.defineProperty(BaseElement, "type", {default = "BaseElement", type = "string"})
|
||||
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"})
|
||||
|
||||
--- Creates a new BaseElement instance
|
||||
--- @param id string The unique identifier for this element
|
||||
--- @param basalt table The basalt instance
|
||||
--- @return table The newly created BaseElement instance
|
||||
--- @usage local element = BaseElement.new("myId", basalt)
|
||||
function BaseElement.new(id, basalt)
|
||||
local self = setmetatable({}, BaseElement):__init()
|
||||
self:init(id, basalt)
|
||||
self.set("type", "BaseElement")
|
||||
return self
|
||||
end
|
||||
|
||||
--- Initializes the BaseElement instance
|
||||
--- @param id string The unique identifier for this element
|
||||
--- @param basalt table The basalt instance
|
||||
--- @return table self The initialized instance
|
||||
function BaseElement:init(id, basalt)
|
||||
self.id = id
|
||||
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, ...)
|
||||
end
|
||||
end
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
--- 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
|
||||
@@ -57,6 +41,57 @@ function BaseElement.listenTo(class, eventName)
|
||||
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(props, basalt)
|
||||
local self = setmetatable({}, BaseElement):__init()
|
||||
self:init(props, basalt)
|
||||
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)
|
||||
if(type(props) == "table")then
|
||||
for k,v in pairs(props)do
|
||||
self[k] = v
|
||||
end
|
||||
end
|
||||
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
|
||||
|
||||
--- 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)
|
||||
@@ -113,6 +148,25 @@ function BaseElement:fireEvent(event, ...)
|
||||
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 true
|
||||
end
|
||||
|
||||
--- Requests a render update for this element
|
||||
--- @usage element:updateRender()
|
||||
function BaseElement:updateRender()
|
||||
|
||||
@@ -1,25 +1,37 @@
|
||||
local Container = require("elements/Container")
|
||||
local elementManager = require("elementManager")
|
||||
local Container = elementManager.getElement("Container")
|
||||
local Render = require("render")
|
||||
|
||||
---@class BaseFrame : Container
|
||||
local BaseFrame = setmetatable({}, Container)
|
||||
BaseFrame.__index = BaseFrame
|
||||
|
||||
---@diagnostic disable-next-line: duplicate-set-field
|
||||
function BaseFrame.new(id, basalt)
|
||||
local self = setmetatable({}, BaseFrame):__init()
|
||||
self:init(id, basalt)
|
||||
self.terminal = term.current() -- change to :setTerm later!!
|
||||
self._render = Render.new(self.terminal)
|
||||
---@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 = self.terminal.getSize()
|
||||
local width, height = value.getSize()
|
||||
self.set("width", width)
|
||||
self.set("height", height)
|
||||
return value
|
||||
end})
|
||||
|
||||
function BaseFrame.new(props, basalt)
|
||||
local self = setmetatable({}, BaseFrame):__init()
|
||||
self:init(props, basalt)
|
||||
self.set("term", term.current())
|
||||
self.set("background", colors.red)
|
||||
self.set("type", "BaseFrame")
|
||||
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
|
||||
@@ -28,12 +40,18 @@ function BaseFrame:textFg(x, y, text, fg)
|
||||
self._render:textFg(x, y, text, fg)
|
||||
end
|
||||
|
||||
---@diagnostic disable-next-line: duplicate-set-field
|
||||
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
|
||||
Container.render(self)
|
||||
self._render:render()
|
||||
self._renderUpdate = false
|
||||
if self._render ~= nil then
|
||||
Container.render(self)
|
||||
self._render:render()
|
||||
self._renderUpdate = false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
local VisualElement = require("elements/VisualElement")
|
||||
local elementManager = require("elementManager")
|
||||
local VisualElement = elementManager.getElement("VisualElement")
|
||||
local getCenteredPosition = require("libraries/utils").getCenteredPosition
|
||||
|
||||
---@class Button : VisualElement
|
||||
@@ -6,23 +7,25 @@ local Button = setmetatable({}, VisualElement)
|
||||
Button.__index = Button
|
||||
|
||||
---@property text string Button Button text
|
||||
Button.defineProperty(Button, "text", {default = "Button", type = "string"})
|
||||
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")
|
||||
|
||||
---@diagnostic disable-next-line: duplicate-set-field
|
||||
function Button.new(id, basalt)
|
||||
function Button.new(props, basalt)
|
||||
local self = setmetatable({}, Button):__init()
|
||||
self:init(id, basalt)
|
||||
self.set("type", "Button")
|
||||
self:init(props, basalt)
|
||||
self.set("width", 10)
|
||||
self.set("height", 3)
|
||||
self.set("z", 5)
|
||||
return self
|
||||
end
|
||||
|
||||
---@diagnostic disable-next-line: duplicate-set-field
|
||||
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")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
local VisualElement = require("elements/VisualElement")
|
||||
local elementManager = require("elementManager")
|
||||
local VisualElement = elementManager.getElement("VisualElement")
|
||||
local expect = require("libraries/expect")
|
||||
local split = require("libraries/utils").split
|
||||
|
||||
local max = math.max
|
||||
|
||||
@@ -9,65 +10,129 @@ 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 not capitalizedName == "BaseFrame" then
|
||||
if capitalizedName ~= "BaseFrame" then
|
||||
Container["add"..capitalizedName] = function(self, ...)
|
||||
expect(1, self, "table")
|
||||
local element = self.basalt.create(k, ...)
|
||||
self.basalt.LOGGER.debug(capitalizedName.." created with ID: " .. element.id)
|
||||
self:addChild(element)
|
||||
return element
|
||||
end
|
||||
--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
|
||||
|
||||
---@diagnostic disable-next-line: duplicate-set-field
|
||||
function Container.new(id, basalt)
|
||||
function Container.new(props, basalt)
|
||||
local self = setmetatable({}, Container):__init()
|
||||
self:init(id, basalt)
|
||||
self.set("type", "Container")
|
||||
self:init(props, basalt)
|
||||
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
|
||||
|
||||
local childZ = child.get("z")
|
||||
local pos = 1
|
||||
for i, existing in ipairs(self._values.children) do
|
||||
if existing.get("z") > childZ then
|
||||
break
|
||||
end
|
||||
pos = i + 1
|
||||
end
|
||||
|
||||
table.insert(self._values.children, pos, child)
|
||||
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) 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:sortChildren()
|
||||
table.sort(self._values.children, function(a, b)
|
||||
return a.get("z") < b.get("z")
|
||||
end)
|
||||
self.set("visibleChildren", sortAndFilterChildren(self, self._values.children))
|
||||
self.set("childrenSorted", true)
|
||||
end
|
||||
|
||||
function Container:sortChildrenEvents(eventName)
|
||||
if self._values.childrenEvents[eventName] then
|
||||
table.sort(self._values.childrenEvents[eventName], function(a, b)
|
||||
return a.get("z") > b.get("z")
|
||||
end)
|
||||
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
|
||||
@@ -89,20 +154,13 @@ function Container:registerChildEvent(child, eventName)
|
||||
end
|
||||
end
|
||||
|
||||
local childZ = child.get("z")
|
||||
local pos = 1
|
||||
for i, existing in ipairs(self._values.childrenEvents[eventName]) do
|
||||
if existing.get("z") < childZ then
|
||||
break
|
||||
end
|
||||
pos = i + 1
|
||||
end
|
||||
|
||||
table.insert(self._values.childrenEvents[eventName], pos, child)
|
||||
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
|
||||
@@ -130,45 +188,98 @@ function Container:unregisterChildEvent(child, eventName)
|
||||
end
|
||||
|
||||
function Container:removeChild(child)
|
||||
for i,v in ipairs(self.children) do
|
||||
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, ...)
|
||||
if(VisualElement.handleEvent(self, event, ...))then
|
||||
local args = {...}
|
||||
if event:find("mouse_") then
|
||||
local button, absX, absY = ...
|
||||
local relX, relY = self:getRelativePosition(absX, absY)
|
||||
args = {button, relX, relY}
|
||||
end
|
||||
if self._values.childrenEvents[event] then
|
||||
for _, child in ipairs(self._values.childrenEvents[event]) do
|
||||
if(child:handleEvent(event, table.unpack(args)))then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
local args = convertMousePosition(self, event, ...)
|
||||
return callChildrenEvents(self, false, event, table.unpack(args))
|
||||
end
|
||||
end
|
||||
|
||||
--[[function Container:mouse_click(button, x, y)
|
||||
function Container:mouse_click(button, x, y)
|
||||
if VisualElement.mouse_click(self, button, x, y) then
|
||||
if self._values.childrenEvents.mouse_click then
|
||||
for _, child in ipairs(self._values.childrenEvents.mouse_click) do
|
||||
if child:mouse_click(button, x, y) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
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]]
|
||||
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")
|
||||
@@ -194,10 +305,17 @@ function Container:textFg(x, y, text, fg)
|
||||
VisualElement.textFg(self, math.max(1, x), math.max(1, y), text:sub(textStart, textStart + textLen - 1), fg)
|
||||
end
|
||||
|
||||
---@diagnostic disable-next-line: duplicate-set-field
|
||||
function Container:render()
|
||||
VisualElement.render(self)
|
||||
for _, child in ipairs(self._values.children) do
|
||||
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
|
||||
|
||||
297
src/elements/Flexbox.lua
Normal file
297
src/elements/Flexbox.lua
Normal file
@@ -0,0 +1,297 @@
|
||||
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
|
||||
--- @param props table The properties to initialize the element with
|
||||
--- @param basalt table The basalt instance
|
||||
--- @return Flexbox object The newly created Flexbox instance
|
||||
--- @usage local element = Flexbox.new("myId", basalt)
|
||||
function Flexbox.new(props, basalt)
|
||||
local self = setmetatable({}, Flexbox):__init()
|
||||
self:init(props, basalt)
|
||||
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
|
||||
@@ -1,18 +1,28 @@
|
||||
local Container = require("elements/Container")
|
||||
local elementManager = require("elementManager")
|
||||
local Container = elementManager.getElement("Container")
|
||||
|
||||
---@class Frame : Container
|
||||
local Frame = setmetatable({}, Container)
|
||||
Frame.__index = Frame
|
||||
|
||||
function Frame.new(id, basalt)
|
||||
--- Creates a new Frame instance
|
||||
--- @param props table The properties to initialize the element with
|
||||
--- @param basalt table The basalt instance
|
||||
--- @return Frame object The newly created Frame instance
|
||||
--- @usage local element = Frame.new("myId", basalt)
|
||||
function Frame.new(props, basalt)
|
||||
local self = setmetatable({}, Frame):__init()
|
||||
self:init(id, basalt)
|
||||
self:init(props, basalt)
|
||||
self.set("width", 12)
|
||||
self.set("height", 6)
|
||||
self.set("background", colors.blue)
|
||||
self.set("type", "Frame")
|
||||
self.set("z", 10)
|
||||
return self
|
||||
end
|
||||
|
||||
function Frame:init(props, basalt)
|
||||
Container.init(self, props, basalt)
|
||||
self.set("type", "Frame")
|
||||
end
|
||||
|
||||
return Frame
|
||||
111
src/elements/Input.lua
Normal file
111
src/elements/Input.lua
Normal file
@@ -0,0 +1,111 @@
|
||||
local VisualElement = require("elements/VisualElement")
|
||||
|
||||
---@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 für Text-Viewport
|
||||
Input.defineProperty(Input, "viewOffset", {default = 0, type = "number"})
|
||||
|
||||
Input.listenTo(Input, "mouse_click")
|
||||
Input.listenTo(Input, "key")
|
||||
Input.listenTo(Input, "char")
|
||||
|
||||
--- Creates a new Input instance
|
||||
--- @param id string The unique identifier for this element
|
||||
--- @param basalt table The basalt instance
|
||||
--- @return Input object The newly created Input instance
|
||||
--- @usage local element = Input.new("myId", basalt)
|
||||
function Input.new(id, basalt)
|
||||
local self = setmetatable({}, Input):__init()
|
||||
self:init(id, basalt)
|
||||
self.set("width", 8)
|
||||
self.set("z", 3)
|
||||
return self
|
||||
end
|
||||
|
||||
function Input:init(id, basalt)
|
||||
VisualElement.init(self, id, 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")
|
||||
self.set("text", text:sub(1, pos-1) .. char .. text:sub(pos))
|
||||
self.set("cursorPos", pos + 1)
|
||||
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")
|
||||
|
||||
if key == keys.left and pos > 1 then
|
||||
self.set("cursorPos", pos - 1)
|
||||
self:setCursor(pos - 1,1, true)
|
||||
elseif key == keys.right and pos <= #text then
|
||||
self.set("cursorPos", pos + 1)
|
||||
self:setCursor(pos + 1,1, true)
|
||||
elseif key == keys.backspace and pos > 1 then
|
||||
self.set("text", text:sub(1, pos-2) .. text:sub(pos))
|
||||
self.set("cursorPos", pos - 1)
|
||||
self:setCursor(pos - 1,1, true)
|
||||
end
|
||||
self:updateViewport()
|
||||
end
|
||||
|
||||
function Input:focus()
|
||||
VisualElement.focus(self)
|
||||
self.set("background", colors.blue)
|
||||
self:setCursor(1,1, true)
|
||||
end
|
||||
|
||||
function Input:blur()
|
||||
VisualElement.blur(self)
|
||||
self.set("background", colors.green)
|
||||
end
|
||||
|
||||
function Input:updateViewport()
|
||||
local width = self.get("width")
|
||||
local text = self.get("text")
|
||||
local cursorPos = self.get("cursorPos")
|
||||
local viewOffset = self.get("viewOffset")
|
||||
|
||||
-- Wenn Cursor außerhalb des sichtbaren Bereichs nach rechts
|
||||
if cursorPos - viewOffset > width then
|
||||
self.set("viewOffset", cursorPos - width)
|
||||
end
|
||||
|
||||
-- Wenn Cursor außerhalb des sichtbaren Bereichs nach links
|
||||
if cursorPos <= viewOffset then
|
||||
self.set("viewOffset", cursorPos - 1)
|
||||
end
|
||||
end
|
||||
|
||||
function Input:render()
|
||||
VisualElement.render(self)
|
||||
local text = self.get("text")
|
||||
local viewOffset = self.get("viewOffset")
|
||||
local width = self.get("width")
|
||||
|
||||
-- Nur den sichtbaren Teil des Textes rendern
|
||||
local visibleText = text:sub(viewOffset + 1, viewOffset + width)
|
||||
self:textFg(1, 1, visibleText, self.get("foreground"))
|
||||
|
||||
if self.get("focused") then
|
||||
local cursorPos = self.get("cursorPos")
|
||||
-- Cursor relativ zum Viewport positionieren
|
||||
self:setCursor(cursorPos - viewOffset, 1, true)
|
||||
end
|
||||
end
|
||||
|
||||
return Input
|
||||
38
src/elements/Label.lua
Normal file
38
src/elements/Label.lua
Normal file
@@ -0,0 +1,38 @@
|
||||
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)
|
||||
self.set("width", #value)
|
||||
return value
|
||||
end})
|
||||
|
||||
--- Creates a new Label instance
|
||||
--- @param name table The properties to initialize the element with
|
||||
--- @param basalt table The basalt instance
|
||||
--- @return Label object The newly created Label instance
|
||||
--- @usage local element = Label.new("myId", basalt)
|
||||
function Label.new(props, basalt)
|
||||
local self = setmetatable({}, Label):__init()
|
||||
self:init(props, basalt)
|
||||
self.set("z", 3)
|
||||
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
|
||||
@@ -1,4 +1,5 @@
|
||||
local BaseElement = require("elements/BaseElement")
|
||||
local elementManager = require("elementManager")
|
||||
local BaseElement = elementManager.getElement("BaseElement")
|
||||
|
||||
---@alias color number
|
||||
|
||||
@@ -8,40 +9,70 @@ VisualElement.__index = VisualElement
|
||||
local tHex = require("libraries/colorHex")
|
||||
|
||||
---@property x number 1 x position of the element
|
||||
BaseElement.defineProperty(VisualElement, "x", {default = 1, type = "number", canTriggerRender = true})
|
||||
VisualElement.defineProperty(VisualElement, "x", {default = 1, type = "number", canTriggerRender = true})
|
||||
---@property y number 1 y position of the element
|
||||
BaseElement.defineProperty(VisualElement, "y", {default = 1, type = "number", canTriggerRender = true})
|
||||
VisualElement.defineProperty(VisualElement, "y", {default = 1, type = "number", canTriggerRender = true})
|
||||
---@property z number 1 z position of the element
|
||||
BaseElement.defineProperty(VisualElement, "z", {default = 1, type = "number", canTriggerRender = true, setter = function(self, value)
|
||||
self.basalt.LOGGER.debug("Setting z to " .. value)
|
||||
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
|
||||
BaseElement.defineProperty(VisualElement, "width", {default = 1, type = "number", canTriggerRender = true})
|
||||
VisualElement.defineProperty(VisualElement, "width", {default = 1, type = "number", canTriggerRender = true})
|
||||
---@property height number 1 height of the element
|
||||
BaseElement.defineProperty(VisualElement, "height", {default = 1, type = "number", canTriggerRender = true})
|
||||
VisualElement.defineProperty(VisualElement, "height", {default = 1, type = "number", canTriggerRender = true})
|
||||
---@property background color black background color of the element
|
||||
BaseElement.defineProperty(VisualElement, "background", {default = colors.black, type = "number", canTriggerRender = true})
|
||||
VisualElement.defineProperty(VisualElement, "background", {default = colors.black, type = "number", canTriggerRender = true})
|
||||
---@property foreground color white foreground color of the element
|
||||
BaseElement.defineProperty(VisualElement, "foreground", {default = colors.white, type = "number", canTriggerRender = true})
|
||||
---@property clicked boolean false element is currently clicked
|
||||
BaseElement.defineProperty(VisualElement, "clicked", {default = false, type = "boolean"})
|
||||
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.listenTo(VisualElement, "focus")
|
||||
VisualElement.listenTo(VisualElement, "blur")
|
||||
|
||||
--- Creates a new VisualElement instance
|
||||
--- @param id string The unique identifier for this element
|
||||
--- @param props table The properties to initialize the element with
|
||||
--- @param basalt table The basalt instance
|
||||
--- @return VisualElement object The newly created VisualElement instance
|
||||
--- @usage local element = VisualElement.new("myId", basalt)
|
||||
function VisualElement.new(id, basalt)
|
||||
function VisualElement.new(props, basalt)
|
||||
local self = setmetatable({}, VisualElement):__init()
|
||||
self:init(id, basalt)
|
||||
self:init(props, basalt)
|
||||
self.set("type", "VisualElement")
|
||||
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
|
||||
@@ -87,9 +118,10 @@ end
|
||||
function VisualElement:mouse_click(button, x, y)
|
||||
if self:isInBounds(x, y) then
|
||||
self.set("clicked", true)
|
||||
self:fireEvent("mouse_click", button, x, y)
|
||||
self:fireEvent("mouse_click", button, self:getRelativePosition(x, y))
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function VisualElement:mouse_up(button, x, y)
|
||||
@@ -98,40 +130,43 @@ function VisualElement:mouse_up(button, x, y)
|
||||
self:fireEvent("mouse_up", button, x, y)
|
||||
return true
|
||||
end
|
||||
self:fireEvent("mouse_release", button, x, y)
|
||||
self:fireEvent("mouse_release", button, self:getRelativePosition(x, y))
|
||||
end
|
||||
|
||||
function VisualElement:mouse_release()
|
||||
self.set("clicked", false)
|
||||
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 VisualElement:handleEvent(event, ...)
|
||||
if(self[event])then
|
||||
return self[event](self, ...)
|
||||
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)
|
||||
if (x == nil) or (y == nil) then
|
||||
x, y = self.get("x"), self.get("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")
|
||||
x = x + px - 1
|
||||
y = y + py - 1
|
||||
xPos = xPos + px - 1
|
||||
yPos = yPos + py - 1
|
||||
parent = parent.parent
|
||||
end
|
||||
|
||||
return x, y
|
||||
return xPos, yPos
|
||||
end
|
||||
|
||||
--- Returns the relative position of the element or the given coordinates.
|
||||
@@ -155,10 +190,19 @@ function VisualElement:getRelativePosition(x, y)
|
||||
y - (elementY - 1) - (parentY - 1)
|
||||
end
|
||||
|
||||
function VisualElement:setCursor(x, y, blink)
|
||||
if self.parent then
|
||||
local absX, absY = self:getAbsolutePosition(x, y)
|
||||
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
|
||||
|
||||
@@ -95,7 +95,7 @@ function errorHandler.error(errMsg)
|
||||
file.close()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
term.setBackgroundColor(colors.black)
|
||||
LOGGER.error(errMsg)
|
||||
error()
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
|
||||
local args = {...}
|
||||
|
||||
local basaltPath = args[1] or "basalt"
|
||||
local basaltPath = fs.getDir(args[2])
|
||||
|
||||
local defaultPath = package.path
|
||||
local format = "path;/path/?.lua;/path/?/init.lua;"
|
||||
@@ -18,6 +16,7 @@ end
|
||||
-- Use xpcall with error handler
|
||||
local ok, result = pcall(require, "main")
|
||||
|
||||
package.path = defaultPath
|
||||
if not ok then
|
||||
errorHandler(result)
|
||||
else
|
||||
|
||||
@@ -24,4 +24,32 @@ function utils.deepCopy(obj)
|
||||
return copy
|
||||
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
|
||||
78
src/main.lua
78
src/main.lua
@@ -1,5 +1,9 @@
|
||||
local benchmark = require("benchmark")
|
||||
benchmark.start("Basalt Initialization")
|
||||
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.
|
||||
---
|
||||
@@ -13,24 +17,65 @@ 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 id? string Optional unique identifier for the element
|
||||
--- @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, id)
|
||||
if(id==nil)then id = elementManager.generateId() end
|
||||
local element = elementManager.getElement(type).new(id, basalt)
|
||||
local ok, result = pcall(require, "main")
|
||||
if not ok then
|
||||
errorManager(false, result)
|
||||
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
|
||||
return elementClass.new(properties, basalt)
|
||||
end
|
||||
return element
|
||||
end
|
||||
|
||||
--- Creates and returns a new frame
|
||||
@@ -87,18 +132,11 @@ end
|
||||
--- @local Internal event handler
|
||||
local function updateEvent(event, ...)
|
||||
if(event=="terminate")then basalt.stop() end
|
||||
if lazyElementsEventHandler(event, ...) then return end
|
||||
|
||||
if event:find("mouse") then
|
||||
if mainFrame then
|
||||
mainFrame:handleEvent(event, ...)
|
||||
end
|
||||
end
|
||||
|
||||
if event:find("key") then
|
||||
if mainFrame then
|
||||
mainFrame:handleEvent(event, ...)
|
||||
end
|
||||
end
|
||||
if(mainFrame:dispatchEvent(event, ...))then
|
||||
return
|
||||
end
|
||||
|
||||
if basalt._events[event] then
|
||||
for _, callback in ipairs(basalt._events[event]) do
|
||||
@@ -137,12 +175,14 @@ end
|
||||
--- @usage basalt.run()
|
||||
--- @usage basalt.run(false)
|
||||
function basalt.run(isActive)
|
||||
benchmark.stop("Basalt Initialization")
|
||||
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
|
||||
|
||||
327
src/plugins/animation.lua
Normal file
327
src/plugins/animation.lua
Normal file
@@ -0,0 +1,327 @@
|
||||
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)
|
||||
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
|
||||
}
|
||||
23
src/plugins/pluginTemplate.lua
Normal file
23
src/plugins/pluginTemplate.lua
Normal file
@@ -0,0 +1,23 @@
|
||||
-- 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
|
||||
}
|
||||
|
||||
33
src/plugins/reactive.lua
Normal file
33
src/plugins/reactive.lua
Normal file
@@ -0,0 +1,33 @@
|
||||
local function setupReactiveProperty(element, propertyName, expression)
|
||||
|
||||
end
|
||||
|
||||
local function createReactiveFunction(expression, scope)
|
||||
local code = expression:gsub(
|
||||
"(%w+)%s*%?%s*([^:]+)%s*:%s*([^}]+)",
|
||||
"%1 and %2 or %3"
|
||||
)
|
||||
|
||||
return load(string.format([[
|
||||
return function(self)
|
||||
return %s
|
||||
end
|
||||
]], code), "reactive", "t", scope)()
|
||||
end
|
||||
|
||||
local BaseElement = {}
|
||||
|
||||
function BaseElement:setReactiveProperty(propertyName, expression)
|
||||
setupReactiveProperty(self, propertyName, expression)
|
||||
return self
|
||||
end
|
||||
|
||||
function BaseElement:setReactive(propertyName, expression)
|
||||
local reactiveFunc = createReactiveFunction(expression, self)
|
||||
self.set(propertyName, reactiveFunc)
|
||||
return self
|
||||
end
|
||||
|
||||
return {
|
||||
BaseElement = BaseElement
|
||||
}
|
||||
184
src/plugins/xml.lua
Normal file
184
src/plugins/xml.lua
Normal file
@@ -0,0 +1,184 @@
|
||||
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("^<!%[CDATA%[") then
|
||||
inCDATA = true
|
||||
cdataContent = ""
|
||||
elseif 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("^</") then
|
||||
current = table.remove(stack)
|
||||
end
|
||||
end
|
||||
return root
|
||||
end
|
||||
|
||||
local function evaluateExpression(expr, scope)
|
||||
if not expr:match("^%${.*}$") then
|
||||
return expr:gsub("%${(.-)}", function(e)
|
||||
local env = setmetatable({}, {__index = function(_, k)
|
||||
return scope and scope[k] or _ENV[k]
|
||||
end})
|
||||
|
||||
local func, err = load("return " .. e, "expression", "t", env)
|
||||
if not func then
|
||||
errorManager.error("Failed to parse expression: " .. err)
|
||||
end
|
||||
return tostring(func())
|
||||
end)
|
||||
end
|
||||
|
||||
expr = expr:match("^%${(.*)}$")
|
||||
local env = setmetatable({}, {__index = function(_, k)
|
||||
return scope and scope[k] or _ENV[k]
|
||||
end})
|
||||
|
||||
local func, err = load("return " .. expr, "expression", "t", env)
|
||||
if not func then
|
||||
errorManager.error("Failed to parse expression: " .. err)
|
||||
end
|
||||
return func()
|
||||
end
|
||||
|
||||
local function convertValue(value, propertyType, scope)
|
||||
if propertyType == "string" and type(value) == "string" then
|
||||
if value:find("${") then
|
||||
return evaluateExpression(value, scope)
|
||||
end
|
||||
end
|
||||
|
||||
if type(value) == "string" and value:match("^%${.*}$") then
|
||||
return evaluateExpression(value, scope)
|
||||
end
|
||||
|
||||
if propertyType == "number" then
|
||||
return tonumber(value)
|
||||
elseif propertyType == "boolean" then
|
||||
return value == "true"
|
||||
elseif propertyType == "color" then
|
||||
return colors[value]
|
||||
elseif propertyType == "table" then
|
||||
local env = setmetatable({}, { __index = _ENV })
|
||||
local func = load("return "..value, nil, "t", env)
|
||||
if func then
|
||||
return func()
|
||||
end
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
local function handleEvent(node, element, scope)
|
||||
for attr, value in pairs(node.attributes) do
|
||||
if attr:match("^on%u") then
|
||||
local eventName = attr:sub(3,3):lower() .. attr:sub(4)
|
||||
if scope[value] then
|
||||
element["on"..eventName:sub(1,1):upper()..eventName:sub(2)](element, scope[value])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for _, child in ipairs(node.children or {}) do
|
||||
if child.name and child.name:match("^on%u") then
|
||||
local eventName = child.name:sub(3,3):lower() .. child.name:sub(4)
|
||||
|
||||
if child.content then
|
||||
local code = child.content:gsub("^%s+", ""):gsub("%s+$", "")
|
||||
|
||||
local func, err = load(string.format([[
|
||||
return %s
|
||||
]], code), "event", "t", scope)
|
||||
|
||||
if err then
|
||||
errorManager.error("Failed to parse event: " .. err)
|
||||
elseif func then
|
||||
element["on"..eventName:sub(1,1):upper()..eventName:sub(2)](element, func())
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local BaseElement = {}
|
||||
|
||||
function BaseElement:fromXML(node)
|
||||
for attr, value in pairs(node.attributes) do
|
||||
local config = self:getPropertyConfig(attr)
|
||||
if config then
|
||||
local convertedValue = convertValue(value, config.type)
|
||||
self.set(attr, convertedValue)
|
||||
end
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
local Container = {}
|
||||
|
||||
function Container:loadXML(content, scope)
|
||||
local tree = parseXML(self, content)
|
||||
|
||||
local function createElements(nodes, parent, scope)
|
||||
for _, node in ipairs(nodes.children) do
|
||||
if not node.name:match("^on") then
|
||||
local elementType = node.name:sub(1,1):upper() .. node.name:sub(2)
|
||||
local element = parent["add"..elementType](parent, node.attributes.name)
|
||||
|
||||
for attr, value in pairs(node.attributes) do
|
||||
local config = element:getPropertyConfig(attr)
|
||||
if config then
|
||||
local convertedValue = convertValue(value, config.type, scope)
|
||||
element.set(attr, convertedValue)
|
||||
end
|
||||
end
|
||||
|
||||
handleEvent(node, element, scope)
|
||||
|
||||
if #node.children > 0 then
|
||||
createElements(node, element, scope)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
createElements(tree, self, scope)
|
||||
return self
|
||||
end
|
||||
|
||||
return {
|
||||
BaseElement = BaseElement,
|
||||
Container = Container
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
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 = {}
|
||||
|
||||
function PropertySystem.defineProperty(class, name, config)
|
||||
if not rawget(class, '_properties') then
|
||||
@@ -22,43 +25,162 @@ function PropertySystem.defineProperty(class, name, config)
|
||||
|
||||
local capitalizedName = name:sub(1,1):upper() .. name:sub(2)
|
||||
|
||||
class["get" .. capitalizedName] = function(self)
|
||||
class["get" .. capitalizedName] = function(self, ...)
|
||||
expect(1, self, "element")
|
||||
local value = self._values[name]
|
||||
return config.getter and config.getter(value) or value
|
||||
return config.getter and config.getter(self, value, ...) or value
|
||||
end
|
||||
|
||||
class["set" .. capitalizedName] = function(self, value)
|
||||
class["set" .. capitalizedName] = function(self, value, ...)
|
||||
expect(1, self, "element")
|
||||
expect(2, value, config.type)
|
||||
if config.setter then
|
||||
value = config.setter(self, value)
|
||||
value = config.setter(self, value, ...)
|
||||
end
|
||||
|
||||
self:_updateProperty(name, value)
|
||||
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({}, 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)
|
||||
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)
|
||||
return blueprint._values[name]
|
||||
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)
|
||||
self.set = function(name, value, ...)
|
||||
local oldValue = self._values[name]
|
||||
self._values[name] = value
|
||||
if(self._properties[name].setter) then
|
||||
value = self._properties[name].setter(self, value)
|
||||
end
|
||||
if oldValue ~= value and self._observers[name] then
|
||||
for _, callback in ipairs(self._observers[name]) do
|
||||
callback(self, value, oldValue)
|
||||
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] = value
|
||||
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)
|
||||
return self._values[name]
|
||||
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
|
||||
return config.getter and config.getter(self, value, ...) or value
|
||||
end
|
||||
|
||||
local properties = {}
|
||||
@@ -139,4 +261,25 @@ function PropertySystem:observe(name, callback)
|
||||
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
|
||||
106
src/render.lua
106
src/render.lua
@@ -1,7 +1,6 @@
|
||||
local Render = {}
|
||||
Render.__index = Render
|
||||
local colorChars = require("libraries/colorHex")
|
||||
local log = require("log")
|
||||
|
||||
function Render.new(terminal)
|
||||
local self = setmetatable({}, Render)
|
||||
@@ -12,19 +11,27 @@ function Render.new(terminal)
|
||||
text = {},
|
||||
fg = {},
|
||||
bg = {},
|
||||
changed = {}
|
||||
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)
|
||||
self.buffer.changed[y] = false
|
||||
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
|
||||
@@ -34,7 +41,7 @@ function Render:blit(x, y, text, fg, bg)
|
||||
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.buffer.changed[y] = true
|
||||
self:addDirtyRect(x, y, #text, 1)
|
||||
|
||||
return self
|
||||
end
|
||||
@@ -55,10 +62,10 @@ function Render:multiBlit(x, y, width, height, text, fg, bg)
|
||||
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)
|
||||
self.buffer.changed[cy] = true
|
||||
end
|
||||
end
|
||||
|
||||
self:addDirtyRect(x, y, width, height)
|
||||
return self
|
||||
end
|
||||
|
||||
@@ -68,7 +75,7 @@ function Render:textFg(x, y, text, fg)
|
||||
|
||||
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.buffer.changed[y] = true
|
||||
self:addDirtyRect(x, y, #text, 1)
|
||||
|
||||
return self
|
||||
end
|
||||
@@ -77,7 +84,7 @@ 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.buffer.changed[y] = true
|
||||
self:addDirtyRect(x, y, #text, 1)
|
||||
|
||||
return self
|
||||
end
|
||||
@@ -86,7 +93,7 @@ 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.buffer.changed[y] = true
|
||||
self:addDirtyRect(x, y, #fg, 1)
|
||||
|
||||
return self
|
||||
end
|
||||
@@ -95,7 +102,7 @@ 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.buffer.changed[y] = true
|
||||
self:addDirtyRect(x, y, #bg, 1)
|
||||
|
||||
return self
|
||||
end
|
||||
@@ -106,23 +113,84 @@ function Render:clear(bg)
|
||||
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.buffer.changed[y] = true
|
||||
self:addDirtyRect(1, y, self.width, 1)
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
function Render:render()
|
||||
for y=1, self.height do
|
||||
if self.buffer.changed[y] then
|
||||
self.terminal.setCursorPos(1, y)
|
||||
self.terminal.blit(
|
||||
self.buffer.text[y],
|
||||
self.buffer.fg[y],
|
||||
self.buffer.bg[y]
|
||||
)
|
||||
self.buffer.changed[y] = false
|
||||
local benchmark = require("benchmark")
|
||||
benchmark.start("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
|
||||
|
||||
-- Nur die Dirty Rectangles rendern
|
||||
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
|
||||
|
||||
benchmark.update("render")
|
||||
self.buffer.dirtyRects = {}
|
||||
|
||||
if self.blink then
|
||||
self.terminal.setCursorPos(self.xCursor, self.yCursor)
|
||||
self.terminal.setCursorBlink(true)
|
||||
else
|
||||
self.terminal.setCursorBlink(false)
|
||||
end
|
||||
|
||||
--benchmark.stop("render")
|
||||
return self
|
||||
end
|
||||
|
||||
-- Hilfsfunktionen für Rectangle-Management
|
||||
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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user