From 0326cc12c7a661966b560dabeffe544d8e3ad16d Mon Sep 17 00:00:00 2001 From: Robert Jelic <36573031+NoryiE@users.noreply.github.com> Date: Tue, 18 Feb 2025 19:13:51 +0100 Subject: [PATCH] Lot of bug fixxes --- generate-docs.lua | 39 ------- src/elements/BaseElement.lua | 13 +-- src/elements/BaseFrame.lua | 20 +++- src/elements/Container.lua | 50 +++++++-- src/elements/Input.lua | 6 +- src/elements/Scrollbar.lua | 180 +++++++++++++++++++++++++++++++++ src/elements/TextBox.lua | 27 +++-- src/elements/Tree.lua | 60 ++++++----- src/elements/VisualElement.lua | 53 ++++++++-- src/main.lua | 64 ++++++++---- src/plugins/reactive.lua | 3 + src/render.lua | 46 ++++++--- 12 files changed, 435 insertions(+), 126 deletions(-) delete mode 100644 generate-docs.lua create mode 100644 src/elements/Scrollbar.lua diff --git a/generate-docs.lua b/generate-docs.lua deleted file mode 100644 index d3a85c6..0000000 --- a/generate-docs.lua +++ /dev/null @@ -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") diff --git a/src/elements/BaseElement.lua b/src/elements/BaseElement.lua index 222a591..f3669d8 100644 --- a/src/elements/BaseElement.lua +++ b/src/elements/BaseElement.lua @@ -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 diff --git a/src/elements/BaseFrame.lua b/src/elements/BaseFrame.lua index 4f84db6..820c854 100644 --- a/src/elements/BaseFrame.lua +++ b/src/elements/BaseFrame.lua @@ -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 \ No newline at end of file diff --git a/src/elements/Container.lua b/src/elements/Container.lua index 2d7a717..fe88780 100644 --- a/src/elements/Container.lua +++ b/src/elements/Container.lua @@ -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 diff --git a/src/elements/Input.lua b/src/elements/Input.lua index 3204376..e86175b 100644 --- a/src/elements/Input.lua +++ b/src/elements/Input.lua @@ -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 diff --git a/src/elements/Scrollbar.lua b/src/elements/Scrollbar.lua new file mode 100644 index 0000000..28ca443 --- /dev/null +++ b/src/elements/Scrollbar.lua @@ -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 diff --git a/src/elements/TextBox.lua b/src/elements/TextBox.lua index 33300e2..7e098d5 100644 --- a/src/elements/TextBox.lua +++ b/src/elements/TextBox.lua @@ -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 diff --git a/src/elements/Tree.lua b/src/elements/Tree.lua index c7f066a..cf0e84a 100644 --- a/src/elements/Tree.lua +++ b/src/elements/Tree.lua @@ -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 diff --git a/src/elements/VisualElement.lua b/src/elements/VisualElement.lua index 7a9ba68..46c1b92 100644 --- a/src/elements/VisualElement.lua +++ b/src/elements/VisualElement.lua @@ -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 diff --git a/src/main.lua b/src/main.lua index 8fef606..7f076ab 100644 --- a/src/main.lua +++ b/src/main.lua @@ -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 diff --git a/src/plugins/reactive.lua b/src/plugins/reactive.lua index c97f768..bdc22fa 100644 --- a/src/plugins/reactive.lua +++ b/src/plugins/reactive.lua @@ -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 = { diff --git a/src/render.lua b/src/render.lua index 325547f..bd5bc9c 100644 --- a/src/render.lua +++ b/src/render.lua @@ -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 \ No newline at end of file