Lot of bug fixxes

This commit is contained in:
Robert Jelic
2025-02-18 19:13:51 +01:00
parent 8b6eaccd18
commit 0326cc12c7
12 changed files with 435 additions and 126 deletions

View File

@@ -1,39 +0,0 @@
local markdown = require("tools/markdown")
local log = require("src/log")
if not fs.exists("docs/references") then
fs.makeDir("docs/references")
end
local function processFile(inputFile)
local parsed = markdown.parseFile(inputFile)
local md = markdown.makeMarkdown(parsed)
local relativePath = inputFile:match("Basalt2/src/(.+)")
if not relativePath then return end
local outputFile = "docs/references/" .. relativePath:gsub("%.lua$", "")
local dir = fs.getDir(outputFile)
if not fs.exists(dir) then
fs.makeDir(dir)
end
--print(string.format("Processing: %s -> %s", inputFile, outputFile))
markdown.saveToFile(outputFile, md)
end
local function processDirectory(path)
for _, file in ipairs(fs.list(path)) do
local fullPath = fs.combine(path, file)
if fs.isDir(fullPath) then
processDirectory(fullPath)
elseif file:match("%.lua$") and not file:match("LuaLS%.lua$") then
processFile(fullPath)
end
end
end
processDirectory("Basalt2/src")

View File

@@ -34,12 +34,13 @@ BaseElement.defineProperty(BaseElement, "eventCallbacks", {default = {}, type =
--- @shortDescription Registers an event that this class can listen to
--- @param class table The class to add the event to
--- @param eventName string The name of the event to register
--- @param event? string The event to handle
--- @usage BaseElement.listenTo(MyClass, "mouse_click")
function BaseElement.listenTo(class, eventName)
function BaseElement.listenTo(class, eventName, event)
if not class._events then
class._events = {}
end
class._events[eventName] = true
class._events[eventName] = {enabled=true, name=eventName, event=event}
end
--- Creates a new BaseElement instance
@@ -64,13 +65,13 @@ function BaseElement:init(props, basalt)
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)
for key,event in pairs(BaseElement._events) do
self._registeredEvents[event.event or event.name] = true
local handlerName = "on" .. event.name:gsub("_(%l)", function(c)
return c:upper()
end):gsub("^%l", string.upper)
self[handlerName] = function(self, ...)
self:registerCallback(event, ...)
self:registerCallback(event.name, ...)
return self
end
end

View File

@@ -93,9 +93,14 @@ end
--- @param x number The x position to set the cursor to
--- @param y number The y position to set the cursor to
--- @param blink boolean Whether the cursor should blink
function BaseFrame:setCursor(x, y, blink)
function BaseFrame:setCursor(x, y, blink, color)
local term = self.get("term")
self._render:setCursor(x, y, blink)
self._render:setCursor(x, y, blink, color)
end
function BaseFrame:mouse_up(button, x, y)
Container.mouse_up(self, button, x, y)
Container.mouse_release(self, button, x, y)
end
--- Renders the Frame
@@ -110,4 +115,15 @@ function BaseFrame:render()
end
end
function BaseFrame:term_resize()
local width, height = self.get("term").getSize()
if(width == self.get("width") and height == self.get("height")) then
return
end
self.set("width", width)
self.set("height", height)
self._render:setSize(width, height)
self._renderUpdate = true
end
return BaseFrame

View File

@@ -3,8 +3,6 @@ local VisualElement = elementManager.getElement("VisualElement")
local expect = require("libraries/expect")
local split = require("libraries/utils").split
local max = math.max
--- The container class. It is a visual element that can contain other elements. It is the base class for all containers,
--- like Frames, BaseFrames, and more.
---@class Container : VisualElement
@@ -303,7 +301,7 @@ local function convertMousePosition(self, event, ...)
return args
end
local function callChildrenEvents(self, visibleOnly, event, ...)
function Container:callChildrenEvents(visibleOnly, event, ...)
local children = visibleOnly and self.get("visibleChildrenEvents") or self.get("childrenEvents")
if children[event] then
local events = children[event]
@@ -325,7 +323,7 @@ end
function Container:handleEvent(event, ...)
VisualElement.handleEvent(self, event, ...)
local args = convertMousePosition(self, event, ...)
return callChildrenEvents(self, false, event, table.unpack(args))
return self:callChildrenEvents(false, event, table.unpack(args))
end
--- Handles mouse click events
@@ -337,7 +335,7 @@ end
function Container:mouse_click(button, x, y)
if VisualElement.mouse_click(self, button, x, y) then
local args = convertMousePosition(self, "mouse_click", button, x, y)
local success, child = callChildrenEvents(self, true, "mouse_click", table.unpack(args))
local success, child = self:callChildrenEvents(true, "mouse_click", table.unpack(args))
if(success)then
self.set("focusedChild", child)
return true
@@ -357,7 +355,7 @@ end
function Container:mouse_up(button, x, y)
if VisualElement.mouse_up(self, button, x, y) then
local args = convertMousePosition(self, "mouse_up", button, x, y)
local success, child = callChildrenEvents(self, true, "mouse_up", table.unpack(args))
local success, child = self:callChildrenEvents(true, "mouse_up", table.unpack(args))
if(success)then
return true
end
@@ -365,6 +363,45 @@ function Container:mouse_up(button, x, y)
return false
end
function Container:mouse_release(button, x, y)
VisualElement.mouse_release(self, button, x, y)
local args = convertMousePosition(self, "mouse_release", button, x, y)
self:callChildrenEvents(false, "mouse_release", table.unpack(args))
end
function Container:mouse_move(_, x, y)
if VisualElement.mouse_move(self, _, x, y) then
local args = convertMousePosition(self, "mouse_move", _, x, y)
local success, child = self:callChildrenEvents(true, "mouse_move", table.unpack(args))
if(success)then
return true
end
end
return false
end
function Container:mouse_drag(button, x, y)
if VisualElement.mouse_drag(self, button, x, y) then
local args = convertMousePosition(self, "mouse_drag", button, x, y)
local success, child = self:callChildrenEvents(true, "mouse_drag", table.unpack(args))
if(success)then
return true
end
end
return false
end
function Container:mouse_scroll(direction, x, y)
if VisualElement.mouse_scroll(self, direction, x, y) then
local args = convertMousePosition(self, "mouse_scroll", direction, x, y)
local success, child = self:callChildrenEvents(true, "mouse_scroll", table.unpack(args))
if(success)then
return true
end
return false
end
end
--- Handles key events
--- @shortDescription Handles key events
--- @param key number The key that was pressed
@@ -438,6 +475,7 @@ function Container:textFg(x, y, text, fg)
if textLen <= 0 then return self end
VisualElement.textFg(self, math.max(1, x), math.max(1, y), text:sub(textStart, textStart + textLen - 1), fg)
return self
end
--- Draws a line of text and fg and bg as colors, it is usually used in the render loop

View File

@@ -23,6 +23,8 @@ Input.defineProperty(Input, "placeholderColor", {default = colors.gray, type = "
Input.defineProperty(Input, "focusedColor", {default = colors.blue, type = "number"})
---@property pattern string? nil Regular expression pattern for input validation
Input.defineProperty(Input, "pattern", {default = nil, type = "string"})
---@property cursorColor number nil Color of the cursor
Input.defineProperty(Input, "cursorColor", {default = nil, type = "number"})
Input.listenTo(Input, "mouse_click")
Input.listenTo(Input, "key")
@@ -106,7 +108,7 @@ function Input:key(key)
end
local relativePos = self.get("cursorPos") - self.get("viewOffset")
self:setCursor(relativePos, 1, true)
self:setCursor(relativePos, 1, true, self.get("cursorColor") or self.get("foreground"))
return true
end
@@ -134,7 +136,7 @@ function Input:mouse_click(button, x, y)
if VisualElement.mouse_click(self, button, x, y) then
local relX, relY = self:getRelativePosition(x, y)
local text = self.get("text")
self:setCursor(math.min(relX, #text + 1), relY, true)
self:setCursor(math.min(relX, #text + 1), relY, true, self.get("cursorColor") or self.get("foreground"))
self:set("cursorPos", relX + self.get("viewOffset"))
return true
end

180
src/elements/Scrollbar.lua Normal file
View File

@@ -0,0 +1,180 @@
local VisualElement = require("elements/VisualElement")
local tHex = require("libraries/colorHex")
---A scrollbar element that can be attached to other elements to control their scroll properties
---@class Scrollbar : VisualElement
local Scrollbar = setmetatable({}, VisualElement)
Scrollbar.__index = Scrollbar
---@property value number 0 Current scroll value
Scrollbar.defineProperty(Scrollbar, "value", {default = 0, type = "number", canTriggerRender = true})
---@property min number 0 Minimum scroll value
Scrollbar.defineProperty(Scrollbar, "min", {default = 0, type = "number", canTriggerRender = true})
---@property max number 100 Maximum scroll value
Scrollbar.defineProperty(Scrollbar, "max", {default = 100, type = "number", canTriggerRender = true})
---@property step number 1 Step size for scroll operations
Scrollbar.defineProperty(Scrollbar, "step", {default = 10, type = "number"})
---@property dragMultiplier number 1 How fast the scrollbar moves when dragging
Scrollbar.defineProperty(Scrollbar, "dragMultiplier", {default = 1, type = "number"})
---@property symbol string " " Symbol used for the scrollbar handle
Scrollbar.defineProperty(Scrollbar, "symbol", {default = " ", type = "string", canTriggerRender = true})
---@property backgroundSymbol string "\127" Symbol used for the scrollbar background
Scrollbar.defineProperty(Scrollbar, "symbolColor", {default = colors.gray, type = "number", canTriggerRender = true})
---@property symbolBackgroundColor color black Background color of the scrollbar handle
Scrollbar.defineProperty(Scrollbar, "symbolBackgroundColor", {default = colors.black, type = "number", canTriggerRender = true})
---@property backgroundSymbol string "\127" Symbol used for the scrollbar background
Scrollbar.defineProperty(Scrollbar, "backgroundSymbol", {default = "\127", type = "string", canTriggerRender = true})
---@property attachedElement table? nil The element this scrollbar is attached to
Scrollbar.defineProperty(Scrollbar, "attachedElement", {default = nil, type = "table"})
---@property attachedProperty string? nil The property being controlled
Scrollbar.defineProperty(Scrollbar, "attachedProperty", {default = nil, type = "string"})
---@property minValue number|function 0 Minimum value or function that returns it
Scrollbar.defineProperty(Scrollbar, "minValue", {default = 0, type = "number"})
---@property maxValue number|function 100 Maximum value or function that returns it
Scrollbar.defineProperty(Scrollbar, "maxValue", {default = 100, type = "number"})
---@property orientation string vertical Orientation of the scrollbar ("vertical" or "horizontal")
Scrollbar.defineProperty(Scrollbar, "orientation", {default = "vertical", type = "string", canTriggerRender = true})
---@property handleSize number 2 Size of the scrollbar handle in characters
Scrollbar.defineProperty(Scrollbar, "handleSize", {default = 2, type = "number", canTriggerRender = true})
Scrollbar.listenTo(Scrollbar, "mouse_click")
Scrollbar.listenTo(Scrollbar, "mouse_release")
Scrollbar.listenTo(Scrollbar, "mouse_drag")
Scrollbar.listenTo(Scrollbar, "mouse_scroll")
--- Creates a new Scrollbar instance
--- @shortDescription Creates a new Scrollbar instance
--- @return Scrollbar self The newly created Scrollbar instance
--- @usage local scrollbar = Scrollbar.new()
function Scrollbar.new()
local self = setmetatable({}, Scrollbar):__init()
self.set("width", 1)
self.set("height", 10)
return self
end
function Scrollbar:init(props, basalt)
VisualElement.init(self, props, basalt)
self.set("type", "Scrollbar")
return self
end
--- Attaches the scrollbar to an element's property
--- @param element BaseElement The element to attach to
--- @param config table Configuration {property = "propertyName", min = number|function, max = number|function}
--- @return Scrollbar self The scrollbar instance
function Scrollbar:attach(element, config)
self.set("attachedElement", element)
self.set("attachedProperty", config.property)
self.set("minValue", config.min or 0)
self.set("maxValue", config.max or 100)
return self
end
function Scrollbar:updateAttachedElement()
local element = self.get("attachedElement")
if not element then return end
local value = self.get("value")
local min = self.get("minValue")
local max = self.get("maxValue")
if type(min) == "function" then min = min() end
if type(max) == "function" then max = max() end
local mappedValue = min + (value / 100) * (max - min)
element.set(self.get("attachedProperty"), math.floor(mappedValue + 0.5))
end
local function getScrollbarSize(self)
return self.get("orientation") == "vertical" and self.get("height") or self.get("width")
end
local function getRelativeScrollPosition(self, x, y)
local relX, relY = self:getRelativePosition(x, y)
return self.get("orientation") == "vertical" and relY or relX
end
function Scrollbar:mouse_click(button, x, y)
if VisualElement.mouse_click(self, button, x, y) then
local size = getScrollbarSize(self)
local value = self.get("value")
local handleSize = self.get("handleSize")
local handlePos = math.floor((value / 100) * (size - handleSize)) + 1
local relPos = getRelativeScrollPosition(self, x, y)
if relPos >= handlePos and relPos < handlePos + handleSize then
self.dragOffset = relPos - handlePos
else
local newValue = ((relPos - 1) / (size - handleSize)) * 100
self.set("value", math.min(100, math.max(0, newValue)))
self:updateAttachedElement()
end
return true
end
end
function Scrollbar:mouse_drag(button, x, y)
if(VisualElement.mouse_drag(self, button, x, y))then
local size = getScrollbarSize(self)
local handleSize = self.get("handleSize")
local dragMultiplier = self.get("dragMultiplier")
local relPos = getRelativeScrollPosition(self, x, y)
relPos = math.max(1, math.min(size, relPos))
local newPos = relPos - (self.dragOffset or 0)
local newValue = (newPos - 1) / (size - handleSize) * 100 * dragMultiplier
self.set("value", math.min(100, math.max(0, newValue)))
self:updateAttachedElement()
return true
end
end
function Scrollbar:mouse_scroll(direction, x, y)
if not self:isInBounds(x, y) then return false end
direction = direction > 0 and -1 or 1
local step = self.get("step")
local currentValue = self.get("value")
local newValue = currentValue - direction * step
self.set("value", math.min(100, math.max(0, newValue)))
self:updateAttachedElement()
return true
end
function Scrollbar:render()
VisualElement.render(self)
local size = getScrollbarSize(self)
local value = self.get("value")
local handleSize = self.get("handleSize")
local symbol = self.get("symbol")
local symbolColor = self.get("symbolColor")
local symbolBackgroundColor = self.get("symbolBackgroundColor")
local bgSymbol = self.get("backgroundSymbol")
local isVertical = self.get("orientation") == "vertical"
local handlePos = math.floor((value / 100) * (size - handleSize)) + 1
for i = 1, size do
if isVertical then
self:blit(1, i, bgSymbol, tHex[self.get("foreground")], tHex[self.get("background")])
else
self:blit(i, 1, bgSymbol, tHex[self.get("foreground")], tHex[self.get("background")])
end
end
for i = handlePos, handlePos + handleSize - 1 do
if isVertical then
self:blit(1, i, symbol, tHex[symbolColor], tHex[symbolBackgroundColor])
else
self:blit(i, 1, symbol, tHex[symbolColor], tHex[symbolBackgroundColor])
end
end
end
return Scrollbar

View File

@@ -20,6 +20,8 @@ TextBox.defineProperty(TextBox, "scrollY", {default = 0, type = "number", canTri
TextBox.defineProperty(TextBox, "editable", {default = true, type = "boolean"})
---@property syntaxPatterns table {} Syntax highlighting patterns
TextBox.defineProperty(TextBox, "syntaxPatterns", {default = {}, type = "table"})
---@property cursorColor number nil Color of the cursor
TextBox.defineProperty(TextBox, "cursorColor", {default = nil, type = "number"})
TextBox.listenTo(TextBox, "mouse_click")
TextBox.listenTo(TextBox, "key")
@@ -41,7 +43,7 @@ end
--- Adds a new syntax highlighting pattern
--- @param pattern string The regex pattern to match
--- @param color color The color to apply
--- @param color colors The color to apply
function TextBox:addSyntaxPattern(pattern, color)
table.insert(self.get("syntaxPatterns"), {pattern = pattern, color = color})
return self
@@ -162,7 +164,14 @@ end
function TextBox:mouse_scroll(direction, x, y)
if self:isInBounds(x, y) then
local scrollY = self.get("scrollY")
self.set("scrollY", math.max(0, scrollY + direction))
local height = self.get("height")
local lines = self.get("lines")
local maxScroll = math.max(0, #lines - height + 2)
local newScroll = math.max(0, math.min(maxScroll, scrollY + direction))
self.set("scrollY", newScroll)
self:updateRender()
return true
end
@@ -182,16 +191,22 @@ function TextBox:mouse_click(button, x, y)
self.set("cursorY", targetY)
self.set("cursorX", math.min(relX + scrollX, #lines[targetY] + 1))
end
self:updateRender()
return true
end
return false
end
function TextBox:setText(text)
self.set("lines", {""})
for line in text:gmatch("[^\n]+") do
table.insert(self.get("lines"), line)
local lines = {}
if text == "" then
lines = {""}
else
for line in (text.."\n"):gmatch("([^\n]*)\n") do
table.insert(lines, line)
end
end
self.set("lines", lines)
return self
end
@@ -244,7 +259,7 @@ function TextBox:render()
local relativeX = self.get("cursorX") - scrollX
local relativeY = self.get("cursorY") - scrollY
if relativeX >= 1 and relativeX <= width and relativeY >= 1 and relativeY <= height then
self:setCursor(relativeX, relativeY, true)
self:setCursor(relativeX, relativeY, true, self.get("cursorColor") or self.get("foreground"))
end
end
end

View File

@@ -13,8 +13,10 @@ Tree.defineProperty(Tree, "nodes", {default = {}, type = "table", canTriggerRend
Tree.defineProperty(Tree, "selectedNode", {default = nil, type = "table", canTriggerRender = true})
---@property expandedNodes table {} Table of nodes that are currently expanded
Tree.defineProperty(Tree, "expandedNodes", {default = {}, type = "table", canTriggerRender = true})
---@property scrollOffset number 0 Current scroll position
---@property scrollOffset number 0 Current vertical scroll position
Tree.defineProperty(Tree, "scrollOffset", {default = 0, type = "number", canTriggerRender = true})
---@property horizontalOffset number 0 Current horizontal scroll position
Tree.defineProperty(Tree, "horizontalOffset", {default = 0, type = "number", canTriggerRender = true})
---@property nodeColor color white Color of unselected nodes
Tree.defineProperty(Tree, "nodeColor", {default = colors.white, type = "number"})
---@property selectedColor color lightBlue Background color of selected node
@@ -106,24 +108,24 @@ local function flattenTree(nodes, expandedNodes, level, result)
end
function Tree:mouse_click(button, x, y)
if not VisualElement.mouse_click(self, button, x, y) then return false end
if VisualElement.mouse_click(self, button, x, y) then
local relX, relY = self:getRelativePosition(x, y)
local flatNodes = flattenTree(self.get("nodes"), self.get("expandedNodes"))
local visibleIndex = relY + self.get("scrollOffset")
local relX, relY = self:getRelativePosition(x, y)
local flatNodes = flattenTree(self.get("nodes"), self.get("expandedNodes"))
local visibleIndex = relY + self.get("scrollOffset")
if flatNodes[visibleIndex] then
local nodeInfo = flatNodes[visibleIndex]
local node = nodeInfo.node
if flatNodes[visibleIndex] then
local nodeInfo = flatNodes[visibleIndex]
local node = nodeInfo.node
if relX <= nodeInfo.level * 2 + 2 then
self:toggleNode(node)
end
if relX <= nodeInfo.level * 2 + 2 then
self:toggleNode(node)
self.set("selectedNode", node)
self:fireEvent("node_select", node)
end
self.set("selectedNode", node)
self:fireEvent("node_select", node)
return true
end
return true
end
function Tree:onSelect(callback)
@@ -131,13 +133,25 @@ function Tree:onSelect(callback)
return self
end
function Tree:mouse_scroll(direction)
local flatNodes = flattenTree(self.get("nodes"), self.get("expandedNodes"))
local maxScroll = math.max(0, #flatNodes - self.get("height"))
local newScroll = math.min(maxScroll, math.max(0, self.get("scrollOffset") + direction))
function Tree:mouse_scroll(direction, x, y)
if VisualElement.mouse_scroll(self, direction, x, y) then
local flatNodes = flattenTree(self.get("nodes"), self.get("expandedNodes"))
local maxScroll = math.max(0, #flatNodes - self.get("height"))
local newScroll = math.min(maxScroll, math.max(0, self.get("scrollOffset") + direction))
self.set("scrollOffset", newScroll)
return true
self.set("scrollOffset", newScroll)
return true
end
end
function Tree:getNodeSize()
local width, height = 0, 0
local flatNodes = flattenTree(self.get("nodes"), self.get("expandedNodes"))
for _, nodeInfo in ipairs(flatNodes) do
width = math.max(width, nodeInfo.level + #nodeInfo.node.text)
end
height = #flatNodes
return width, height
end
function Tree:render()
@@ -148,6 +162,7 @@ function Tree:render()
local selectedNode = self.get("selectedNode")
local expandedNodes = self.get("expandedNodes")
local scrollOffset = self.get("scrollOffset")
local horizontalOffset = self.get("horizontalOffset")
for y = 1, height do
local nodeInfo = flatNodes[y + scrollOffset]
@@ -162,11 +177,10 @@ function Tree:render()
end
local bg = node == selectedNode and self.get("selectedColor") or self.get("background")
local text = indent .. symbol .." " .. (node.text or "Node")
text = sub(text, 1, self.get("width"))
local fullText = indent .. symbol .." " .. (node.text or "Node")
local text = sub(fullText, horizontalOffset + 1, horizontalOffset + self.get("width"))
self:textFg(1, y, text .. string.rep(" ", self.get("width") - #text), self.get("foreground"))
else
self:textFg(1, y, string.rep(" ", self.get("width")), self.get("foreground"), self.get("background"))
end

View File

@@ -30,6 +30,8 @@ VisualElement.defineProperty(VisualElement, "background", {default = colors.blac
VisualElement.defineProperty(VisualElement, "foreground", {default = colors.white, type = "number", canTriggerRender = true})
---@property clicked boolean false Whether the element is currently clicked
VisualElement.defineProperty(VisualElement, "clicked", {default = false, type = "boolean"})
---@property hover boolean false Whether the mouse is currently hover over the element (Craftos-PC only)
VisualElement.defineProperty(VisualElement, "hover", {default = false, type = "boolean"})
---@property backgroundEnabled boolean true Whether to render the background
VisualElement.defineProperty(VisualElement, "backgroundEnabled", {default = true, type = "boolean", canTriggerRender = true})
---@property focused boolean false Whether the element has input focus
@@ -81,6 +83,8 @@ VisualElement.combineProperties(VisualElement, "color", "foreground", "backgroun
VisualElement.listenTo(VisualElement, "focus")
VisualElement.listenTo(VisualElement, "blur")
VisualElement.listenTo(VisualElement, "mouse_enter", "mouse_move")
VisualElement.listenTo(VisualElement, "mouse_leave", "mouse_move")
local max, min = math.max, math.min
@@ -185,12 +189,12 @@ end
--- @param y number The y position of the release
--- @return boolean release Whether the element was released on the element
function VisualElement:mouse_up(button, x, y)
self.set("clicked", false)
if self:isInBounds(x, y) then
self:fireEvent("mouse_up", button, x, y)
self.set("clicked", false)
self:fireEvent("mouse_up", button, self:getRelativePosition(x, y))
return true
end
self:fireEvent("mouse_release", button, self:getRelativePosition(x, y))
return false
end
--- Handles a mouse release event
@@ -198,11 +202,42 @@ end
--- @param button number The button that was released
--- @param x number The x position of the release
--- @param y number The y position of the release
--- @return boolean release Whether the element was released on the element
function VisualElement:mouse_release(button, x, y)
if self.get("clicked") then
self:fireEvent("mouse_release", button, self:getRelativePosition(x, y))
self.set("clicked", false)
self:fireEvent("mouse_release", button, self:getRelativePosition(x, y))
self.set("clicked", false)
end
function VisualElement:mouse_move(_, x, y)
if(x==nil)or(y==nil)then
return
end
local hover = self.get("hover")
if(self:isInBounds(x, y))then
if(not hover)then
self.set("hover", true)
self:fireEvent("mouse_enter", self:getRelativePosition(x, y))
end
return true
else
if(hover)then
self.set("hover", false)
self:fireEvent("mouse_leave", self:getRelativePosition(x, y))
end
end
return false
end
function VisualElement:mouse_scroll(direction, x, y)
if(self:isInBounds(x, y))then
self:fireEvent("mouse_scroll", direction, self:getRelativePosition(x, y))
return true
end
return false
end
function VisualElement:mouse_drag(button, x, y)
if(self.get("clicked"))then
self:fireEvent("mouse_drag", button, self:getRelativePosition(x, y))
return true
end
return false
@@ -272,11 +307,11 @@ end
--- @param x number The x position of the cursor
--- @param y number The y position of the cursor
--- @param blink boolean Whether the cursor should blink
function VisualElement:setCursor(x, y, blink)
function VisualElement:setCursor(x, y, blink, color)
if self.parent then
local absX, absY = self:getAbsolutePosition(x, y)
absX = max(self.get("x"), min(absX, self.get("width") + self.get("x") - 1))
return self.parent:setCursor(absX, absY, blink)
return self.parent:setCursor(absX, absY, blink, color)
end
end

View File

@@ -1,6 +1,7 @@
local elementManager = require("elementManager")
local errorManager = require("errorManager")
local propertySystem = require("propertySystem")
local expect = require("libraries/expect")
--- This is the UI Manager and the starting point for your project. The following functions allow you to influence the default behavior of Basalt.
@@ -123,27 +124,42 @@ function basalt.setActiveFrame(frame)
mainFrame = frame
end
--- Schedules a function to be updated
--- @shortDescription Schedules a function to be updated
--- Schedules a function to run in a coroutine
--- @shortDescription Schedules a function to run in a coroutine
--- @function scheduleUpdate
--- @param func function The function to schedule
--- @return number Id The schedule ID
--- @return thread func The scheduled function
--- @usage local id = basalt.scheduleUpdate(myFunction)
function basalt.scheduleUpdate(func)
table.insert(basalt._schedule, func)
return #basalt._schedule
function basalt.schedule(func)
expect(1, func, "function")
local co = coroutine.create(func)
local ok, result = coroutine.resume(co)
if(ok)then
table.insert(basalt._schedule, {coroutine=co, filter=result})
else
errorManager.header = "Basalt Schedule Error"
errorManager.error(result)
end
return co
end
--- Removes a scheduled update
--- @shortDescription Removes a scheduled update
--- @function removeSchedule
--- @param id number The schedule ID to remove
--- @param func thread The scheduled function to remove
--- @return boolean success Whether the scheduled function was removed
--- @usage basalt.removeSchedule(scheduleId)
function basalt.removeSchedule(id)
basalt._schedule[id] = nil
function basalt.removeSchedule(func)
for i, v in ipairs(basalt._schedule) do
if(v.coroutine==func)then
table.remove(basalt._schedule, i)
return true
end
end
return false
end
---@private
local function updateEvent(event, ...)
if(event=="terminate")then basalt.stop() end
if lazyElementsEventHandler(event, ...) then return end
@@ -154,6 +170,19 @@ local function updateEvent(event, ...)
end
end
for _, func in ipairs(basalt._schedule) do
if(event==func.filter)then
local ok, result = coroutine.resume(func.coroutine, event, ...)
if(not ok)then
errorManager.header = "Basalt Schedule Error"
errorManager.error(result)
end
end
if(coroutine.status(func.coroutine)=="dead")then
basalt.removeSchedule(func.coroutine)
end
end
if basalt._events[event] then
for _, callback in ipairs(basalt._events[event]) do
callback(...)
@@ -161,22 +190,19 @@ local function updateEvent(event, ...)
end
end
---@private
local function renderFrames()
if(mainFrame)then
mainFrame:render()
end
end
--- Updates all scheduled functions
--- @shortDescription Updates all scheduled functions
--- Runs basalt once
--- @shortDescription Runs basalt once
--- @vararg any The event to run with
--- @usage basalt.update()
function basalt.update()
for k,v in pairs(basalt._schedule) do
if type(v)=="function" then
v()
end
end
function basalt.update(...)
updateEvent(...)
renderFrames()
end
--- Stops the Basalt runtime

View File

@@ -194,6 +194,9 @@ PropertySystem.addSetterHook(function(element, propertyName, value, config)
end
end)
--- This module provides reactive functionality for elements, it adds no new functionality for elements.
--- It is used to evaluate expressions in property values and update the element when the expression changes.
---@class Reactive
local BaseElement = {}
BaseElement.hooks = {

View File

@@ -1,4 +1,5 @@
local colorChars = require("libraries/colorHex")
local log = require("log")
--- This is the render module for Basalt. It tries to mimic the functionality of the `term` API. but with additional
--- functionality. It also has a buffer system to reduce the number of calls
@@ -13,6 +14,8 @@ local colorChars = require("libraries/colorHex")
local Render = {}
Render.__index = Render
local sub = string.sub
--- Creates a new Render object
--- @param terminal table The terminal object to render to
--- @return Render
@@ -66,9 +69,9 @@ function Render:blit(x, y, text, fg, bg)
error("Text, fg, and bg must be the same length")
end
self.buffer.text[y] = self.buffer.text[y]:sub(1,x-1) .. text .. self.buffer.text[y]:sub(x+#text)
self.buffer.fg[y] = self.buffer.fg[y]:sub(1,x-1) .. fg .. self.buffer.fg[y]:sub(x+#fg)
self.buffer.bg[y] = self.buffer.bg[y]:sub(1,x-1) .. bg .. self.buffer.bg[y]:sub(x+#bg)
self.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.buffer.bg[y] = sub(self.buffer.bg[y]:sub(1,x-1) .. bg .. self.buffer.bg[y]:sub(x+#bg), 1, self.width)
self:addDirtyRect(x, y, #text, 1)
return self
@@ -96,9 +99,9 @@ function Render:multiBlit(x, y, width, height, text, fg, bg)
for dy=0, height-1 do
local cy = y + dy
if cy >= 1 and cy <= self.height then
self.buffer.text[cy] = self.buffer.text[cy]:sub(1,x-1) .. text .. self.buffer.text[cy]:sub(x+#text)
self.buffer.fg[cy] = self.buffer.fg[cy]:sub(1,x-1) .. fg .. self.buffer.fg[cy]:sub(x+#fg)
self.buffer.bg[cy] = self.buffer.bg[cy]:sub(1,x-1) .. bg .. self.buffer.bg[cy]:sub(x+#bg)
self.buffer.text[cy] = sub(self.buffer.text[cy]:sub(1,x-1) .. text .. self.buffer.text[cy]:sub(x+#text), 1, self.width)
self.buffer.fg[cy] = sub(self.buffer.fg[cy]:sub(1,x-1) .. fg .. self.buffer.fg[cy]:sub(x+#fg), 1, self.width)
self.buffer.bg[cy] = sub(self.buffer.bg[cy]:sub(1,x-1) .. bg .. self.buffer.bg[cy]:sub(x+#bg), 1, self.width)
end
end
@@ -115,9 +118,10 @@ end
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] = 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.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)
return self
@@ -133,8 +137,8 @@ function Render:textBg(x, y, text, bg)
if y < 1 or y > self.height then return self end
bg = colorChars[bg] or "f"
self.buffer.text[y] = self.buffer.text[y]:sub(1,x-1) .. text .. self.buffer.text[y]:sub(x+#text)
self.buffer.bg[y] = self.buffer.bg[y]:sub(1,x-1) .. bg:rep(#text) .. self.buffer.bg[y]:sub(x+#text)
self.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.bg[y] = sub(self.buffer.bg[y]:sub(1,x-1) .. bg:rep(#text) .. self.buffer.bg[y]:sub(x+#text), 1, self.width)
self:addDirtyRect(x, y, #text, 1)
return self
@@ -148,7 +152,7 @@ end
function Render:text(x, y, text)
if y < 1 or y > self.height then return self end
self.buffer.text[y] = self.buffer.text[y]:sub(1,x-1) .. text .. self.buffer.text[y]:sub(x+#text)
self.buffer.text[y] = sub(self.buffer.text[y]:sub(1,x-1) .. text .. self.buffer.text[y]:sub(x+#text), 1, self.width)
self:addDirtyRect(x, y, #text, 1)
return self
@@ -162,7 +166,7 @@ end
function Render:fg(x, y, fg)
if y < 1 or y > self.height then return self end
self.buffer.fg[y] = self.buffer.fg[y]:sub(1,x-1) .. fg .. self.buffer.fg[y]:sub(x+#fg)
self.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, #fg, 1)
return self
@@ -176,7 +180,7 @@ end
function Render:bg(x, y, bg)
if y < 1 or y > self.height then return self end
self.buffer.bg[y] = self.buffer.bg[y]:sub(1,x-1) .. bg .. self.buffer.bg[y]:sub(x+#bg)
self.buffer.bg[y] = sub(self.buffer.bg[y]:sub(1,x-1) .. bg .. self.buffer.bg[y]:sub(x+#bg), 1, self.width)
self:addDirtyRect(x, y, #bg, 1)
return self
@@ -230,6 +234,7 @@ function Render:render()
self.buffer.dirtyRects = {}
if self.blink then
self.terminal.setTextColor(self.cursorColor)
self.terminal.setCursorPos(self.xCursor, self.yCursor)
self.terminal.setCursorBlink(true)
else
@@ -271,12 +276,14 @@ end
--- @param y number The y position of the cursor
--- @param blink boolean Whether the cursor should blink
--- @return Render
function Render:setCursor(x, y, blink)
function Render:setCursor(x, y, blink, color)
if color ~= nil then self.terminal.setTextColor(color) end
self.terminal.setCursorPos(x, y)
self.terminal.setCursorBlink(blink)
self.xCursor = x
self.yCursor = y
self.blink = blink
self.cursorColor = color
return self
end
@@ -306,4 +313,15 @@ function Render:getSize()
return self.width, self.height
end
function Render:setSize(width, height)
self.width = width
self.height = height
for y=1, self.height do
self.buffer.text[y] = string.rep(" ", self.width)
self.buffer.fg[y] = string.rep("0", self.width)
self.buffer.bg[y] = string.rep("f", self.width)
end
return self
end
return Render