diff --git a/examples/benchmarks.lua b/examples/benchmarks.lua index f562272..85dff12 100644 --- a/examples/benchmarks.lua +++ b/examples/benchmarks.lua @@ -1,4 +1,4 @@ -local basalt = require("src") +local basalt = require("basalt") local main = basalt.getMainFrame() local btn = main:addButton() diff --git a/examples/scrollableFrame.lua b/examples/scrollableFrame.lua new file mode 100644 index 0000000..63589e8 --- /dev/null +++ b/examples/scrollableFrame.lua @@ -0,0 +1,57 @@ +local basalt = require("basalt") + +local main = basalt.getMainFrame() + +-- Vertical scrolling +-- This function gets the overall height of all the children in a container +local function getChildrenHeight(container) + local height = 0 + for _, child in ipairs(container.get("children")) do + if(child.get("visible"))then + local newHeight = child.get("y") + child.get("height") + if newHeight > height then + height = newHeight + end + end + end + return height +end + +-- Create a new frame +local scrollingFrame = main:addFrame({width = 20, height = 10, x = 2, y = 2, backgroundColor = colors.gray}) + +-- Add a scroll function to the frame +scrollingFrame:onScroll(function(self, delta) + local offset = math.max(0, math.min(self.get("offsetY") + delta, getChildrenHeight(self) - self.get("height"))) + self:setOffsetY(offset) +end) + +scrollingFrame:addButton({text = "Button 1", x=2, y = 2}) +scrollingFrame:addButton({text = "Button 2", x=2, y = 6}) +scrollingFrame:addButton({text = "Button 3", x=2, y = 10}) + +-- Horizontal scrolling +local function getChildrenWidth(container) + local width = 0 + for _, child in ipairs(container.get("children")) do + if(child.get("visible"))then + local newWidth = child.get("x") + child.get("width") + if newWidth > width then + width = newWidth + end + end + end + return width +end + +local scrollingFrame2 = main:addFrame({width = 20, height = 10, x = 25, y = 2, backgroundColor = colors.gray}) +scrollingFrame2:onScroll(function(self, delta) + local offset = math.max(0, math.min(self.get("offsetX") + delta, getChildrenWidth(self) - self.get("width"))) + self:setOffsetX(offset) +end) + +scrollingFrame2:addButton({text = "Button 1", x=2, y = 2}) +scrollingFrame2:addButton({text = "Button 2", x=13, y = 2}) +scrollingFrame2:addButton({text = "Button 3", x=24, y = 2}) + +basalt.run() \ No newline at end of file diff --git a/examples/states.lua b/examples/states.lua index bae9717..a31cf9f 100644 --- a/examples/states.lua +++ b/examples/states.lua @@ -1,4 +1,4 @@ -local basalt = require("src") +local basalt = require("basalt") -- Create main frame local main = basalt.getMainFrame() diff --git a/src/elements/Input.lua b/src/elements/Input.lua index 7168fcf..2227255 100644 --- a/src/elements/Input.lua +++ b/src/elements/Input.lua @@ -196,6 +196,10 @@ function Input:render() return end + if(focused) then + self:setCursor(self.get("cursorPos") - viewOffset, 1, true, self.get("cursorColor") or self.get("foreground")) + end + local visibleText = text:sub(viewOffset + 1, viewOffset + width) self:textFg(1, 1, visibleText, self.get("foreground")) end diff --git a/src/elements/VisualElement.lua b/src/elements/VisualElement.lua index 05ea402..3ea825c 100644 --- a/src/elements/VisualElement.lua +++ b/src/elements/VisualElement.lua @@ -410,6 +410,10 @@ end function VisualElement:setCursor(x, y, blink, color) if self.parent then local xPos, yPos = self:calculatePosition() + if(x + xPos - 1<1)or(x + xPos - 1>self.parent.get("width"))or + (y + yPos - 1<1)or(y + yPos - 1>self.parent.get("height"))then + return self.parent:setCursor(x + xPos - 1, y + yPos - 1, false) + end return self.parent:setCursor(x + xPos - 1, y + yPos - 1, blink, color) end return self diff --git a/src/plugins/xml.lua b/src/plugins/xml.lua index 9e658e5..d7c383f 100644 --- a/src/plugins/xml.lua +++ b/src/plugins/xml.lua @@ -1,76 +1,252 @@ 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 XMLParser = {} +local TokenType = { + TAG_OPEN = "TAG_OPEN", + TAG_CLOSE = "TAG_CLOSE", + TAG_SELF_CLOSE = "TAG_SELF_CLOSE", + ATTRIBUTE = "ATTRIBUTE", + TEXT = "TEXT", + CDATA = "CDATA", + COMMENT = "COMMENT" +} -local function parseXML(self, xmlString) - local stack = {} - local root = {children = {}} - local current = root - local inCDATA = false - local cdataContent = "" +local function tokenize(xml) + local tokens = {} + local position = 1 + local lineNumber = 1 - for line in xmlString:gmatch("[^\r\n]+") do - line = line:match("^%s*(.-)%s*$") + while position <= #xml do + local char = xml:sub(position, position) - if 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 char:match("%s") then + position = position + 1 + elseif xml:sub(position, position + 8) == "", position + 9) + if not endPos then errorManager.error("Unclosed CDATA section") end + table.insert(tokens, { + type = TokenType.CDATA, + value = xml:sub(position + 9, endPos - 1) + }) + position = endPos + 3 + elseif xml:sub(position, position + 3) == "", position + 4) + if not endPos then errorManager.error("Unclosed comment") end + table.insert(tokens, { + type = TokenType.COMMENT, + value = xml:sub(position + 4, endPos - 1) + }) + position = endPos + 3 + elseif char == "<" then + if xml:sub(position + 1, position + 1) == "/" then + local endPos = xml:find(">", position) + if not endPos then errorManager.error("Unclosed tag") end + table.insert(tokens, { + type = TokenType.TAG_CLOSE, + value = xml:sub(position + 2, endPos - 1):match("^%s*(.-)%s*$") + }) + position = endPos + 1 + else + local tagContent = "" + position = position + 1 + local selfClosing = false - if not line:match("/>$") then - table.insert(stack, current) - current = tag + while position <= #xml do + char = xml:sub(position, position) + if char == ">" then + table.insert(tokens, { + type = selfClosing and TokenType.TAG_SELF_CLOSE or TokenType.TAG_OPEN, + value = tagContent:match("^%s*(.-)%s*$") + }) + position = position + 1 + break + elseif char == "/" and xml:sub(position + 1, position + 1) == ">" then + + table.insert(tokens, { + type = TokenType.TAG_SELF_CLOSE, + value = tagContent:match("^%s*(.-)%s*$") + }) + position = position + 2 + break + elseif char == "/" and xml:sub(position - 1, position - 1):match("%s") then + + selfClosing = true + else + tagContent = tagContent .. char + end + position = position + 1 + end end - elseif line:match("^, got ", + current.name, token.value)) + end + table.remove(stack) + current = stack[#stack] + + elseif token.type == TokenType.TEXT then + table.insert(current.children, { + name = "#text", + value = token.value, + line = token.line + }) + + elseif token.type == TokenType.CDATA then + table.insert(current.children, { + name = "#cdata", + value = token.value, + line = token.line + }) + + elseif token.type == TokenType.COMMENT then + table.insert(current.children, { + name = "#comment", + value = token.value, + line = token.line + }) + end + + i = i + 1 + end + return root end +function XMLParser.parse(xmlString) + local tokens = tokenize(xmlString) + return parse(tokens) +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}) + if not expr then return expr end - local func, err = load("return " .. e, "expression", "t", env) - if not func then - errorManager.error("Failed to parse expression: " .. err) + if expr:match("^%${.*}$") then + local inner = expr:match("^%${(.*)}$") + if inner:match("^[%w_]+$") then + if scope and scope[inner] then + return scope[inner] + else + errorManager.error(string.format('Variable "%s" not found in scope', inner)) + return expr end - return tostring(func()) - end) + end + + local env = setmetatable({}, { + __index = function(_, k) + if scope and scope[k] then + return scope[k] + elseif _ENV[k] then + return _ENV[k] + else + error(string.format('Variable "%s" not found in scope', k)) + end + end + }) + + local func, err = load("return " .. inner, "expression", "t", env) + if not func then + errorManager.error("Failed to parse expression: " .. err) + return expr + end + + local ok, result = pcall(func) + if not ok then + errorManager.error("Failed to evaluate expression: " .. result) + return expr + end + + return result 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() + return expr:gsub("%${([^}]+)}", function(e) + if e:match("^[%w_]+$") then + if scope and scope[e] then + return tostring(scope[e]) + else + errorManager.error(string.format('Variable "%s" not found in scope', e)) + return e + end + end + 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) + return e + end + return tostring(func()) + end) end local function convertValue(value, propertyType, scope) @@ -103,55 +279,179 @@ local function convertValue(value, propertyType, scope) 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]) +local actionHandlers = { + setProperty = function(node, element, scope) + return function(...) + local target = node.attributes.target or "self" + local targetElement + + if target == "self" then + targetElement = element + elseif target == "parent" then + targetElement = element.parent + else + targetElement = element:getBaseFrame():getChild(target) end + + if not targetElement then + errorManager.error(string.format('Target "%s" not found', target)) + return + end + + local property = node.attributes.property + local propertyConfig = targetElement:getPropertyConfig(property) + if not propertyConfig then + errorManager.error(string.format('Unknown property "%s"', property)) + return + end + + local value = convertValue(node.attributes.value, propertyConfig.type, scope) + targetElement.set(property, value) + end + end, + + execute = function(node, element, scope) + return function(...) + local funcName = node.attributes["function"] + if not scope[funcName] then + errorManager.error(string.format('Function "%s" not found in scope', funcName)) + return + end + scope[funcName](element, ...) + end + end, + + setValue = function(node, element, scope) + return function(...) + local name = node.attributes.name + local value = convertValue(node.attributes.value, "string", scope) + scope[name] = value + end + end +} + +local eventParameters = { + onClick = {"self", "button", "x", "y"}, + onScroll = {"self", "direction", "x", "y"}, + onDrag = {"self", "button", "x", "y"}, + onKey = {"self", "key"}, + onChar = {"self", "char"}, + onKeyUp = {"self", "key"}, + +} + +local function handleEvent(node, element, scope) + local isEventNode = node.name:match("^on%u") + if not isEventNode then return end + + local eventName = node.name:sub(3,3):lower() .. node.name:sub(4) + local handlers = {} + + for _, child in ipairs(node.children or {}) do + if child.name == "#cdata" then + if not isEventNode then + errorManager.error("CDATA blocks can only be used inside event tags") + return + end + + local eventName = node.name:sub(3) + local params = eventParameters["on"..eventName] or {"self"} + + local paramString = table.concat(params, ", ") + + local codeTemplate = [[ + return function(%s) + %s + end + ]] + + local env = {} + if scope then + for k,v in pairs(scope) do + env[k] = v + end + end + + env.colors = colors + env.term = term + env.math = math + + local code = child.value:gsub("^%s+", ""):gsub("%s+$", "") + local finalCode = string.format(codeTemplate, paramString, code) + + local func, err = load(finalCode, "event", "t", env) + if err then + errorManager.error("Failed to parse event: " .. err) + elseif func then + local eventName = node.name:sub(3,3):lower() .. node.name:sub(4) + element["on"..eventName:sub(1,1):upper()..eventName:sub(2)](element, func()) + end + elseif child.name ~= "#text" then + local handler = actionHandlers[child.name] + if not handler then + errorManager.error(string.format('Unknown action tag "%s"', child.name)) + return + end + table.insert(handlers, handler(child, element, scope)) 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 + if #handlers > 0 then + element["on"..eventName:sub(1,1):upper()..eventName:sub(2)](element, function(...) + for _, handler in ipairs(handlers) do + handler(...) end - end + end) end end ---- The XML plugin provides XML parsing and UI creation from XML markup ----@class BaseElement -local BaseElement = {} +local function parsePropertyTag(node, element, scope) + local propertyConfig = element:getPropertyConfig(node.name) ---- Creates an element from an XML node ---- @shortDescription Creates element from XML node ---- @param self BaseElement The element instance ---- @param node table The XML node to create from ---- @return BaseElement self The element instance -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) + if propertyConfig then + if propertyConfig.type == "table" then + local tableData = {} + + for _, child in ipairs(node.children) do + if child.name == "item" or child.name == "entry" then + local entry = {} + + for attr, value in pairs(child.attributes) do + entry[attr] = convertValue(value, "string", scope) + end + + for _, prop in ipairs(child.children) do + if prop.name ~= "#text" and prop.name ~= "#cdata" then + if prop.children and #prop.children > 0 then + local firstChild = prop.children[1] + if firstChild.name == "#text" then + entry[prop.name] = convertValue(firstChild.value, "string", scope) + end + else + local subEntry = {} + for subAttr, subValue in pairs(prop.attributes) do + subEntry[subAttr] = convertValue(subValue, "string", scope) + end + entry[prop.name] = next(subEntry) and subEntry or "" + end + end + end + + table.insert(tableData, entry) + end + end + + element.set(node.name, tableData) + return true + else + local textNode = node.children[1] + if textNode and textNode.name == "#text" then + element.set(node.name, convertValue(textNode.value, propertyConfig.type, scope)) + return true + end end end - return self + return false end ---@class Container @@ -171,27 +471,45 @@ local Container = {} --- ]] --- container:loadXML(xml) function Container:loadXML(content, scope) - local tree = parseXML(self, content) + scope = scope or {} + local tree = XMLParser.parse(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) + if node.name:sub(1,1) ~= "#" then + if node.name:match("^on") then + handleEvent(node, parent, scope) + else + local handled = parsePropertyTag(node, parent, scope) - 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) + if not handled then + local elementType = node.name:sub(1,1):upper() .. node.name:sub(2) + local addMethod = "add"..elementType + + if not parent[addMethod] then + local parentType = parent.get and parent.get("type") or "Unknown" + errorManager.error(string.format( + 'Tag <%s> is not valid inside <%s>', + node.name, parentType:lower() + )) + return + end + + local element = parent[addMethod](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 + + if #node.children > 0 then + createElements(node, element, scope) + end end end - - handleEvent(node, element, scope) - - if #node.children > 0 then - createElements(node, element, scope) - end end end end @@ -201,7 +519,5 @@ function Container:loadXML(content, scope) end return { - BaseElement = BaseElement, Container = Container -} - +} \ No newline at end of file