From d36c7e78672fb72bb766d090aeeebbf6b6d79d6d Mon Sep 17 00:00:00 2001 From: Robert Jelic <36573031+NoryiE@users.noreply.github.com> Date: Sun, 20 Apr 2025 10:27:48 +0200 Subject: [PATCH] - Added Timer - Fixed BaseElement Visual request issues - Added number animation - Added entries animation --- src/elements/Container.lua | 1 + src/elements/Flexbox.lua | 799 ++++++++++++++++++++++++++++++------- src/elements/Graph.lua | 2 +- src/elements/Table.lua | 33 +- src/elements/TextBox.lua | 2 +- src/elements/Timer.lua | 83 ++++ src/plugins/animation.lua | 38 ++ src/plugins/reactive.lua | 4 +- src/plugins/theme.lua | 2 - 9 files changed, 806 insertions(+), 158 deletions(-) create mode 100644 src/elements/Timer.lua diff --git a/src/elements/Container.lua b/src/elements/Container.lua index a9cecb6..32b0350 100644 --- a/src/elements/Container.lua +++ b/src/elements/Container.lua @@ -109,6 +109,7 @@ end --- @param child table The child to check --- @return boolean boolean the child is visible function Container:isChildVisible(child) + if not child:isType("VisualElement") then return false end if(child.get("visible") == false)then return false end if(child._destroyed)then return false end local containerW, containerH = self.get("width"), self.get("height") diff --git a/src/elements/Flexbox.lua b/src/elements/Flexbox.lua index b73751a..c953241 100644 --- a/src/elements/Flexbox.lua +++ b/src/elements/Flexbox.lua @@ -31,6 +31,19 @@ Flexbox.defineProperty(Flexbox, "flexJustifyContent", { return value end }) +---@property flexAlignItems string "flex-start" The alignment of flex items along the cross axis +Flexbox.defineProperty(Flexbox, "flexAlignItems", { + default = "flex-start", + type = "string", + setter = function(self, value) + if not value:match("^flex%-") and value ~= "stretch" then + value = "flex-" .. value + end + return value + end +}) +---@property flexCrossPadding number 0 The padding on both sides of the cross axis +Flexbox.defineProperty(Flexbox, "flexCrossPadding", {default = 0, type = "number"}) ---@property flexWrap boolean false Whether to wrap flex items onto multiple lines ---@property flexUpdateLayout boolean false Whether to update the layout of the flexbox Flexbox.defineProperty(Flexbox, "flexWrap", {default = false, type = "boolean"}) @@ -55,189 +68,650 @@ local lineBreakElement = { getVisible = function(self) return true end, } - local function sortElements(self, direction, spacing, wrap) - local elements = self.get("children") local sortedElements = {} - if not(wrap)then - local index = 1 - local lineSize = 1 - local lineOffset = 1 - for _,v in pairs(elements)do - if(sortedElements[index]==nil)then sortedElements[index]={offset=1} end - - local childHeight = direction == "row" and v.get("height") or v.get("width") - if childHeight > lineSize then - lineSize = childHeight - end - if(v == lineBreakElement)then - lineOffset = lineOffset + lineSize + spacing - lineSize = 1 - index = index + 1 - sortedElements[index] = {offset=lineOffset} - else - table.insert(sortedElements[index], v) - end - end - elseif(wrap)then - local lineSize = 1 - local lineOffset = 1 - - local maxSize = direction == "row" and self.get("width") or self.get("height") - local usedSize = 0 - local index = 1 - - for _,v in pairs(elements) do - if(sortedElements[index]==nil) then sortedElements[index]={offset=1} end - - if v:getType() == "lineBreak" then - lineOffset = lineOffset + lineSize + spacing - usedSize = 0 - lineSize = 1 - index = index + 1 - sortedElements[index] = {offset=lineOffset} - else - local objSize = direction == "row" and v.get("width") or v.get("height") - if(objSize+usedSize<=maxSize) then - table.insert(sortedElements[index], v) - usedSize = usedSize + objSize + spacing - else - lineOffset = lineOffset + lineSize + spacing - lineSize = direction == "row" and v.get("height") or v.get("width") - index = index + 1 - usedSize = objSize + spacing - sortedElements[index] = {offset=lineOffset, v} - end - - local childHeight = direction == "row" and v.get("height") or v.get("width") - if childHeight > lineSize then - lineSize = childHeight - end - end - end - end - return sortedElements + local visibleElements = {} + local childCount = 0 + + -- We can't use self.get("visibleChildren") here + --because it would exclude elements that are obscured + for _, elem in pairs(self.get("children")) do + if elem.get("visible") then + table.insert(visibleElements, elem) + if elem ~= lineBreakElement then + childCount = childCount + 1 + end + end + end + + + if childCount == 0 then + return sortedElements + end + + if not wrap then + sortedElements[1] = {offset=1} + + for _, elem in ipairs(visibleElements) do + if elem == lineBreakElement then + local nextIndex = #sortedElements + 1 + if sortedElements[nextIndex] == nil then + sortedElements[nextIndex] = {offset=1} + end + else + table.insert(sortedElements[#sortedElements], elem) + end + end + else + local containerSize = direction == "row" and self.get("width") or self.get("height") + + local segments = {{}} + local currentSegment = 1 + + for _, elem in ipairs(visibleElements) do + if elem == lineBreakElement then + currentSegment = currentSegment + 1 + segments[currentSegment] = {} + else + table.insert(segments[currentSegment], elem) + end + end + + for segmentIndex, segment in ipairs(segments) do + if #segment == 0 then + sortedElements[#sortedElements + 1] = {offset=1} + else + local rows = {} + local currentRow = {} + local currentWidth = 0 + + for _, elem in ipairs(segment) do + local intrinsicSize = 0 + local currentSize = direction == "row" and elem.get("width") or elem.get("height") + + local hasIntrinsic = false + if direction == "row" then + local ok, intrinsicWidth = pcall(function() return elem.get("intrinsicWidth") end) + if ok and intrinsicWidth then + intrinsicSize = intrinsicWidth + hasIntrinsic = true + end + else + local ok, intrinsicHeight = pcall(function() return elem.get("intrinsicHeight") end) + if ok and intrinsicHeight then + intrinsicSize = intrinsicHeight + hasIntrinsic = true + end + end + + local elemSize = hasIntrinsic and intrinsicSize or currentSize + + local spaceNeeded = elemSize + + if #currentRow > 0 then + spaceNeeded = spaceNeeded + spacing + end + + if currentWidth + spaceNeeded <= containerSize or #currentRow == 0 then + table.insert(currentRow, elem) + currentWidth = currentWidth + spaceNeeded + else + table.insert(rows, currentRow) + currentRow = {elem} + currentWidth = elemSize + end + end + + if #currentRow > 0 then + table.insert(rows, currentRow) + end + + for _, row in ipairs(rows) do + sortedElements[#sortedElements + 1] = {offset=1} + for _, elem in ipairs(row) do + table.insert(sortedElements[#sortedElements], elem) + end + end + end + end + end + + local filteredElements = {} + for i, rowOrColumn in ipairs(sortedElements) do + if #rowOrColumn > 0 then + table.insert(filteredElements, rowOrColumn) + end + end + + return filteredElements end local function calculateRow(self, children, spacing, justifyContent) - local containerWidth = self.get("width") - - local usedSpace = spacing * (#children - 1) - local totalFlexGrow = 0 - + -- Make a copy of children that filters out lineBreak elements + local filteredChildren = {} for _, child in ipairs(children) do if child ~= lineBreakElement then - usedSpace = usedSpace + child.get("width") - totalFlexGrow = totalFlexGrow + child.get("flexGrow") + table.insert(filteredChildren, child) end end - - local remainingSpace = containerWidth - usedSpace - local extraSpacePerUnit = totalFlexGrow > 0 and (remainingSpace / totalFlexGrow) or 0 - local distributedSpace = 0 - - local currentX = 1 - for i, child in ipairs(children) do - if child ~= lineBreakElement then - local childWidth = child.get("width") - - if child.get("flexGrow") > 0 then - - if i == #children then - local extraSpace = remainingSpace - distributedSpace - childWidth = childWidth + extraSpace - else - local extraSpace = math.floor(extraSpacePerUnit * child.get("flexGrow")) - childWidth = childWidth + extraSpace - distributedSpace = distributedSpace + extraSpace + + -- Skip processing if no children + if #filteredChildren == 0 then + return + end + + local containerWidth = self.get("width") + local containerHeight = self.get("height") + local alignItems = self.get("flexAlignItems") + local crossPadding = self.get("flexCrossPadding") + local wrap = self.get("flexWrap") + + -- Safety check + if containerWidth <= 0 then return end + + -- Calculate available cross axis space (considering padding) + local availableCrossAxisSpace = containerHeight - (crossPadding * 2) + if availableCrossAxisSpace < 1 then + availableCrossAxisSpace = containerHeight + crossPadding = 0 + end + + -- Cache local variables to reduce function calls + local max = math.max + local min = math.min + local floor = math.floor + local ceil = math.ceil + + -- Categorize elements and calculate their minimal widths and flexibilities + local totalFixedWidth = 0 + local totalFlexGrow = 0 + local minWidths = {} + local flexGrows = {} + local flexShrinks = {} + + -- First pass: collect fixed widths and flex properties + for _, child in ipairs(filteredChildren) do + local grow = child.get("flexGrow") or 0 + local shrink = child.get("flexShrink") or 0 + local width = child.get("width") + + -- Track element properties + flexGrows[child] = grow + flexShrinks[child] = shrink + minWidths[child] = width + + -- Calculate total flex grow factor + if grow > 0 then + totalFlexGrow = totalFlexGrow + grow + else + -- If not flex grow, it's a fixed element + totalFixedWidth = totalFixedWidth + width + end + end + + -- Calculate total spacing + local elementsCount = #filteredChildren + local totalSpacing = (elementsCount > 1) and ((elementsCount - 1) * spacing) or 0 + + -- Calculate available space for flex items + local availableSpace = containerWidth - totalFixedWidth - totalSpacing + + -- Second pass: distribute available space to flex-grow items + if availableSpace > 0 and totalFlexGrow > 0 then + -- Container has extra space - distribute according to flex-grow + for _, child in ipairs(filteredChildren) do + local grow = flexGrows[child] + if grow > 0 then + -- Calculate flex basis (never less than minWidth) + local minWidth = minWidths[child] + local flexWidth = floor((grow / totalFlexGrow) * availableSpace) + + -- Set calculated width, ensure it's at least 1 + child.set("width", max(flexWidth, 1)) + end + end + elseif availableSpace < 0 then + -- Container doesn't have enough space - check for shrinkable items + local totalFlexShrink = 0 + local shrinkableItems = {} + + -- Find shrinkable items + for _, child in ipairs(filteredChildren) do + local shrink = flexShrinks[child] + if shrink > 0 then + totalFlexShrink = totalFlexShrink + shrink + table.insert(shrinkableItems, child) + end + end + + -- If we have shrinkable items, shrink them proportionally + if totalFlexShrink > 0 and #shrinkableItems > 0 then + local excessWidth = -availableSpace + + for _, child in ipairs(shrinkableItems) do + local width = child.get("width") + local shrink = flexShrinks[child] + local proportion = shrink / totalFlexShrink + local reduction = ceil(excessWidth * proportion) + + -- Ensure width doesn't go below 1 + child.set("width", max(1, width - reduction)) + end + end + + -- Recalculate fixed widths after shrinking + totalFixedWidth = 0 + for _, child in ipairs(filteredChildren) do + totalFixedWidth = totalFixedWidth + child.get("width") + end + + -- If we still have flex-grow items, ensure they have proportional space + if totalFlexGrow > 0 then + local growableItems = {} + local totalGrowableInitialWidth = 0 + + -- Find growable items + for _, child in ipairs(filteredChildren) do + if flexGrows[child] > 0 then + table.insert(growableItems, child) + totalGrowableInitialWidth = totalGrowableInitialWidth + child.get("width") + end + end + + -- Ensure flexGrow items get at least some width, even if space is tight + if #growableItems > 0 and totalGrowableInitialWidth > 0 then + -- Minimum guaranteed width for flex items (at least 20% of container) + local minFlexSpace = max(floor(containerWidth * 0.2), #growableItems) + + -- Reserve space for flex items + local reservedFlexSpace = min(minFlexSpace, containerWidth - totalSpacing) + + -- Distribute among flex items + for _, child in ipairs(growableItems) do + local grow = flexGrows[child] + local proportion = grow / totalFlexGrow + local flexWidth = max(1, floor(reservedFlexSpace * proportion)) + child.set("width", flexWidth) end end - - child.set("x", currentX) - child.set("y", children.offset or 1) - child.set("width", childWidth) - currentX = currentX + childWidth + spacing end end - - if justifyContent == "flex-end" then - local offset = containerWidth - (currentX - spacing - 1) - for _, child in ipairs(children) do - child.set("x", child.get("x") + offset) + + -- Step 3: Position elements (never allow overlapping) + local currentX = 1 + + -- Place all elements sequentially + for _, child in ipairs(filteredChildren) do + -- Apply X coordinate + child.set("x", currentX) + + -- Apply Y coordinate (based on vertical alignment) ONLY if not in wrapped mode + if not wrap then + if alignItems == "stretch" then + -- Vertical stretch to fill container, considering padding + child.set("height", availableCrossAxisSpace) + child.set("y", 1 + crossPadding) + else + local childHeight = child.get("height") + local y = 1 + + if alignItems == "flex-end" then + -- Bottom align + y = containerHeight - childHeight + 1 + elseif alignItems == "flex-center" or alignItems == "center" then + -- Center align + y = floor((containerHeight - childHeight) / 2) + 1 + end + + -- Ensure Y value is not less than 1 + child.set("y", max(1, y)) + end end - elseif justifyContent == "flex-center" or justifyContent == "center" then -- Akzeptiere beide Formate - local offset = math.floor((containerWidth - (currentX - spacing - 1)) / 2) - for _, child in ipairs(children) do - child.set("x", child.get("x") + offset) + + -- Final safety check height doesn't exceed container - only for elements with flexShrink + local bottomEdge = child.get("y") + child.get("height") - 1 + if bottomEdge > containerHeight and (child.get("flexShrink") or 0) > 0 then + child.set("height", max(1, containerHeight - child.get("y") + 1)) + end + + -- Update position for next element - advance by element width + spacing + currentX = currentX + child.get("width") + spacing + end + + -- Apply justifyContent only if there's remaining space + local lastChild = filteredChildren[#filteredChildren] + local usedWidth = 0 + if lastChild then + usedWidth = lastChild.get("x") + lastChild.get("width") - 1 + end + + local remainingSpace = containerWidth - usedWidth + + if remainingSpace > 0 then + if justifyContent == "flex-end" then + for _, child in ipairs(filteredChildren) do + child.set("x", child.get("x") + remainingSpace) + end + elseif justifyContent == "flex-center" or justifyContent == "center" then + local offset = floor(remainingSpace / 2) + for _, child in ipairs(filteredChildren) do + child.set("x", child.get("x") + offset) + end end end end local function calculateColumn(self, children, spacing, justifyContent) - local containerHeight = self.get("height") - - local usedSpace = spacing * (#children - 1) - local totalFlexGrow = 0 - + -- Make a copy of children that filters out lineBreak elements + local filteredChildren = {} for _, child in ipairs(children) do if child ~= lineBreakElement then - usedSpace = usedSpace + child.get("height") - totalFlexGrow = totalFlexGrow + child.get("flexGrow") + table.insert(filteredChildren, child) end end - - local remainingSpace = containerHeight - usedSpace - local extraSpacePerUnit = totalFlexGrow > 0 and (remainingSpace / totalFlexGrow) or 0 - local distributedSpace = 0 - - local currentY = 1 - for i, child in ipairs(children) do - if child ~= lineBreakElement then - local childHeight = child.get("height") - - if child.get("flexGrow") > 0 then - - if i == #children then - local extraSpace = remainingSpace - distributedSpace - childHeight = childHeight + extraSpace - else - local extraSpace = math.floor(extraSpacePerUnit * child.get("flexGrow")) - childHeight = childHeight + extraSpace - distributedSpace = distributedSpace + extraSpace + + -- Skip processing if no children + if #filteredChildren == 0 then + return + end + + local containerWidth = self.get("width") + local containerHeight = self.get("height") + local alignItems = self.get("flexAlignItems") + local crossPadding = self.get("flexCrossPadding") + local wrap = self.get("flexWrap") + + -- Safety check + if containerHeight <= 0 then return end + + -- Calculate available cross axis space (considering padding) + local availableCrossAxisSpace = containerWidth - (crossPadding * 2) + if availableCrossAxisSpace < 1 then + availableCrossAxisSpace = containerWidth + crossPadding = 0 + end + + -- Cache local variables to reduce function calls + local max = math.max + local min = math.min + local floor = math.floor + local ceil = math.ceil + + -- Categorize elements and calculate their minimal heights and flexibilities + local totalFixedHeight = 0 + local totalFlexGrow = 0 + local minHeights = {} + local flexGrows = {} + local flexShrinks = {} + + -- First pass: collect fixed heights and flex properties + for _, child in ipairs(filteredChildren) do + local grow = child.get("flexGrow") or 0 + local shrink = child.get("flexShrink") or 0 + local height = child.get("height") + + -- Track element properties + flexGrows[child] = grow + flexShrinks[child] = shrink + minHeights[child] = height + + -- Calculate total flex grow factor + if grow > 0 then + totalFlexGrow = totalFlexGrow + grow + else + -- If not flex grow, it's a fixed element + totalFixedHeight = totalFixedHeight + height + end + end + + -- Calculate total spacing + local elementsCount = #filteredChildren + local totalSpacing = (elementsCount > 1) and ((elementsCount - 1) * spacing) or 0 + + -- Calculate available space for flex items + local availableSpace = containerHeight - totalFixedHeight - totalSpacing + + -- Second pass: distribute available space to flex-grow items + if availableSpace > 0 and totalFlexGrow > 0 then + -- Container has extra space - distribute according to flex-grow + for _, child in ipairs(filteredChildren) do + local grow = flexGrows[child] + if grow > 0 then + -- Calculate flex basis (never less than minHeight) + local minHeight = minHeights[child] + local flexHeight = floor((grow / totalFlexGrow) * availableSpace) + + -- Set calculated height, ensure it's at least 1 + child.set("height", max(flexHeight, 1)) + end + end + elseif availableSpace < 0 then + -- Container doesn't have enough space - check for shrinkable items + local totalFlexShrink = 0 + local shrinkableItems = {} + + -- Find shrinkable items + for _, child in ipairs(filteredChildren) do + local shrink = flexShrinks[child] + if shrink > 0 then + totalFlexShrink = totalFlexShrink + shrink + table.insert(shrinkableItems, child) + end + end + + -- If we have shrinkable items, shrink them proportionally + if totalFlexShrink > 0 and #shrinkableItems > 0 then + local excessHeight = -availableSpace + + for _, child in ipairs(shrinkableItems) do + local height = child.get("height") + local shrink = flexShrinks[child] + local proportion = shrink / totalFlexShrink + local reduction = ceil(excessHeight * proportion) + + -- Ensure height doesn't go below 1 + child.set("height", max(1, height - reduction)) + end + end + + -- Recalculate fixed heights after shrinking + totalFixedHeight = 0 + for _, child in ipairs(filteredChildren) do + totalFixedHeight = totalFixedHeight + child.get("height") + end + + -- If we still have flex-grow items, ensure they have proportional space + if totalFlexGrow > 0 then + local growableItems = {} + local totalGrowableInitialHeight = 0 + + -- Find growable items + for _, child in ipairs(filteredChildren) do + if flexGrows[child] > 0 then + table.insert(growableItems, child) + totalGrowableInitialHeight = totalGrowableInitialHeight + child.get("height") + end + end + + -- Ensure flexGrow items get at least some height, even if space is tight + if #growableItems > 0 and totalGrowableInitialHeight > 0 then + -- Minimum guaranteed height for flex items (at least 20% of container) + local minFlexSpace = max(floor(containerHeight * 0.2), #growableItems) + + -- Reserve space for flex items + local reservedFlexSpace = min(minFlexSpace, containerHeight - totalSpacing) + + -- Distribute among flex items + for _, child in ipairs(growableItems) do + local grow = flexGrows[child] + local proportion = grow / totalFlexGrow + local flexHeight = max(1, floor(reservedFlexSpace * proportion)) + child.set("height", flexHeight) end end - - child.set("x", children.offset or 1) - child.set("y", currentY) - child.set("height", childHeight) - currentY = currentY + childHeight + spacing end end - - if justifyContent == "flex-end" then - local offset = containerHeight - (currentY - spacing - 1) - for _, child in ipairs(children) do - child.set("y", child.get("y") + offset) + + -- Step 3: Position elements (never allow overlapping) + local currentY = 1 + + -- Place all elements sequentially + for _, child in ipairs(filteredChildren) do + -- Apply Y coordinate + child.set("y", currentY) + + -- Apply X coordinate (based on horizontal alignment) + if not wrap then + if alignItems == "stretch" then + -- Horizontal stretch to fill container, considering padding + child.set("width", availableCrossAxisSpace) + child.set("x", 1 + crossPadding) + else + local childWidth = child.get("width") + local x = 1 + + if alignItems == "flex-end" then + -- Right align + x = containerWidth - childWidth + 1 + elseif alignItems == "flex-center" or alignItems == "center" then + -- Center align + x = floor((containerWidth - childWidth) / 2) + 1 + end + + -- Ensure X value is not less than 1 + child.set("x", max(1, x)) + end end - elseif justifyContent == "flex-center" or justifyContent == "center" then -- Akzeptiere beide Formate - local offset = math.floor((containerHeight - (currentY - spacing - 1)) / 2) - for _, child in ipairs(children) do - child.set("y", child.get("y") + offset) + + -- Final safety check width doesn't exceed container - only for elements with flexShrink + local rightEdge = child.get("x") + child.get("width") - 1 + if rightEdge > containerWidth and (child.get("flexShrink") or 0) > 0 then + child.set("width", max(1, containerWidth - child.get("x") + 1)) + end + + -- Update position for next element - advance by element height + spacing + currentY = currentY + child.get("height") + spacing + end + + -- Apply justifyContent only if there's remaining space + local lastChild = filteredChildren[#filteredChildren] + local usedHeight = 0 + if lastChild then + usedHeight = lastChild.get("y") + lastChild.get("height") - 1 + end + + local remainingSpace = containerHeight - usedHeight + + if remainingSpace > 0 then + if justifyContent == "flex-end" then + for _, child in ipairs(filteredChildren) do + child.set("y", child.get("y") + remainingSpace) + end + elseif justifyContent == "flex-center" or justifyContent == "center" then + local offset = floor(remainingSpace / 2) + for _, child in ipairs(filteredChildren) do + child.set("y", child.get("y") + offset) + end end end end +-- Optimize updateLayout function local function updateLayout(self, direction, spacing, justifyContent, wrap) - local elements = sortElements(self, direction, spacing, wrap) - if direction == "row" then - for _,v in pairs(elements)do - calculateRow(self, v, spacing, justifyContent) - end - else - for _,v in pairs(elements)do - calculateColumn(self, v, spacing, justifyContent) + if self.get("width") <= 0 or self.get("height") <= 0 then + return + end + + direction = (direction == "row" or direction == "column") and direction or "row" + + local currentWidth, currentHeight = self.get("width"), self.get("height") + local sizeChanged = currentWidth ~= self._lastLayoutWidth or currentHeight ~= self._lastLayoutHeight + + self._lastLayoutWidth = currentWidth + self._lastLayoutHeight = currentHeight + + if wrap and sizeChanged and (currentWidth > self._lastLayoutWidth or currentHeight > self._lastLayoutHeight) then + for _, child in pairs(self.get("children")) do + if child ~= lineBreakElement and child:getVisible() and child.get("flexGrow") and child.get("flexGrow") > 0 then + if direction == "row" then + local ok, value = pcall(function() return child.get("intrinsicWidth") end) + if ok and value then + child.set("width", value) + end + else + local ok, value = pcall(function() return child.get("intrinsicHeight") end) + if ok and value then + child.set("height", value) + end + end + end end end + + local elements = sortElements(self, direction, spacing, wrap) + if #elements == 0 then return end + + local layoutFunction = direction == "row" and calculateRow or calculateColumn + + if direction == "row" and wrap then + local currentY = 1 + for i, rowOrColumn in ipairs(elements) do + if #rowOrColumn > 0 then + for _, element in ipairs(rowOrColumn) do + if element ~= lineBreakElement then + element.set("y", currentY) + end + end + + layoutFunction(self, rowOrColumn, spacing, justifyContent) + + local rowHeight = 0 + for _, element in ipairs(rowOrColumn) do + if element ~= lineBreakElement then + rowHeight = math.max(rowHeight, element.get("height")) + end + end + + if i < #elements then + currentY = currentY + rowHeight + spacing + else + currentY = currentY + rowHeight + end + end + end + elseif direction == "column" and wrap then + local currentX = 1 + for i, rowOrColumn in ipairs(elements) do + if #rowOrColumn > 0 then + for _, element in ipairs(rowOrColumn) do + if element ~= lineBreakElement then + element.set("x", currentX) + end + end + + layoutFunction(self, rowOrColumn, spacing, justifyContent) + + local columnWidth = 0 + for _, element in ipairs(rowOrColumn) do + if element ~= lineBreakElement then + columnWidth = math.max(columnWidth, element.get("width")) + end + end + + if i < #elements then + currentX = currentX + columnWidth + spacing + else + currentX = currentX + columnWidth + end + end + end + else + for _, rowOrColumn in ipairs(elements) do + layoutFunction(self, rowOrColumn, spacing, justifyContent) + end + end + self:sortChildren() + self.set("childrenEventsSorted", false) self.set("flexUpdateLayout", false) end @@ -251,8 +725,19 @@ function Flexbox.new() self.set("height", 6) self.set("background", colors.blue) self.set("z", 10) + + self._lastLayoutWidth = 0 + self._lastLayoutHeight = 0 + self:observe("width", function() self.set("flexUpdateLayout", true) end) self:observe("height", function() self.set("flexUpdateLayout", true) end) + self:observe("flexDirection", function() self.set("flexUpdateLayout", true) end) + self:observe("flexSpacing", function() self.set("flexUpdateLayout", true) end) + self:observe("flexWrap", function() self.set("flexUpdateLayout", true) end) + self:observe("flexJustifyContent", function() self.set("flexUpdateLayout", true) end) + self:observe("flexAlignItems", function() self.set("flexUpdateLayout", true) end) + self:observe("flexCrossPadding", function() self.set("flexUpdateLayout", true) end) + return self end @@ -278,6 +763,24 @@ function Flexbox:addChild(element) element:instanceProperty("flexGrow", {default = 0, type = "number"}) element:instanceProperty("flexShrink", {default = 0, type = "number"}) element:instanceProperty("flexBasis", {default = 0, type = "number"}) + element:instanceProperty("intrinsicWidth", {default = element.get("width"), type = "number"}) + element:instanceProperty("intrinsicHeight", {default = element.get("height"), type = "number"}) + + element:observe("flexGrow", function() self.set("flexUpdateLayout", true) end) + element:observe("flexShrink", function() self.set("flexUpdateLayout", true) end) + + element:observe("width", function(_, newValue, oldValue) + if element.get("flexGrow") == 0 then + element.set("intrinsicWidth", newValue) + end + self.set("flexUpdateLayout", true) + end) + element:observe("height", function(_, newValue, oldValue) + if element.get("flexGrow") == 0 then + element.set("intrinsicHeight", newValue) + end + self.set("flexUpdateLayout", true) + end) end self.set("flexUpdateLayout", true) diff --git a/src/elements/Graph.lua b/src/elements/Graph.lua index 8ce4e4e..ab762c3 100644 --- a/src/elements/Graph.lua +++ b/src/elements/Graph.lua @@ -221,4 +221,4 @@ function Graph:render() end end -return Graph +return Graph \ No newline at end of file diff --git a/src/elements/Table.lua b/src/elements/Table.lua index 66b0985..cc9da49 100644 --- a/src/elements/Table.lua +++ b/src/elements/Table.lua @@ -66,6 +66,29 @@ function Table:init(props, basalt) return self end +--- Adds a new column to the table +--- @shortDescription Adds a new column to the table +--- @param name string The name of the column +--- @param width number The width of the column +--- @return Table self The Table instance +function Table:addColumn(name, width) + local columns = self.get("columns") + table.insert(columns, {name = name, width = width}) + self.set("columns", columns) + return self +end + +--- Adds a new row of data to the table +--- @shortDescription Adds a new row of data to the table +--- @param ... any The data for the new row +--- @return Table self The Table instance +function Table:addData(...) + local data = self.get("data") + table.insert(data, {...}) + self.set("data", data) + return self +end + --- Sorts the table data by column --- @shortDescription Sorts the table data by the specified column --- @param columnIndex number The index of the column to sort by @@ -87,7 +110,6 @@ function Table:sortData(columnIndex, fn) return fn(a[columnIndex], b[columnIndex]) end) end - self.set("data", data) return self end @@ -186,9 +208,12 @@ function Table:render() if i < #columns then paddedText = string.sub(paddedText, 1, col.width - 1) .. " " end - self:blit(currentX, y, string.sub(paddedText, 1, col.width), - string.sub(string.rep(tHex[self.get("foreground")], col.width), 1, width-currentX+1), - string.sub(string.rep(tHex[bg], col.width), 1, width-currentX+1)) + local finalText = string.sub(paddedText, 1, col.width) + local finalForeground = string.sub(string.rep(tHex[self.get("foreground")], col.width), 1, width-currentX+1) + local finalBackground = string.sub(string.rep(tHex[bg], col.width), 1, width-currentX+1) + self:blit(currentX, y, finalText, + finalForeground, + finalBackground) currentX = currentX + col.width end else diff --git a/src/elements/TextBox.lua b/src/elements/TextBox.lua index 985e214..03f0c3f 100644 --- a/src/elements/TextBox.lua +++ b/src/elements/TextBox.lua @@ -311,4 +311,4 @@ function TextBox:render() end end -return TextBox +return TextBox \ No newline at end of file diff --git a/src/elements/Timer.lua b/src/elements/Timer.lua new file mode 100644 index 0000000..229b03b --- /dev/null +++ b/src/elements/Timer.lua @@ -0,0 +1,83 @@ +local elementManager = require("elementManager") +local BaseElement = elementManager.getElement("BaseElement") +---@cofnigDescription The Timer is a non-visual element that can be used to perform actions at specific intervals. + +--- The Timer is a non-visual element that can be used to perform actions at specific intervals. +---@class Timer : BaseElement +local Timer = setmetatable({}, BaseElement) +Timer.__index = Timer + +---@property interval number 1 The interval in seconds at which the timer will trigger its action. +Timer.defineProperty(Timer, "interval", {default = 1, type = "number"}) +---@property action function function The action to be performed when the timer triggers. +Timer.defineProperty(Timer, "action", {default = function() end, type = "function"}) +---@property running boolean false Indicates whether the timer is currently running or not. +Timer.defineProperty(Timer, "running", {default = false, type = "boolean"}) +---@property amount number -1 The amount of time the timer should run. +Timer.defineProperty(Timer, "amount", {default = -1, type = "number"}) + +Timer.defineEvent(Timer, "timer") + +--- @shortDescription Creates a new Timer instance +--- @return table self The created instance +--- @private +function Timer.new() + local self = setmetatable({}, Timer):__init() + self.class = Timer + return self +end + +--- @shortDescription Initializes the Timer instance +--- @param props table The properties to initialize the element with +--- @param basalt table The basalt instance +--- @protected +function Timer:init(props, basalt) + BaseElement.init(self, props, basalt) + self.set("type", "Timer") +end + +--- Starts the timer with the specified interval. +--- @shortDescription Starts the timer +--- @param self Timer The Timer instance to start +--- @return Timer self The Timer instance +function Timer:start() + if not self.running then + self.running = true + local time = self.get("interval") + self.timerId = os.startTimer(time) + end + return self +end + +--- Stops the timer if it is currently running. +--- @shortDescription Stops the timer +--- @param self Timer The Timer instance to stop +--- @return Timer self The Timer instance +function Timer:stop() + if self.running then + self.running = false + os.cancelTimer(self.timerId) + end + return self +end + +--- @protected +--- @shortDescription Dispatches events to the Timer instance +function Timer:dispatchEvent(event, ...) + BaseElement.dispatchEvent(self, event, ...) + if event == "timer" then + local timerId = select(1, ...) + if timerId == self.timerId then + self.action() + local amount = self.get("amount") + if amount > 0 then + self.set("amount", amount - 1) + end + if amount ~= 0 then + self.timerId = os.startTimer(self.get("interval")) + end + end + end +end + +return Timer \ No newline at end of file diff --git a/src/plugins/animation.lua b/src/plugins/animation.lua index 40d0b05..b687af0 100644 --- a/src/plugins/animation.lua +++ b/src/plugins/animation.lua @@ -329,6 +329,44 @@ Animation.registerAnimation("moveOffset", { end }) +Animation.registerAnimation("number", { + start = function(anim) + anim.startValue = anim.element.get(anim.args[1]) + anim.targetValue = anim.args[2] + end, + + update = function(anim, progress) + local value = anim.startValue + (anim.targetValue - anim.startValue) * progress + anim.element.set(anim.args[1], math.floor(value)) + return progress >= 1 + end, + + complete = function(anim) + anim.element.set(anim.args[1], anim.targetValue) + end +}) + +Animation.registerAnimation("entries", { + start = function(anim) + anim.startColor = anim.element.get(anim.args[1]) + anim.colorList = anim.args[2] + end, + + update = function(anim, progress) + local list = anim.colorList + local index = math.floor(#list * progress) + 1 + if index > #list then + index = #list + end + anim.element.set(anim.args[1], list[index]) + + end, + + complete = function(anim) + anim.element.set(anim.args[1], anim.colorList[#anim.colorList]) + end +}) + Animation.registerAnimation("morphText", { start = function(anim) local startText = anim.element.get(anim.args[1]) diff --git a/src/plugins/reactive.lua b/src/plugins/reactive.lua index aeae996..0528820 100644 --- a/src/plugins/reactive.lua +++ b/src/plugins/reactive.lua @@ -147,7 +147,7 @@ local function setupObservers(element, expr, propertyName) local observer = { target = target, property = prop, - callback = function() + callback = function() element:updateRender() end } @@ -198,7 +198,7 @@ end) --- @usage local button = main:addButton({text="Exit"}) --- @usage button:setX("{parent.x - 12}") --- @usage button:setBackground("{self.clicked and colors.red or colors.green}") ---- @usage button:setWidth("{self.text:len() + 2}") +--- @usage button:setWidth("{#self.text + 2}") ---@class Reactive local BaseElement = {} diff --git a/src/plugins/theme.lua b/src/plugins/theme.lua index f341d11..c6e45e5 100644 --- a/src/plugins/theme.lua +++ b/src/plugins/theme.lua @@ -177,8 +177,6 @@ function BaseElement:applyTheme(applyToChildren) end end self.set(prop, value) - else - errorManager.error("Invalid property '" .. prop .. "' in theme for " .. self._values.type) end end end