- 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:
3
.gitignore
vendored
3
.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 args = table.pack(...)
|
||||||
local dir = fs.getDir(args[2] or "basalt")
|
local dir = fs.getDir(args[2] or "basalt")
|
||||||
|
local subDir = args[1]
|
||||||
if(dir==nil)then
|
if(dir==nil)then
|
||||||
error("Unable to find directory "..args[2].." please report this bug to our discord.")
|
error("Unable to find directory "..args[2].." please report this bug to our discord.")
|
||||||
end
|
end
|
||||||
|
|
||||||
local log = require("log")
|
local log = require("log")
|
||||||
|
local defaultPath = package.path
|
||||||
|
local format = "path;/path/?.lua;/path/?/init.lua;"
|
||||||
|
local main = format:gsub("path", dir)
|
||||||
|
|
||||||
local ElementManager = {}
|
local ElementManager = {}
|
||||||
ElementManager._elements = {}
|
ElementManager._elements = {}
|
||||||
@@ -27,55 +31,19 @@ if fs.exists(elementsDirectory) then
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function ElementManager.extendMethod(element, methodName, newMethod, originalMethod)
|
log.info("Loading plugins from "..pluginsDirectory)
|
||||||
if not originalMethod then
|
if fs.exists(pluginsDirectory) then
|
||||||
element[methodName] = newMethod
|
for _, file in ipairs(fs.list(pluginsDirectory)) do
|
||||||
return
|
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
|
end
|
||||||
element[methodName] = function(self, ...)
|
table.insert(ElementManager._plugins[k], v)
|
||||||
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]
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -84,7 +52,9 @@ end
|
|||||||
|
|
||||||
function ElementManager.loadElement(name)
|
function ElementManager.loadElement(name)
|
||||||
if not ElementManager._elements[name].loaded then
|
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] = {
|
ElementManager._elements[name] = {
|
||||||
class = element,
|
class = element,
|
||||||
plugins = element.plugins,
|
plugins = element.plugins,
|
||||||
@@ -92,20 +62,44 @@ function ElementManager.loadElement(name)
|
|||||||
}
|
}
|
||||||
log.debug("Loaded element: "..name)
|
log.debug("Loaded element: "..name)
|
||||||
|
|
||||||
-- Load element's required plugins
|
if(ElementManager._plugins[name]~=nil)then
|
||||||
if element.requires then
|
for _, plugin in pairs(ElementManager._plugins[name]) do
|
||||||
for pluginName, _ in pairs(element.requires) do
|
if(plugin.setup)then
|
||||||
--ElementManager.loadPlugin(pluginName)
|
plugin.setup(element)
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function ElementManager.registerPlugin(name, plugin)
|
if(plugin.hooks)then
|
||||||
if not plugin.provides then
|
for methodName, hooks in pairs(plugin.hooks) do
|
||||||
error("Plugin must specify what it provides")
|
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
|
||||||
ElementManager._plugins[name] = plugin
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function ElementManager.getElement(name)
|
function ElementManager.getElement(name)
|
||||||
@@ -119,11 +113,4 @@ function ElementManager.getElementList()
|
|||||||
return ElementManager._elements
|
return ElementManager._elements
|
||||||
end
|
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
|
return ElementManager
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
local PropertySystem = require("propertySystem")
|
local PropertySystem = require("propertySystem")
|
||||||
|
local uuid = require("/libraries/utils").uuid
|
||||||
|
|
||||||
--- The base class for all UI elements in Basalt
|
--- The base class for all UI elements in Basalt
|
||||||
--- @class BaseElement : PropertySystem
|
--- @class BaseElement : PropertySystem
|
||||||
@@ -7,45 +8,28 @@ BaseElement.__index = BaseElement
|
|||||||
BaseElement._events = {}
|
BaseElement._events = {}
|
||||||
|
|
||||||
--- @property type string BaseElement The type identifier of the element
|
--- @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
|
--- @property eventCallbacks table {} Table containing all registered event callbacks
|
||||||
BaseElement.defineProperty(BaseElement, "eventCallbacks", {default = {}, type = "table"})
|
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
|
--- Registers an event that this class can listen to
|
||||||
--- @param class table The class to add the event to
|
--- @param class table The class to add the event to
|
||||||
--- @param eventName string The name of the event to register
|
--- @param eventName string The name of the event to register
|
||||||
@@ -57,6 +41,57 @@ function BaseElement.listenTo(class, eventName)
|
|||||||
class._events[eventName] = true
|
class._events[eventName] = true
|
||||||
end
|
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
|
--- Enables or disables event listening for a specific event
|
||||||
--- @param eventName string The name of the event to listen for
|
--- @param eventName string The name of the event to listen for
|
||||||
--- @param enable? boolean Whether to enable or disable the event (default: true)
|
--- @param enable? boolean Whether to enable or disable the event (default: true)
|
||||||
@@ -113,6 +148,25 @@ function BaseElement:fireEvent(event, ...)
|
|||||||
return self
|
return self
|
||||||
end
|
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
|
--- Requests a render update for this element
|
||||||
--- @usage element:updateRender()
|
--- @usage element:updateRender()
|
||||||
function BaseElement: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")
|
local Render = require("render")
|
||||||
|
|
||||||
---@class BaseFrame : Container
|
---@class BaseFrame : Container
|
||||||
local BaseFrame = setmetatable({}, Container)
|
local BaseFrame = setmetatable({}, Container)
|
||||||
BaseFrame.__index = BaseFrame
|
BaseFrame.__index = BaseFrame
|
||||||
|
|
||||||
---@diagnostic disable-next-line: duplicate-set-field
|
---@property text term term nil text
|
||||||
function BaseFrame.new(id, basalt)
|
BaseFrame.defineProperty(BaseFrame, "term", {default = nil, type = "table", setter = function(self, value)
|
||||||
local self = setmetatable({}, BaseFrame):__init()
|
if value == nil or value.setCursorPos == nil then
|
||||||
self:init(id, basalt)
|
return value
|
||||||
self.terminal = term.current() -- change to :setTerm later!!
|
end
|
||||||
self._render = Render.new(self.terminal)
|
self._render = Render.new(value)
|
||||||
self._renderUpdate = true
|
self._renderUpdate = true
|
||||||
local width, height = self.terminal.getSize()
|
local width, height = value.getSize()
|
||||||
self.set("width", width)
|
self.set("width", width)
|
||||||
self.set("height", height)
|
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("background", colors.red)
|
||||||
self.set("type", "BaseFrame")
|
|
||||||
return self
|
return self
|
||||||
end
|
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)
|
function BaseFrame:multiBlit(x, y, width, height, text, fg, bg)
|
||||||
self._render:multiBlit(x, y, width, height, text, fg, bg)
|
self._render:multiBlit(x, y, width, height, text, fg, bg)
|
||||||
end
|
end
|
||||||
@@ -28,13 +40,19 @@ function BaseFrame:textFg(x, y, text, fg)
|
|||||||
self._render:textFg(x, y, text, fg)
|
self._render:textFg(x, y, text, fg)
|
||||||
end
|
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()
|
function BaseFrame:render()
|
||||||
if(self._renderUpdate) then
|
if(self._renderUpdate) then
|
||||||
|
if self._render ~= nil then
|
||||||
Container.render(self)
|
Container.render(self)
|
||||||
self._render:render()
|
self._render:render()
|
||||||
self._renderUpdate = false
|
self._renderUpdate = false
|
||||||
end
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return BaseFrame
|
return BaseFrame
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
local VisualElement = require("elements/VisualElement")
|
local elementManager = require("elementManager")
|
||||||
|
local VisualElement = elementManager.getElement("VisualElement")
|
||||||
local getCenteredPosition = require("libraries/utils").getCenteredPosition
|
local getCenteredPosition = require("libraries/utils").getCenteredPosition
|
||||||
|
|
||||||
---@class Button : VisualElement
|
---@class Button : VisualElement
|
||||||
@@ -6,23 +7,25 @@ local Button = setmetatable({}, VisualElement)
|
|||||||
Button.__index = Button
|
Button.__index = Button
|
||||||
|
|
||||||
---@property text string Button Button text
|
---@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
|
---@event mouse_click The event that is triggered when the button is clicked
|
||||||
Button.listenTo(Button, "mouse_click")
|
Button.listenTo(Button, "mouse_click")
|
||||||
|
|
||||||
---@diagnostic disable-next-line: duplicate-set-field
|
function Button.new(props, basalt)
|
||||||
function Button.new(id, basalt)
|
|
||||||
local self = setmetatable({}, Button):__init()
|
local self = setmetatable({}, Button):__init()
|
||||||
self:init(id, basalt)
|
self:init(props, basalt)
|
||||||
self.set("type", "Button")
|
|
||||||
self.set("width", 10)
|
self.set("width", 10)
|
||||||
self.set("height", 3)
|
self.set("height", 3)
|
||||||
self.set("z", 5)
|
self.set("z", 5)
|
||||||
return self
|
return self
|
||||||
end
|
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()
|
function Button:render()
|
||||||
VisualElement.render(self)
|
VisualElement.render(self)
|
||||||
local text = self.get("text")
|
local text = self.get("text")
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
local VisualElement = require("elements/VisualElement")
|
|
||||||
local elementManager = require("elementManager")
|
local elementManager = require("elementManager")
|
||||||
|
local VisualElement = elementManager.getElement("VisualElement")
|
||||||
local expect = require("libraries/expect")
|
local expect = require("libraries/expect")
|
||||||
|
local split = require("libraries/utils").split
|
||||||
|
|
||||||
local max = math.max
|
local max = math.max
|
||||||
|
|
||||||
@@ -9,65 +10,129 @@ local Container = setmetatable({}, VisualElement)
|
|||||||
Container.__index = Container
|
Container.__index = Container
|
||||||
|
|
||||||
Container.defineProperty(Container, "children", {default = {}, type = "table"})
|
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, "childrenEvents", {default = {}, type = "table"})
|
||||||
Container.defineProperty(Container, "eventListenerCount", {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
|
for k, _ in pairs(elementManager:getElementList()) do
|
||||||
local capitalizedName = k:sub(1,1):upper() .. k:sub(2)
|
local capitalizedName = k:sub(1,1):upper() .. k:sub(2)
|
||||||
--if not capitalizedName == "BaseFrame" then
|
if capitalizedName ~= "BaseFrame" then
|
||||||
Container["add"..capitalizedName] = function(self, ...)
|
Container["add"..capitalizedName] = function(self, ...)
|
||||||
expect(1, self, "table")
|
expect(1, self, "table")
|
||||||
local element = self.basalt.create(k, ...)
|
local element = self.basalt.create(k, ...)
|
||||||
self.basalt.LOGGER.debug(capitalizedName.." created with ID: " .. element.id)
|
|
||||||
self:addChild(element)
|
self:addChild(element)
|
||||||
return element
|
return element
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
---@diagnostic disable-next-line: duplicate-set-field
|
function Container.new(props, basalt)
|
||||||
function Container.new(id, basalt)
|
|
||||||
local self = setmetatable({}, Container):__init()
|
local self = setmetatable({}, Container):__init()
|
||||||
self:init(id, basalt)
|
self:init(props, basalt)
|
||||||
self.set("type", "Container")
|
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function Container:init(props, basalt)
|
||||||
|
VisualElement.init(self, props, basalt)
|
||||||
|
self.set("type", "Container")
|
||||||
|
end
|
||||||
|
|
||||||
function Container:addChild(child)
|
function Container:addChild(child)
|
||||||
if child == self then
|
if child == self then
|
||||||
error("Cannot add container to itself")
|
error("Cannot add container to itself")
|
||||||
end
|
end
|
||||||
|
|
||||||
local childZ = child.get("z")
|
table.insert(self._values.children, child)
|
||||||
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)
|
|
||||||
child.parent = self
|
child.parent = self
|
||||||
|
self.set("childrenSorted", false)
|
||||||
self:registerChildrenEvents(child)
|
self:registerChildrenEvents(child)
|
||||||
return self
|
return self
|
||||||
end
|
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()
|
function Container:sortChildren()
|
||||||
table.sort(self._values.children, function(a, b)
|
self.set("visibleChildren", sortAndFilterChildren(self, self._values.children))
|
||||||
return a.get("z") < b.get("z")
|
self.set("childrenSorted", true)
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function Container:sortChildrenEvents(eventName)
|
function Container:sortChildrenEvents(eventName)
|
||||||
if self._values.childrenEvents[eventName] then
|
if self._values.childrenEvents[eventName] then
|
||||||
table.sort(self._values.childrenEvents[eventName], function(a, b)
|
self._values.visibleChildrenEvents[eventName] = sortAndFilterChildren(self, self._values.childrenEvents[eventName])
|
||||||
return a.get("z") > b.get("z")
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
|
self.set("childrenEventsSorted", true)
|
||||||
end
|
end
|
||||||
|
|
||||||
function Container:registerChildrenEvents(child)
|
function Container:registerChildrenEvents(child)
|
||||||
|
if(child._registeredEvents == nil)then return end
|
||||||
for event in pairs(child._registeredEvents) do
|
for event in pairs(child._registeredEvents) do
|
||||||
self:registerChildEvent(child, event)
|
self:registerChildEvent(child, event)
|
||||||
end
|
end
|
||||||
@@ -89,20 +154,13 @@ function Container:registerChildEvent(child, eventName)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local childZ = child.get("z")
|
self.set("childrenEventsSorted", false)
|
||||||
local pos = 1
|
table.insert(self._values.childrenEvents[eventName], child)
|
||||||
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._values.eventListenerCount[eventName] = self._values.eventListenerCount[eventName] + 1
|
self._values.eventListenerCount[eventName] = self._values.eventListenerCount[eventName] + 1
|
||||||
end
|
end
|
||||||
|
|
||||||
function Container:removeChildrenEvents(child)
|
function Container:removeChildrenEvents(child)
|
||||||
|
if(child._registeredEvents == nil)then return end
|
||||||
for event in pairs(child._registeredEvents) do
|
for event in pairs(child._registeredEvents) do
|
||||||
self:unregisterChildEvent(child, event)
|
self:unregisterChildEvent(child, event)
|
||||||
end
|
end
|
||||||
@@ -130,45 +188,98 @@ function Container:unregisterChildEvent(child, eventName)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function Container:removeChild(child)
|
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
|
if v == child then
|
||||||
table.remove(self._values.children, i)
|
table.remove(self._values.children, i)
|
||||||
child.parent = nil
|
child.parent = nil
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
self:removeChildrenEvents(child)
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
function Container:handleEvent(event, ...)
|
function Container:getChild(path)
|
||||||
if(VisualElement.handleEvent(self, event, ...))then
|
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 = {...}
|
local args = {...}
|
||||||
if event:find("mouse_") then
|
if event:find("mouse_") then
|
||||||
local button, absX, absY = ...
|
local button, absX, absY = ...
|
||||||
local relX, relY = self:getRelativePosition(absX, absY)
|
local relX, relY = self:getRelativePosition(absX, absY)
|
||||||
args = {button, relX, relY}
|
args = {button, relX, relY}
|
||||||
end
|
end
|
||||||
if self._values.childrenEvents[event] then
|
return args
|
||||||
for _, child in ipairs(self._values.childrenEvents[event]) do
|
end
|
||||||
if(child:handleEvent(event, table.unpack(args)))then
|
|
||||||
return true
|
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
|
end
|
||||||
end
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function Container:handleEvent(event, ...)
|
||||||
|
if(VisualElement.handleEvent(self, event, ...))then
|
||||||
|
local args = convertMousePosition(self, event, ...)
|
||||||
|
return callChildrenEvents(self, false, event, table.unpack(args))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
--[[function Container:mouse_click(button, x, y)
|
function Container:mouse_click(button, x, y)
|
||||||
if VisualElement.mouse_click(self, button, x, y) then
|
if VisualElement.mouse_click(self, button, x, y) then
|
||||||
if self._values.childrenEvents.mouse_click then
|
local args = convertMousePosition(self, "mouse_click", button, x, y)
|
||||||
for _, child in ipairs(self._values.childrenEvents.mouse_click) do
|
local success, child = callChildrenEvents(self, true, "mouse_click", table.unpack(args))
|
||||||
if child:mouse_click(button, x, y) then
|
if(success)then
|
||||||
|
self.set("focusedChild", child)
|
||||||
return true
|
return true
|
||||||
end
|
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
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function Container:char(char)
|
||||||
|
if self.get("focusedChild") then
|
||||||
|
return self.get("focusedChild"):dispatchEvent("char", char)
|
||||||
end
|
end
|
||||||
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)
|
function Container:multiBlit(x, y, width, height, text, fg, bg)
|
||||||
local w, h = self.get("width"), self.get("height")
|
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)
|
VisualElement.textFg(self, math.max(1, x), math.max(1, y), text:sub(textStart, textStart + textLen - 1), fg)
|
||||||
end
|
end
|
||||||
|
|
||||||
---@diagnostic disable-next-line: duplicate-set-field
|
|
||||||
function Container:render()
|
function Container:render()
|
||||||
VisualElement.render(self)
|
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
|
if child == self then
|
||||||
self.basalt.LOGGER.error("CIRCULAR REFERENCE DETECTED!")
|
self.basalt.LOGGER.error("CIRCULAR REFERENCE DETECTED!")
|
||||||
return
|
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
|
---@class Frame : Container
|
||||||
local Frame = setmetatable({}, Container)
|
local Frame = setmetatable({}, Container)
|
||||||
Frame.__index = Frame
|
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()
|
local self = setmetatable({}, Frame):__init()
|
||||||
self:init(id, basalt)
|
self:init(props, basalt)
|
||||||
self.set("width", 12)
|
self.set("width", 12)
|
||||||
self.set("height", 6)
|
self.set("height", 6)
|
||||||
self.set("background", colors.blue)
|
self.set("background", colors.blue)
|
||||||
self.set("type", "Frame")
|
|
||||||
self.set("z", 10)
|
self.set("z", 10)
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function Frame:init(props, basalt)
|
||||||
|
Container.init(self, props, basalt)
|
||||||
|
self.set("type", "Frame")
|
||||||
|
end
|
||||||
|
|
||||||
return Frame
|
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
|
---@alias color number
|
||||||
|
|
||||||
@@ -8,40 +9,70 @@ VisualElement.__index = VisualElement
|
|||||||
local tHex = require("libraries/colorHex")
|
local tHex = require("libraries/colorHex")
|
||||||
|
|
||||||
---@property x number 1 x position of the element
|
---@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
|
---@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
|
---@property z number 1 z position of the element
|
||||||
BaseElement.defineProperty(VisualElement, "z", {default = 1, type = "number", canTriggerRender = true, setter = function(self, value)
|
VisualElement.defineProperty(VisualElement, "z", {default = 1, type = "number", canTriggerRender = true, setter = function(self, value)
|
||||||
self.basalt.LOGGER.debug("Setting z to " .. value)
|
|
||||||
if self.parent then
|
if self.parent then
|
||||||
self.parent:sortChildren()
|
self.parent:sortChildren()
|
||||||
end
|
end
|
||||||
return value
|
return value
|
||||||
end})
|
end})
|
||||||
|
|
||||||
---@property width number 1 width of the element
|
---@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
|
---@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
|
---@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
|
---@property foreground color white foreground color of the element
|
||||||
BaseElement.defineProperty(VisualElement, "foreground", {default = colors.white, type = "number", canTriggerRender = true})
|
VisualElement.defineProperty(VisualElement, "foreground", {default = colors.white, type = "number", canTriggerRender = true})
|
||||||
---@property clicked boolean false element is currently clicked
|
---@property clicked boole an false element is currently clicked
|
||||||
BaseElement.defineProperty(VisualElement, "clicked", {default = false, type = "boolean"})
|
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
|
--- 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
|
--- @param basalt table The basalt instance
|
||||||
--- @return VisualElement object The newly created VisualElement instance
|
--- @return VisualElement object The newly created VisualElement instance
|
||||||
--- @usage local element = VisualElement.new("myId", basalt)
|
--- @usage local element = VisualElement.new("myId", basalt)
|
||||||
function VisualElement.new(id, basalt)
|
function VisualElement.new(props, basalt)
|
||||||
local self = setmetatable({}, VisualElement):__init()
|
local self = setmetatable({}, VisualElement):__init()
|
||||||
self:init(id, basalt)
|
self:init(props, basalt)
|
||||||
self.set("type", "VisualElement")
|
self.set("type", "VisualElement")
|
||||||
return self
|
return self
|
||||||
end
|
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
|
--- 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 x number The x position to draw
|
||||||
--- @param y number The y position to draw
|
--- @param y number The y position to draw
|
||||||
@@ -87,9 +118,10 @@ end
|
|||||||
function VisualElement:mouse_click(button, x, y)
|
function VisualElement:mouse_click(button, x, y)
|
||||||
if self:isInBounds(x, y) then
|
if self:isInBounds(x, y) then
|
||||||
self.set("clicked", true)
|
self.set("clicked", true)
|
||||||
self:fireEvent("mouse_click", button, x, y)
|
self:fireEvent("mouse_click", button, self:getRelativePosition(x, y))
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
function VisualElement:mouse_up(button, x, y)
|
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)
|
self:fireEvent("mouse_up", button, x, y)
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
self:fireEvent("mouse_release", button, x, y)
|
self:fireEvent("mouse_release", button, self:getRelativePosition(x, y))
|
||||||
end
|
end
|
||||||
|
|
||||||
function VisualElement:mouse_release()
|
function VisualElement:mouse_release()
|
||||||
self.set("clicked", false)
|
self.set("clicked", false)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Handles all events
|
function VisualElement:focus()
|
||||||
--- @param event string The event to handle
|
self:fireEvent("focus")
|
||||||
--- @vararg any The arguments for the event
|
end
|
||||||
--- @return boolean? handled Whether the event was handled
|
|
||||||
function VisualElement:handleEvent(event, ...)
|
function VisualElement:blur()
|
||||||
if(self[event])then
|
self:fireEvent("blur")
|
||||||
return self[event](self, ...)
|
self:setCursor(1,1, false)
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Returns the absolute position of the element or the given coordinates.
|
--- Returns the absolute position of the element or the given coordinates.
|
||||||
---@param x? number x position
|
---@param x? number x position
|
||||||
---@param y? number y position
|
---@param y? number y position
|
||||||
function VisualElement:getAbsolutePosition(x, y)
|
function VisualElement:getAbsolutePosition(x, y)
|
||||||
if (x == nil) or (y == nil) then
|
local xPos, yPos = self.get("x"), self.get("y")
|
||||||
x, y = self.get("x"), self.get("y")
|
if(x ~= nil) then
|
||||||
|
xPos = xPos + x - 1
|
||||||
|
end
|
||||||
|
if(y ~= nil) then
|
||||||
|
yPos = yPos + y - 1
|
||||||
end
|
end
|
||||||
|
|
||||||
local parent = self.parent
|
local parent = self.parent
|
||||||
while parent do
|
while parent do
|
||||||
local px, py = parent.get("x"), parent.get("y")
|
local px, py = parent.get("x"), parent.get("y")
|
||||||
x = x + px - 1
|
xPos = xPos + px - 1
|
||||||
y = y + py - 1
|
yPos = yPos + py - 1
|
||||||
parent = parent.parent
|
parent = parent.parent
|
||||||
end
|
end
|
||||||
|
|
||||||
return x, y
|
return xPos, yPos
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Returns the relative position of the element or the given coordinates.
|
--- 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)
|
y - (elementY - 1) - (parentY - 1)
|
||||||
end
|
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
|
--- Renders the element
|
||||||
--- @usage element:render()
|
--- @usage element:render()
|
||||||
function VisualElement:render()
|
function VisualElement:render()
|
||||||
|
if(not self.get("backgroundEnabled"))then
|
||||||
|
return
|
||||||
|
end
|
||||||
local width, height = self.get("width"), self.get("height")
|
local width, height = self.get("width"), self.get("height")
|
||||||
self:multiBlit(1, 1, width, height, " ", tHex[self.get("foreground")], tHex[self.get("background")])
|
self:multiBlit(1, 1, width, height, " ", tHex[self.get("foreground")], tHex[self.get("background")])
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
|
|
||||||
local args = {...}
|
local args = {...}
|
||||||
|
local basaltPath = fs.getDir(args[2])
|
||||||
local basaltPath = args[1] or "basalt"
|
|
||||||
|
|
||||||
local defaultPath = package.path
|
local defaultPath = package.path
|
||||||
local format = "path;/path/?.lua;/path/?/init.lua;"
|
local format = "path;/path/?.lua;/path/?/init.lua;"
|
||||||
@@ -18,6 +16,7 @@ end
|
|||||||
-- Use xpcall with error handler
|
-- Use xpcall with error handler
|
||||||
local ok, result = pcall(require, "main")
|
local ok, result = pcall(require, "main")
|
||||||
|
|
||||||
|
package.path = defaultPath
|
||||||
if not ok then
|
if not ok then
|
||||||
errorHandler(result)
|
errorHandler(result)
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -24,4 +24,32 @@ function utils.deepCopy(obj)
|
|||||||
return copy
|
return copy
|
||||||
end
|
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
|
return utils
|
||||||
76
src/main.lua
76
src/main.lua
@@ -1,5 +1,9 @@
|
|||||||
|
local benchmark = require("benchmark")
|
||||||
|
benchmark.start("Basalt Initialization")
|
||||||
local elementManager = require("elementManager")
|
local elementManager = require("elementManager")
|
||||||
local errorManager = require("errorManager")
|
local errorManager = require("errorManager")
|
||||||
|
local propertySystem = require("propertySystem")
|
||||||
|
|
||||||
|
|
||||||
--- This is the UI Manager and the starting point for your project. The following functions allow you to influence the default behavior of Basalt.
|
--- 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._schedule = {}
|
||||||
basalt._plugins = {}
|
basalt._plugins = {}
|
||||||
basalt.LOGGER = require("log")
|
basalt.LOGGER = require("log")
|
||||||
|
basalt.path = fs.getDir(select(2, ...))
|
||||||
|
|
||||||
local mainFrame = nil
|
local mainFrame = nil
|
||||||
local updaterActive = false
|
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.
|
--- Creates and returns a new UI element of the specified type.
|
||||||
--- @shortDescription Creates a new UI element
|
--- @shortDescription Creates a new UI element
|
||||||
--- @param type string The type of element to create (e.g. "Button", "Label", "BaseFrame")
|
--- @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
|
--- @return table element The created element instance
|
||||||
--- @usage local button = basalt.create("Button")
|
--- @usage local button = basalt.create("Button")
|
||||||
function basalt.create(type, id)
|
function basalt.create(type, properties, lazyLoading, parent)
|
||||||
if(id==nil)then id = elementManager.generateId() end
|
if(_type(properties)=="string")then properties = {name=properties} end
|
||||||
local element = elementManager.getElement(type).new(id, basalt)
|
if(properties == nil)then properties = {name = type} end
|
||||||
local ok, result = pcall(require, "main")
|
local elementClass = elementManager.getElement(type)
|
||||||
if not ok then
|
if(lazyLoading)then
|
||||||
errorManager(false, result)
|
local blueprint = propertySystem.blueprint(elementClass, properties, basalt, parent)
|
||||||
|
table.insert(lazyElements, blueprint)
|
||||||
|
queueLazyElements()
|
||||||
|
return blueprint
|
||||||
|
else
|
||||||
|
return elementClass.new(properties, basalt)
|
||||||
end
|
end
|
||||||
return element
|
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Creates and returns a new frame
|
--- Creates and returns a new frame
|
||||||
@@ -87,17 +132,10 @@ end
|
|||||||
--- @local Internal event handler
|
--- @local Internal event handler
|
||||||
local function updateEvent(event, ...)
|
local function updateEvent(event, ...)
|
||||||
if(event=="terminate")then basalt.stop() end
|
if(event=="terminate")then basalt.stop() end
|
||||||
|
if lazyElementsEventHandler(event, ...) then return end
|
||||||
|
|
||||||
if event:find("mouse") then
|
if(mainFrame:dispatchEvent(event, ...))then
|
||||||
if mainFrame then
|
return
|
||||||
mainFrame:handleEvent(event, ...)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if event:find("key") then
|
|
||||||
if mainFrame then
|
|
||||||
mainFrame:handleEvent(event, ...)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if basalt._events[event] then
|
if basalt._events[event] then
|
||||||
@@ -137,12 +175,14 @@ end
|
|||||||
--- @usage basalt.run()
|
--- @usage basalt.run()
|
||||||
--- @usage basalt.run(false)
|
--- @usage basalt.run(false)
|
||||||
function basalt.run(isActive)
|
function basalt.run(isActive)
|
||||||
|
benchmark.stop("Basalt Initialization")
|
||||||
updaterActive = isActive
|
updaterActive = isActive
|
||||||
if(isActive==nil)then updaterActive = true end
|
if(isActive==nil)then updaterActive = true end
|
||||||
local function f()
|
local function f()
|
||||||
renderFrames()
|
renderFrames()
|
||||||
while updaterActive do
|
while updaterActive do
|
||||||
updateEvent(os.pullEventRaw())
|
updateEvent(os.pullEventRaw())
|
||||||
|
renderFrames()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
while updaterActive do
|
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 deepCopy = require("libraries/utils").deepCopy
|
||||||
local expect = require("libraries/expect")
|
local expect = require("libraries/expect")
|
||||||
|
local errorManager = require("errorManager")
|
||||||
|
local log = require("log")
|
||||||
|
|
||||||
--- @class PropertySystem
|
--- @class PropertySystem
|
||||||
local PropertySystem = {}
|
local PropertySystem = {}
|
||||||
PropertySystem.__index = PropertySystem
|
PropertySystem.__index = PropertySystem
|
||||||
|
|
||||||
PropertySystem._properties = {}
|
PropertySystem._properties = {}
|
||||||
|
local blueprintTemplates = {}
|
||||||
|
|
||||||
function PropertySystem.defineProperty(class, name, config)
|
function PropertySystem.defineProperty(class, name, config)
|
||||||
if not rawget(class, '_properties') then
|
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)
|
local capitalizedName = name:sub(1,1):upper() .. name:sub(2)
|
||||||
|
|
||||||
class["get" .. capitalizedName] = function(self)
|
class["get" .. capitalizedName] = function(self, ...)
|
||||||
expect(1, self, "element")
|
expect(1, self, "element")
|
||||||
local value = self._values[name]
|
local value = self._values[name]
|
||||||
return config.getter and config.getter(value) or value
|
return config.getter and config.getter(self, value, ...) or value
|
||||||
end
|
end
|
||||||
|
|
||||||
class["set" .. capitalizedName] = function(self, value)
|
class["set" .. capitalizedName] = function(self, value, ...)
|
||||||
expect(1, self, "element")
|
expect(1, self, "element")
|
||||||
expect(2, value, config.type)
|
expect(2, value, config.type)
|
||||||
if config.setter then
|
if config.setter then
|
||||||
value = config.setter(self, value)
|
value = config.setter(self, value, ...)
|
||||||
end
|
end
|
||||||
|
|
||||||
self:_updateProperty(name, value)
|
self:_updateProperty(name, value)
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
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()
|
function PropertySystem:__init()
|
||||||
self._values = {}
|
self._values = {}
|
||||||
self._observers = {}
|
self._observers = {}
|
||||||
|
|
||||||
self.set = function(name, value)
|
self.set = function(name, value, ...)
|
||||||
local oldValue = self._values[name]
|
local oldValue = self._values[name]
|
||||||
self._values[name] = value
|
local config = self._properties[name]
|
||||||
if(self._properties[name].setter) then
|
if(config~=nil)then
|
||||||
value = self._properties[name].setter(self, value)
|
if(config.setter) then
|
||||||
|
value = config.setter(self, value, ...)
|
||||||
end
|
end
|
||||||
|
if config.canTriggerRender then
|
||||||
|
self:updateRender()
|
||||||
|
end
|
||||||
|
self._values[name] = value
|
||||||
if oldValue ~= value and self._observers[name] then
|
if oldValue ~= value and self._observers[name] then
|
||||||
for _, callback in ipairs(self._observers[name]) do
|
for _, callback in ipairs(self._observers[name]) do
|
||||||
callback(self, value, oldValue)
|
callback(self, value, oldValue)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
self.get = function(name)
|
self.get = function(name, ...)
|
||||||
return self._values[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
|
end
|
||||||
|
|
||||||
local properties = {}
|
local properties = {}
|
||||||
@@ -139,4 +261,25 @@ function PropertySystem:observe(name, callback)
|
|||||||
return self
|
return self
|
||||||
end
|
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
|
return PropertySystem
|
||||||
102
src/render.lua
102
src/render.lua
@@ -1,7 +1,6 @@
|
|||||||
local Render = {}
|
local Render = {}
|
||||||
Render.__index = Render
|
Render.__index = Render
|
||||||
local colorChars = require("libraries/colorHex")
|
local colorChars = require("libraries/colorHex")
|
||||||
local log = require("log")
|
|
||||||
|
|
||||||
function Render.new(terminal)
|
function Render.new(terminal)
|
||||||
local self = setmetatable({}, Render)
|
local self = setmetatable({}, Render)
|
||||||
@@ -12,19 +11,27 @@ function Render.new(terminal)
|
|||||||
text = {},
|
text = {},
|
||||||
fg = {},
|
fg = {},
|
||||||
bg = {},
|
bg = {},
|
||||||
changed = {}
|
dirtyRects = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
for y=1, self.height do
|
for y=1, self.height do
|
||||||
self.buffer.text[y] = string.rep(" ", self.width)
|
self.buffer.text[y] = string.rep(" ", self.width)
|
||||||
self.buffer.fg[y] = string.rep("0", self.width)
|
self.buffer.fg[y] = string.rep("0", self.width)
|
||||||
self.buffer.bg[y] = string.rep("f", self.width)
|
self.buffer.bg[y] = string.rep("f", self.width)
|
||||||
self.buffer.changed[y] = false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return self
|
return self
|
||||||
end
|
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)
|
function Render:blit(x, y, text, fg, bg)
|
||||||
if y < 1 or y > self.height then return self end
|
if y < 1 or y > self.height then return self end
|
||||||
if(#text ~= #fg or #text ~= #bg)then
|
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.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.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.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
|
return self
|
||||||
end
|
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.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.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.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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
self:addDirtyRect(x, y, width, height)
|
||||||
return self
|
return self
|
||||||
end
|
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.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.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
|
return self
|
||||||
end
|
end
|
||||||
@@ -77,7 +84,7 @@ function Render:text(x, y, text)
|
|||||||
if y < 1 or y > self.height then return self end
|
if y < 1 or y > self.height then return self end
|
||||||
|
|
||||||
self.buffer.text[y] = self.buffer.text[y]:sub(1,x-1) .. text .. self.buffer.text[y]:sub(x+#text)
|
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
|
return self
|
||||||
end
|
end
|
||||||
@@ -86,7 +93,7 @@ function Render:fg(x, y, fg)
|
|||||||
if y < 1 or y > self.height then return self end
|
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.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
|
return self
|
||||||
end
|
end
|
||||||
@@ -95,7 +102,7 @@ function Render:bg(x, y, bg)
|
|||||||
if y < 1 or y > self.height then return self end
|
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.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
|
return self
|
||||||
end
|
end
|
||||||
@@ -106,23 +113,84 @@ function Render:clear(bg)
|
|||||||
self.buffer.text[y] = string.rep(" ", self.width)
|
self.buffer.text[y] = string.rep(" ", self.width)
|
||||||
self.buffer.fg[y] = string.rep("0", self.width)
|
self.buffer.fg[y] = string.rep("0", self.width)
|
||||||
self.buffer.bg[y] = string.rep(bgChar, self.width)
|
self.buffer.bg[y] = string.rep(bgChar, self.width)
|
||||||
self.buffer.changed[y] = true
|
self:addDirtyRect(1, y, self.width, 1)
|
||||||
end
|
end
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
function Render:render()
|
function Render:render()
|
||||||
for y=1, self.height do
|
local benchmark = require("benchmark")
|
||||||
if self.buffer.changed[y] then
|
benchmark.start("render")
|
||||||
self.terminal.setCursorPos(1, y)
|
|
||||||
|
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.terminal.blit(
|
||||||
self.buffer.text[y],
|
self.buffer.text[y]:sub(rect.x, rect.x + rect.width - 1),
|
||||||
self.buffer.fg[y],
|
self.buffer.fg[y]:sub(rect.x, rect.x + rect.width - 1),
|
||||||
self.buffer.bg[y]
|
self.buffer.bg[y]:sub(rect.x, rect.x + rect.width - 1)
|
||||||
)
|
)
|
||||||
self.buffer.changed[y] = false
|
|
||||||
end
|
end
|
||||||
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
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user