Update with some fixxes and improvements

This commit is contained in:
Robert Jelic
2025-02-20 11:32:33 +01:00
parent 2dfb2443cd
commit 6393198552
14 changed files with 446 additions and 154 deletions

View File

@@ -54,6 +54,8 @@ end
--- @param fg string The foreground color
--- @param bg string The background color
function BaseFrame:multiBlit(x, y, width, height, text, fg, bg)
if(x<1)then width = width + x - 1; x = 1 end
if(y<1)then height = height + y - 1; y = 1 end
self._render:multiBlit(x, y, width, height, text, fg, bg)
end
@@ -64,6 +66,7 @@ end
--- @param text string The text to render
--- @param fg colors The foreground color
function BaseFrame:textFg(x, y, text, fg)
if x < 1 then text = string.sub(text, 1 - x); x = 1 end
self._render:textFg(x, y, text, fg)
end
@@ -74,6 +77,7 @@ end
--- @param text string The text to render
--- @param bg colors The background color
function BaseFrame:textBg(x, y, text, bg)
if x < 1 then text = string.sub(text, 1 - x); x = 1 end
self._render:textBg(x, y, text, bg)
end
@@ -85,6 +89,11 @@ end
--- @param fg string The foreground color
--- @param bg string The background color
function BaseFrame:blit(x, y, text, fg, bg)
if x < 1 then
text = string.sub(text, 1 - x)
fg = string.sub(fg, 1 - x)
bg = string.sub(bg, 1 - x)
x = 1 end
self._render:blit(x, y, text, fg, bg)
end

View File

@@ -2,7 +2,6 @@ local elementManager = require("elementManager")
local VisualElement = elementManager.getElement("VisualElement")
local getCenteredPosition = require("libraries/utils").getCenteredPosition
--- This is the button class. It is a visual element that can be clicked.
---@class Button : VisualElement
local Button = setmetatable({}, VisualElement)
Button.__index = Button
@@ -12,30 +11,21 @@ Button.defineProperty(Button, "text", {default = "Button", type = "string", canT
---@event mouse_click The event that is triggered when the button is clicked
Button.listenTo(Button, "mouse_click")
Button.listenTo(Button, "mouse_up")
--- Creates a new Button instance
--- @shortDescription Creates a new Button instance
--- @return table self The created instance
function Button.new()
function Button.new(props, basalt)
local self = setmetatable({}, Button):__init()
self:init(props, basalt)
self.set("width", 10)
self.set("height", 3)
self.set("z", 5)
return self
end
--- Initializes the Button instance
--- @shortDescription Initializes the Button instance
--- @param props table The properties to initialize the element with
--- @param basalt table The basalt instance
function Button:init(props, basalt)
VisualElement.init(self, props, basalt)
self.set("type", "Button")
end
--- Renders the Button
--- @shortDescription Renders the Button
function Button:render()
VisualElement.render(self)
local text = self.get("text")

View File

@@ -7,10 +7,26 @@ Checkbox.__index = Checkbox
---@property checked boolean Whether checkbox is checked
Checkbox.defineProperty(Checkbox, "checked", {default = false, type = "boolean", canTriggerRender = true})
---@property text string Label text
Checkbox.defineProperty(Checkbox, "text", {default = "", type = "string", canTriggerRender = true})
---@property symbol string Check symbol
Checkbox.defineProperty(Checkbox, "symbol", {default = "x", type = "string"})
---@property text string empty Text to display
Checkbox.defineProperty(Checkbox, "text", {default = " ", type = "string", canTriggerRender = true, setter=function(self, value)
local checkedText = self.get("checkedText")
local width = math.max(#value, #checkedText)
if(self.get("autoSize"))then
self.set("width", width)
end
return value
end})
---@property checkedText string Text when checked
Checkbox.defineProperty(Checkbox, "checkedText", {default = "x", type = "string", canTriggerRender = true, setter=function(self, value)
local text = self.get("text")
local width = math.max(#value, #text)
if(self.get("autoSize"))then
self.set("width", width)
end
return value
end})
---@property autoSize boolean true Whether to automatically size the checkbox
Checkbox.defineProperty(Checkbox, "autoSize", {default = true, type = "boolean"})
Checkbox.listenTo(Checkbox, "mouse_click")
@@ -19,8 +35,6 @@ Checkbox.listenTo(Checkbox, "mouse_click")
--- @return Checkbox self The created instance
function Checkbox.new()
local self = setmetatable({}, Checkbox):__init()
self.set("width", 1)
self.set("height", 1)
return self
end
@@ -53,13 +67,12 @@ end
function Checkbox:render()
VisualElement.render(self)
local text = self.get("checked") and self.get("symbol") or " "
self:textFg(1, 1, "["..text.."]", self.get("foreground"))
local checked = self.get("checked")
local defaultText = self.get("text")
local checkedText = self.get("checkedText")
local text = string.sub(checked and checkedText or defaultText, 1, self.get("width"))
local label = self.get("text")
if #label > 0 then
self:textFg(4, 1, label, self.get("foreground"))
end
self:textFg(1, 1, text, self.get("foreground"))
end
return Checkbox

View File

@@ -47,6 +47,19 @@ Container.defineProperty(Container, "visibleChildren", {default = {}, type = "ta
---@property visibleChildrenEvents table {} The visible children events of the container
Container.defineProperty(Container, "visibleChildrenEvents", {default = {}, type = "table"})
---@property offsetX number 0 Horizontal content offset
Container.defineProperty(Container, "offsetX", {default = 0, type = "number", canTriggerRender = true, setter=function(self, value)
self.set("childrenSorted", false)
self.set("childrenEventsSorted", false)
return value
end})
---@property offsetY number 0 Vertical content offset
Container.defineProperty(Container, "offsetY", {default = 0, type = "number", canTriggerRender = true, setter=function(self, value)
self.set("childrenSorted", false)
self.set("childrenEventsSorted", false)
return value
end})
for k, _ in pairs(elementManager:getElementList()) do
local capitalizedName = k:sub(1,1):upper() .. k:sub(2)
if capitalizedName ~= "BaseFrame" then
@@ -87,14 +100,26 @@ end
--- @param child table The child to check
--- @return boolean boolean the child is visible
function Container:isChildVisible(child)
local containerW, containerH = self.get("width"), self.get("height")
local offsetX, offsetY = self.get("offsetX"), self.get("offsetY")
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
local relativeX
local relativeY
if(child.get("ignoreOffset"))then
relativeX = childX
relativeY = childY
else
relativeX = childX - offsetX
relativeY = childY - offsetY
end
return (relativeX + childW > 0) and
(relativeX <= containerW) and
(relativeY + childH > 0) and
(relativeY <= containerH)
end
--- Adds a child to the container
@@ -115,7 +140,7 @@ end
local function sortAndFilterChildren(self, children)
local visibleChildren = {}
for _, child in ipairs(children) do
if self:isChildVisible(child) and child.get("visible") then
table.insert(visibleChildren, child)
@@ -295,7 +320,8 @@ local function convertMousePosition(self, event, ...)
local args = {...}
if event:find("mouse_") then
local button, absX, absY = ...
local relX, relY = self:getRelativePosition(absX, absY)
local xOffset, yOffset = self.get("offsetX"), self.get("offsetY")
local relX, relY = self:getRelativePosition(absX + xOffset, absY + yOffset)
args = {button, relX, relY}
end
return args
@@ -308,7 +334,7 @@ function Container:callChildrenEvents(visibleOnly, event, ...)
for i = #events, 1, -1 do
local child = events[i]
if(child:dispatchEvent(event, ...))then
return true, child
return true, child
end
end
end
@@ -447,7 +473,7 @@ end
--- @return Container self The container instance
function Container:multiBlit(x, y, width, height, text, fg, bg)
local w, h = self.get("width"), self.get("height")
width = x < 1 and math.min(width + x - 1, w) or math.min(width, math.max(0, w - x + 1))
height = y < 1 and math.min(height + y - 1, h) or math.min(height, math.max(0, h - y + 1))

View File

@@ -1,5 +1,6 @@
local elementManager = require("elementManager")
local VisualElement = elementManager.getElement("VisualElement")
local wrapText = require("libraries/utils").wrapText
--- This is the label class. It provides a simple text display element that automatically
--- resizes its width based on the text content.
@@ -8,9 +9,23 @@ local Label = setmetatable({}, VisualElement)
Label.__index = Label
---@property text string Label The text content to display. Can be a string or a function that returns a string
Label.defineProperty(Label, "text", {default = "Label", type = "string", setter = function(self, value)
Label.defineProperty(Label, "text", {default = "Label", type = "string", canTriggerRender = true, setter = function(self, value)
if(type(value)=="function")then value = value() end
self.set("width", #value)
if(self.get("autoSize"))then
self.set("width", #value)
else
self.set("height", #wrapText(value, self.get("width")))
end
return value
end})
---@property autoSize boolean true Whether the label should automatically resize its width based on the text content
Label.defineProperty(Label, "autoSize", {default = true, type = "boolean", canTriggerRender = true, setter = function(self, value)
if(value)then
self.set("width", #self.get("text"))
else
self.set("height", #wrapText(self.get("text"), self.get("width")))
end
return value
end})
@@ -37,12 +52,28 @@ function Label:init(props, basalt)
return self
end
--- Gets the wrapped lines of the Label
--- @shortDescription Gets the wrapped lines of the Label
--- @return table wrappedText The wrapped lines of the Label
function Label:getWrappedText()
local text = self.get("text")
local wrappedText = wrapText(text, self.get("width"))
return wrappedText
end
--- Renders the Label
--- @shortDescription Renders the Label by drawing its text content
function Label:render()
VisualElement.render(self)
local text = self.get("text")
self:textFg(1, 1, text, self.get("foreground"))
if(self.get("autoSize"))then
self:textFg(1, 1, text, self.get("foreground"))
else
local wrappedText = wrapText(text, self.get("width"))
for i, line in ipairs(wrappedText) do
self:textFg(1, i, line, self.get("foreground"))
end
end
end
return Label

View File

@@ -29,6 +29,7 @@ function List.new()
local self = setmetatable({}, List):__init()
self.set("width", 16)
self.set("height", 8)
self.set("z", 5)
self.set("background", colors.gray)
return self
end
@@ -41,6 +42,7 @@ end
function List:init(props, basalt)
VisualElement.init(self, props, basalt)
self.set("type", "List")
return self
end
--- Adds an item to the list

View File

@@ -64,6 +64,9 @@ VisualElement.defineProperty(VisualElement, "visible", {default = true, type = "
return value
end})
---@property ignoreOffset boolean false Whether to ignore the parent's offset
VisualElement.defineProperty(VisualElement, "ignoreOffset", {default = false, type = "boolean"})
---@combinedProperty position {x y} Combined x, y position
VisualElement.combineProperties(VisualElement, "position", "x", "y")
---@combinedProperty size {width height} Combined width, height
@@ -85,6 +88,7 @@ VisualElement.listenTo(VisualElement, "focus")
VisualElement.listenTo(VisualElement, "blur")
VisualElement.listenTo(VisualElement, "mouse_enter", "mouse_move")
VisualElement.listenTo(VisualElement, "mouse_leave", "mouse_move")
VisualElement.listenTo(VisualElement, "mouse_scroll")
local max, min = math.max, math.min
@@ -112,8 +116,9 @@ end
--- @shortDescription Multi-character drawing with colors
---@protected
function VisualElement:multiBlit(x, y, width, height, text, fg, bg)
x = x + self.get("x") - 1
y = y + self.get("y") - 1
local xElement, yElement = self:calculatePosition()
x = x + xElement - 1
y = y + yElement - 1
self.parent:multiBlit(x, y, width, height, text, fg, bg)
end
@@ -124,8 +129,9 @@ end
--- @param text string The text char to draw
--- @param fg color The foreground color
function VisualElement:textFg(x, y, text, fg)
x = x + self.get("x") - 1
y = y + self.get("y") - 1
local xElement, yElement = self:calculatePosition()
x = x + xElement - 1
y = y + yElement - 1
self.parent:textFg(x, y, text, fg)
end
@@ -136,8 +142,9 @@ end
--- @param text string The text char to draw
--- @param bg color The background color
function VisualElement:textBg(x, y, text, bg)
x = x + self.get("x") - 1
y = y + self.get("y") - 1
local xElement, yElement = self:calculatePosition()
x = x + xElement - 1
y = y + yElement - 1
self.parent:textBg(x, y, text, bg)
end
@@ -149,8 +156,9 @@ end
--- @param fg string The foreground color
--- @param bg string The background color
function VisualElement:blit(x, y, text, fg, bg)
x = x + self.get("x") - 1
y = y + self.get("y") - 1
local xElement, yElement = self:calculatePosition()
x = x + xElement - 1
y = y + yElement - 1
self.parent:blit(x, y, text, fg, bg)
end
@@ -162,6 +170,12 @@ end
function VisualElement:isInBounds(x, y)
local xPos, yPos = self.get("x"), self.get("y")
local width, height = self.get("width"), self.get("height")
if(self.get("ignoreOffset"))then
if(self.parent)then
x = x - self.parent.get("offsetX")
y = y - self.parent.get("offsetY")
end
end
return x >= xPos and x <= xPos + width - 1 and
y >= yPos and y <= yPos + height - 1
@@ -256,6 +270,18 @@ function VisualElement:blur()
self:setCursor(1,1, false)
end
function VisualElement:calculatePosition()
local x, y = self.get("x"), self.get("y")
if not self.get("ignoreOffset") then
if self.parent ~= nil then
local xO, yO = self.parent.get("offsetX"), self.parent.get("offsetY")
x = x - xO
y = y - yO
end
end
return x, y
end
--- Returns the absolute position of the element or the given coordinates.
--- @shortDescription Returns the absolute position of the element
---@param x? number x position
@@ -295,9 +321,7 @@ function VisualElement:getRelativePosition(x, y)
parentX, parentY = self.parent:getRelativePosition()
end
local elementX = self.get("x")
local elementY = self.get("y")
local elementX, elementY = self.get("x"), self.get("y")
return x - (elementX - 1) - (parentX - 1),
y - (elementY - 1) - (parentY - 1)
end

View File

@@ -47,25 +47,61 @@ function utils.uuid()
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
function utils.split(str, delimiter)
local result = {}
for match in (str..delimiter):gmatch("(.-)"..delimiter) do
table.insert(result, match)
end
return result
end
while true do
local index = str:find(sep, start, true)
if not index then
parts[splitIndex] = str:sub(start, len)
break
function utils.removeTags(input)
return input:gsub("{[^}]+}", "")
end
function utils.wrapText(str, width)
if str == nil then return {} end
str = utils.removeTags(str)
local lines = {}
local paragraphs = utils.split(str, "\n\n")
for i, paragraph in ipairs(paragraphs) do
if #paragraph == 0 then
table.insert(lines, "")
if i < #paragraphs then
table.insert(lines, "")
end
else
local textLines = utils.split(paragraph, "\n")
for _, line in ipairs(textLines) do
local words = utils.split(line, " ")
local currentLine = ""
for _, word in ipairs(words) do
if #currentLine == 0 then
currentLine = word
elseif #currentLine + #word + 1 <= width then
currentLine = currentLine .. " " .. word
else
table.insert(lines, currentLine)
currentLine = word
end
end
if #currentLine > 0 then
table.insert(lines, currentLine)
end
end
if i < #paragraphs then
table.insert(lines, "")
end
end
parts[splitIndex] = str:sub(start, index - 1)
start = index + 1
splitIndex = splitIndex + 1
end
return parts
return lines
end
return utils

View File

@@ -48,7 +48,7 @@ function AnimationInstance.new(element, animType, args, duration, easing)
self.element = element
self.type = animType
self.args = args
self.duration = duration
self.duration = duration or 1
self.startTime = 0
self.isPaused = false
self.handlers = registeredAnimations[animType]
@@ -288,6 +288,26 @@ Animation.registerAnimation("move", {
end
})
Animation.registerAnimation("moveOffset", {
start = function(anim)
anim.startX = anim.element.get("offsetX")
anim.startY = anim.element.get("offsetY")
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("offsetX", math.floor(x))
anim.element.set("offsetY", math.floor(y))
return progress >= 1
end,
complete = function(anim)
anim.element.set("offsetX", anim.args[1])
anim.element.set("offsetY", anim.args[2])
end
})
Animation.registerAnimation("morphText", {
start = function(anim)
local startText = anim.element.get(anim.args[1])
@@ -384,12 +404,12 @@ function VisualElement.hooks.dispatchEvent(self, event, ...)
if animation then
animation:event(event, ...)
end
return true
end
end
---@private
function VisualElement.setup(element)
VisualElementBaseDispatchEvent = element.dispatchEvent
element.defineProperty(element, "animation", {default = nil, type = "table"})
element.listenTo(element, "timer")
end

View File

@@ -313,6 +313,11 @@ function API.start(name, options)
profile.name = name
profile.startTime = os.clock() * 1000
profile.custom = true
profile.calls = 0
profile.totalTime = 0
profile.minTime = math.huge
profile.maxTime = 0
profile.lastTime = 0
activeProfiles[name] = profile
end

View File

@@ -64,7 +64,7 @@ local function parseExpression(expr, element, propName)
elseif objName == "parent" then
return element.parent.get(propName)
else
local target = element:getBaseFrame():getChild(objName)
local target = element.parent:getChild(objName)
if not target then
errorManager.header = "Reactive evaluation error"
errorManager.error("Could not find element: " .. objName)
@@ -103,7 +103,7 @@ local function validateReferences(expr, element)
return false
end
else
local target = element:getBaseFrame():getChild(ref)
local target = element.parent:getChild(ref)
if not target then
errorManager.header = "Reactive evaluation error"
errorManager.error("Referenced element not found: " .. ref)

View File

@@ -91,7 +91,6 @@ function Render:multiBlit(x, y, width, height, text, fg, bg)
if(#text ~= #fg or #text ~= #bg)then
error("Text, fg, and bg must be the same length")
end
text = text:rep(width)
fg = fg:rep(width)
bg = bg:rep(width)
@@ -119,7 +118,6 @@ function Render:textFg(x, y, text, fg)
if y < 1 or y > self.height then return self end
fg = colorChars[fg] or "0"
fg = fg:rep(#text)
self.buffer.text[y] = sub(self.buffer.text[y]:sub(1,x-1) .. text .. self.buffer.text[y]:sub(x+#text), 1, self.width)
self.buffer.fg[y] = sub(self.buffer.fg[y]:sub(1,x-1) .. fg .. self.buffer.fg[y]:sub(x+#fg), 1, self.width)
self:addDirtyRect(x, y, #text, 1)