diff --git a/FlexLove.lua b/FlexLove.lua index f418aa6..cf56d17 100644 --- a/FlexLove.lua +++ b/FlexLove.lua @@ -91,12 +91,14 @@ flexlove._LICENSE = [[ ]] -- GC (Garbage Collection) configuration +---@type GCConfig flexlove._gcConfig = { strategy = "auto", -- "auto", "periodic", "manual", "disabled" memoryThreshold = 100, -- MB before forcing GC interval = 60, -- Frames between GC steps (for periodic mode) stepSize = 200, -- Work units per GC step (higher = more aggressive) } +---@type GCState flexlove._gcState = { framesSinceLastGC = 0, lastMemory = 0, @@ -104,6 +106,7 @@ flexlove._gcState = { } -- Deferred callback queue for operations that cannot run while Canvas is active +---@type function[] flexlove._deferredCallbacks = {} -- Track accumulated delta time for immediate mode updates @@ -474,8 +477,11 @@ function flexlove.endFrame() flexlove._Performance:resetFrameCounters() end +---@type love.Canvas? flexlove._gameCanvas = nil +---@type love.Canvas? flexlove._backdropCanvas = nil +---@type {width: number, height: number} flexlove._canvasDimensions = { width = 0, height = 0 } --- Render all UI elements with optional backdrop blur support for glassmorphic effects @@ -596,9 +602,10 @@ function flexlove.draw(gameDrawFunc, postDrawFunc) -- of love.draw() after ALL canvases have been released. end ----@param element Element ----@param target Element ----@return boolean +--- Check if element is an ancestor of target +---@param element Element The potential ancestor element +---@param target Element The target element to check +---@return boolean isAncestor True if element is an ancestor of target local function isAncestor(element, target) local current = target.parent while current do @@ -811,7 +818,7 @@ end --- Monitor memory management behavior to diagnose performance issues and tune GC settings --- Use this to identify memory leaks or optimize garbage collection timing ----@return table stats {gcCount, framesSinceLastGC, currentMemoryMB, strategy} +---@return GCStats stats GC statistics function flexlove.getGCStats() return { gcCount = flexlove._gcState.gcCount, @@ -1116,7 +1123,7 @@ end --- height = "10vh", --- }) ---@param expr string The calc expression (e.g., "50% - 10vw", "100px + 20%") ----@return table calcObject A calc expression object that will be evaluated during layout +---@return CalcObject calcObject A calc expression object that will be evaluated during layout function flexlove.calc(expr) return Calc.new(expr) end diff --git a/modules/Calc.lua b/modules/Calc.lua index a220f95..b156cc9 100644 --- a/modules/Calc.lua +++ b/modules/Calc.lua @@ -4,7 +4,7 @@ local Calc = {} --- Initialize Calc module with dependencies ----@param deps table Dependencies: { ErrorHandler = table? } +---@param deps CalcDependencies Dependencies: { ErrorHandler = ErrorHandler? } function Calc.init(deps) Calc._ErrorHandler = deps.ErrorHandler end @@ -24,7 +24,7 @@ local TokenType = { --- Tokenize a calc expression string into tokens ---@param expr string The expression to tokenize (e.g., "50% - 10vw") ----@return table|nil tokens Array of tokens with type, value, unit +---@return CalcToken[]? tokens Array of tokens with type, value, unit ---@return string? error Error message if tokenization fails local function tokenize(expr) local tokens = {} @@ -157,13 +157,13 @@ end --- Parser for calc expressions using recursive descent ---@class Parser ----@field tokens table Array of tokens +---@field tokens CalcToken[] Array of tokens ---@field pos number Current token position local Parser = {} Parser.__index = Parser --- Create a new parser ----@param tokens table Array of tokens +---@param tokens CalcToken[] Array of tokens ---@return Parser function Parser.new(tokens) local self = setmetatable({}, Parser) @@ -173,7 +173,7 @@ function Parser.new(tokens) end --- Get current token ----@return table token Current token +---@return CalcToken token Current token function Parser:current() return self.tokens[self.pos] end @@ -184,7 +184,7 @@ function Parser:advance() end --- Parse expression (handles + and -) ----@return table ast Abstract syntax tree node +---@return CalcASTNode ast Abstract syntax tree node function Parser:parseExpression() local left = self:parseTerm() @@ -203,7 +203,7 @@ function Parser:parseExpression() end --- Parse term (handles * and /) ----@return table ast Abstract syntax tree node +---@return CalcASTNode ast Abstract syntax tree node function Parser:parseTerm() local left = self:parseFactor() @@ -222,7 +222,7 @@ function Parser:parseTerm() end --- Parse factor (handles numbers and parentheses) ----@return table ast Abstract syntax tree node +---@return CalcASTNode ast Abstract syntax tree node function Parser:parseFactor() local token = self:current() @@ -247,7 +247,7 @@ function Parser:parseFactor() end --- Parse the tokens into an AST ----@return table ast Abstract syntax tree +---@return CalcASTNode ast Abstract syntax tree function Parser:parse() local ast = self:parseExpression() if self:current().type ~= TokenType.EOF then @@ -259,7 +259,7 @@ end --- Create a calc expression object that can be resolved later --- This is the main API function that users call ---@param expr string The calc expression (e.g., "50% - 10vw") ----@return table calcObject A calc expression object with AST +---@return CalcObject calcObject A calc expression object with AST function Calc.new(expr) -- Tokenize local tokens, err = tokenize(expr) @@ -316,7 +316,7 @@ function Calc.isCalc(value) end --- Resolve a calc expression to pixel value ----@param calcObj table The calc expression object +---@param calcObj CalcObject The calc expression object ---@param viewportWidth number Viewport width in pixels ---@param viewportHeight number Viewport height in pixels ---@param parentSize number? Parent dimension for percentage units diff --git a/modules/Element.lua b/modules/Element.lua index ec4cf27..59c89e9 100644 --- a/modules/Element.lua +++ b/modules/Element.lua @@ -162,6 +162,7 @@ function Element.init(deps) Element._Color = deps.Color Element._Context = deps.Context Element._Units = deps.Units + Element._Calc = deps.Calc Element._utils = deps.utils Element._InputEvent = deps.InputEvent Element._EventHandler = deps.EventHandler @@ -826,11 +827,22 @@ function Element.new(props) local widthProp = props.width local tempWidth = 0 -- Temporary width for padding resolution if widthProp then - if type(widthProp) == "string" then + -- Check if it's a CalcObject (table with _isCalc marker) + local isCalc = Element._Calc and Element._Calc.isCalc(widthProp) + if type(widthProp) == "string" or isCalc then local value, unit = Element._Units.parse(widthProp) self.units.width = { value = value, unit = unit } local parentWidth = self.parent and self.parent.width or viewportWidth tempWidth = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, parentWidth) + -- Defensive check: ensure tempWidth is a number after resolution + if type(tempWidth) ~= "number" then + Element._ErrorHandler:warn("Element", "LAY_003", { + issue = "width resolution returned non-number value", + type = type(tempWidth), + value = tostring(tempWidth), + }) + tempWidth = 0 + end else tempWidth = Element._Context.baseScale and (widthProp * scaleX) or widthProp self.units.width = { value = widthProp, unit = "px" } @@ -856,11 +868,22 @@ function Element.new(props) local heightProp = props.height local tempHeight = 0 -- Temporary height for padding resolution if heightProp then - if type(heightProp) == "string" then + -- Check if it's a CalcObject (table with _isCalc marker) + local isCalc = Element._Calc and Element._Calc.isCalc(heightProp) + if type(heightProp) == "string" or isCalc then local value, unit = Element._Units.parse(heightProp) self.units.height = { value = value, unit = unit } local parentHeight = self.parent and self.parent.height or viewportHeight tempHeight = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, parentHeight) + -- Defensive check: ensure tempHeight is a number after resolution + if type(tempHeight) ~= "number" then + Element._ErrorHandler:warn("Element", "LAY_003", { + issue = "height resolution returned non-number value", + type = type(tempHeight), + value = tostring(tempHeight), + }) + tempHeight = 0 + end else -- Apply base scaling to pixel values tempHeight = Element._Context.baseScale and (heightProp * scaleY) or heightProp @@ -877,14 +900,25 @@ function Element.new(props) --- child positioning --- if props.gap then - if type(props.gap) == "string" then + local isCalc = Element._Calc and Element._Calc.isCalc(props.gap) + if type(props.gap) == "string" or isCalc then local value, unit = Element._Units.parse(props.gap) self.units.gap = { value = value, unit = unit } -- Gap percentages should be relative to the element's own size, not parent -- For horizontal flex, gap is based on width; for vertical flex, based on height local flexDir = props.flexDirection or Element._utils.enums.FlexDirection.HORIZONTAL local containerSize = (flexDir == Element._utils.enums.FlexDirection.HORIZONTAL) and self.width or self.height - self.gap = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, containerSize) + local resolvedGap = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, containerSize) + -- Defensive check: ensure gap is a number after resolution + if type(resolvedGap) ~= "number" then + Element._ErrorHandler:warn("Element", "LAY_003", { + issue = "gap resolution returned non-number value", + type = type(resolvedGap), + value = tostring(resolvedGap), + }) + resolvedGap = 0 + end + self.gap = resolvedGap else self.gap = props.gap self.units.gap = { value = props.gap, unit = "px" } @@ -925,6 +959,27 @@ function Element.new(props) -- For auto-sized elements, this is content width; for explicit sizing, this is border-box width local tempPadding if use9PatchPadding then + -- Ensure tempWidth and tempHeight are numbers (not CalcObjects) + -- This should already be true after Units.resolve(), but add defensive check + if type(tempWidth) ~= "number" then + if Element._ErrorHandler then + Element._ErrorHandler:warn("Element", "LAY_003", { + issue = "tempWidth is not a number after resolution", + type = type(tempWidth), + }) + end + tempWidth = 0 + end + if type(tempHeight) ~= "number" then + if Element._ErrorHandler then + Element._ErrorHandler:warn("Element", "LAY_003", { + issue = "tempHeight is not a number after resolution", + type = type(tempHeight), + }) + end + tempHeight = 0 + end + -- Get scaled 9-patch content padding from ThemeManager local scaledPadding = self._themeManager:getScaledContentPadding(tempWidth, tempHeight) if scaledPadding then @@ -1092,10 +1147,19 @@ function Element.new(props) -- Handle x position with units if props.x then - if type(props.x) == "string" or type(props.x) == "table" then + local isCalc = Element._Calc and Element._Calc.isCalc(props.x) + if type(props.x) == "string" or isCalc then local value, unit = Element._Units.parse(props.x) self.units.x = { value = value, unit = unit } - self.x = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, viewportWidth) + local resolvedX = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, viewportWidth) + if type(resolvedX) ~= "number" then + Element._ErrorHandler:warn("Element", "LAY_003", { + issue = "x resolution returned non-number value", + type = type(resolvedX), + }) + resolvedX = 0 + end + self.x = resolvedX else -- Apply base scaling to pixel positions self.x = Element._Context.baseScale and (props.x * scaleX) or props.x @@ -1108,10 +1172,19 @@ function Element.new(props) -- Handle y position with units if props.y then - if type(props.y) == "string" or type(props.y) == "table" then + local isCalc = Element._Calc and Element._Calc.isCalc(props.y) + if type(props.y) == "string" or isCalc then local value, unit = Element._Units.parse(props.y) self.units.y = { value = value, unit = unit } - self.y = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, viewportHeight) + local resolvedY = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, viewportHeight) + if type(resolvedY) ~= "number" then + Element._ErrorHandler:warn("Element", "LAY_003", { + issue = "y resolution returned non-number value", + type = type(resolvedY), + }) + resolvedY = 0 + end + self.y = resolvedY else -- Apply base scaling to pixel positions self.y = Element._Context.baseScale and (props.y * scaleY) or props.y @@ -1181,11 +1254,19 @@ function Element.new(props) -- Handle x position with units if props.x then - if type(props.x) == "string" or type(props.x) == "table" then + local isCalc = Element._Calc and Element._Calc.isCalc(props.x) + if type(props.x) == "string" or isCalc then local value, unit = Element._Units.parse(props.x) self.units.x = { value = value, unit = unit } local parentWidth = self.parent.width local offsetX = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, parentWidth) + if type(offsetX) ~= "number" then + Element._ErrorHandler:warn("Element", "LAY_003", { + issue = "x resolution returned non-number value", + type = type(offsetX), + }) + offsetX = 0 + end self.x = baseX + offsetX else -- Apply base scaling to pixel positions @@ -1200,11 +1281,19 @@ function Element.new(props) -- Handle y position with units if props.y then - if type(props.y) == "string" or type(props.y) == "table" then + local isCalc = Element._Calc and Element._Calc.isCalc(props.y) + if type(props.y) == "string" or isCalc then local value, unit = Element._Units.parse(props.y) self.units.y = { value = value, unit = unit } local parentHeight = self.parent.height local offsetY = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, parentHeight) + if type(offsetY) ~= "number" then + Element._ErrorHandler:warn("Element", "LAY_003", { + issue = "y resolution returned non-number value", + type = type(offsetY), + }) + offsetY = 0 + end self.y = baseY + offsetY else -- Apply base scaling to pixel positions @@ -1225,11 +1314,19 @@ function Element.new(props) local baseY = self.parent.y + self.parent.padding.top if props.x then - if type(props.x) == "string" or type(props.x) == "table" then + local isCalc = Element._Calc and Element._Calc.isCalc(props.x) + if type(props.x) == "string" or isCalc then local value, unit = Element._Units.parse(props.x) self.units.x = { value = value, unit = unit } local parentWidth = self.parent.width local offsetX = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, parentWidth) + if type(offsetX) ~= "number" then + Element._ErrorHandler:warn("Element", "LAY_003", { + issue = "x resolution returned non-number value", + type = type(offsetX), + }) + offsetX = 0 + end self.x = baseX + offsetX else -- Apply base scaling to pixel offsets @@ -1243,11 +1340,19 @@ function Element.new(props) end if props.y then - if type(props.y) == "string" or type(props.y) == "table" then + local isCalc = Element._Calc and Element._Calc.isCalc(props.y) + if type(props.y) == "string" or isCalc then local value, unit = Element._Units.parse(props.y) self.units.y = { value = value, unit = unit } parentHeight = self.parent.height local offsetY = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, parentHeight) + if type(offsetY) ~= "number" then + Element._ErrorHandler:warn("Element", "LAY_003", { + issue = "y resolution returned non-number value", + type = type(offsetY), + }) + offsetY = 0 + end self.y = baseY + offsetY else -- Apply base scaling to pixel offsets @@ -1283,10 +1388,19 @@ function Element.new(props) -- Handle positioning properties for ALL elements (with or without parent) -- Handle top positioning with units if props.top then - if type(props.top) == "string" then + local isCalc = Element._Calc and Element._Calc.isCalc(props.top) + if type(props.top) == "string" or isCalc then local value, unit = Element._Units.parse(props.top) self.units.top = { value = value, unit = unit } - self.top = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, viewportHeight) + local resolvedTop = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, viewportHeight) + if type(resolvedTop) ~= "number" then + Element._ErrorHandler:warn("Element", "LAY_003", { + issue = "top resolution returned non-number value", + type = type(resolvedTop), + }) + resolvedTop = 0 + end + self.top = resolvedTop else self.top = props.top self.units.top = { value = props.top, unit = "px" } @@ -1298,10 +1412,19 @@ function Element.new(props) -- Handle right positioning with units if props.right then - if type(props.right) == "string" then + local isCalc = Element._Calc and Element._Calc.isCalc(props.right) + if type(props.right) == "string" or isCalc then local value, unit = Element._Units.parse(props.right) self.units.right = { value = value, unit = unit } - self.right = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, viewportWidth) + local resolvedRight = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, viewportWidth) + if type(resolvedRight) ~= "number" then + Element._ErrorHandler:warn("Element", "LAY_003", { + issue = "right resolution returned non-number value", + type = type(resolvedRight), + }) + resolvedRight = 0 + end + self.right = resolvedRight else self.right = props.right self.units.right = { value = props.right, unit = "px" } @@ -1313,10 +1436,19 @@ function Element.new(props) -- Handle bottom positioning with units if props.bottom then - if type(props.bottom) == "string" then + local isCalc = Element._Calc and Element._Calc.isCalc(props.bottom) + if type(props.bottom) == "string" or isCalc then local value, unit = Element._Units.parse(props.bottom) self.units.bottom = { value = value, unit = unit } - self.bottom = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, viewportHeight) + local resolvedBottom = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, viewportHeight) + if type(resolvedBottom) ~= "number" then + Element._ErrorHandler:warn("Element", "LAY_003", { + issue = "bottom resolution returned non-number value", + type = type(resolvedBottom), + }) + resolvedBottom = 0 + end + self.bottom = resolvedBottom else self.bottom = props.bottom self.units.bottom = { value = props.bottom, unit = "px" } @@ -1328,10 +1460,19 @@ function Element.new(props) -- Handle left positioning with units if props.left then - if type(props.left) == "string" then + local isCalc = Element._Calc and Element._Calc.isCalc(props.left) + if type(props.left) == "string" or isCalc then local value, unit = Element._Units.parse(props.left) self.units.left = { value = value, unit = unit } - self.left = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, viewportWidth) + local resolvedLeft = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, viewportWidth) + if type(resolvedLeft) ~= "number" then + Element._ErrorHandler:warn("Element", "LAY_003", { + issue = "left resolution returned non-number value", + type = type(resolvedLeft), + }) + resolvedLeft = 0 + end + self.left = resolvedLeft else self.left = props.left self.units.left = { value = props.left, unit = "px" } @@ -1378,9 +1519,18 @@ function Element.new(props) -- Handle columnGap and rowGap if props.columnGap then - if type(props.columnGap) == "string" then + local isCalc = Element._Calc and Element._Calc.isCalc(props.columnGap) + if type(props.columnGap) == "string" or isCalc then local value, unit = Element._Units.parse(props.columnGap) - self.columnGap = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, self.width) + local resolvedColumnGap = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, self.width) + if type(resolvedColumnGap) ~= "number" then + Element._ErrorHandler:warn("Element", "LAY_003", { + issue = "columnGap resolution returned non-number value", + type = type(resolvedColumnGap), + }) + resolvedColumnGap = 0 + end + self.columnGap = resolvedColumnGap else self.columnGap = props.columnGap end @@ -1389,9 +1539,18 @@ function Element.new(props) end if props.rowGap then - if type(props.rowGap) == "string" then + local isCalc = Element._Calc and Element._Calc.isCalc(props.rowGap) + if type(props.rowGap) == "string" or isCalc then local value, unit = Element._Units.parse(props.rowGap) - self.rowGap = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, self.height) + local resolvedRowGap = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, self.height) + if type(resolvedRowGap) ~= "number" then + Element._ErrorHandler:warn("Element", "LAY_003", { + issue = "rowGap resolution returned non-number value", + type = type(resolvedRowGap), + }) + resolvedRowGap = 0 + end + self.rowGap = resolvedRowGap else self.rowGap = props.rowGap end diff --git a/modules/types.lua b/modules/types.lua index 5f788dd..ed3110e 100644 --- a/modules/types.lua +++ b/modules/types.lua @@ -34,24 +34,24 @@ local AnimationProps = {} ---@class ElementProps ---@field id string? -- Unique identifier for the element (auto-generated in immediate mode if not provided) ---@field parent Element? -- Parent element for hierarchical structure ----@field x number|string? -- X coordinate of the element (default: 0) ----@field y number|string? -- Y coordinate of the element (default: 0) +---@field x number|string|CalcObject? -- X coordinate: number (px), string ("50%", "10vw"), or CalcObject from FlexLove.calc() (default: 0) +---@field y number|string|CalcObject? -- Y coordinate: number (px), string ("50%", "10vh"), or CalcObject from FlexLove.calc() (default: 0) ---@field z number? -- Z-index for layering (default: 0) ----@field width number|string? -- Width of the element (default: calculated automatically) ----@field height number|string? -- Height of the element (default: calculated automatically) ----@field top number|string? -- Offset from top edge (CSS-style positioning) ----@field right number|string? -- Offset from right edge (CSS-style positioning) ----@field bottom number|string? -- Offset from bottom edge (CSS-style positioning) ----@field left number|string? -- Offset from left edge (CSS-style positioning) +---@field width number|string|CalcObject? -- Width of the element: number (px), string ("50%", "10vw"), or CalcObject from FlexLove.calc() (default: calculated automatically) +---@field height number|string|CalcObject? -- Height of the element: number (px), string ("50%", "10vh"), or CalcObject from FlexLove.calc() (default: calculated automatically) +---@field top number|string|CalcObject? -- Offset from top edge: number (px), string ("50%", "10vh"), or CalcObject (CSS-style positioning) +---@field right number|string|CalcObject? -- Offset from right edge: number (px), string ("50%", "10vw"), or CalcObject (CSS-style positioning) +---@field bottom number|string|CalcObject? -- Offset from bottom edge: number (px), string ("50%", "10vh"), or CalcObject (CSS-style positioning) +---@field left number|string|CalcObject? -- Offset from left edge: number (px), string ("50%", "10vw"), or CalcObject (CSS-style positioning) ---@field border Border? -- Border configuration for the element ---@field borderColor Color? -- Color of the border (default: black) ---@field opacity number? -- Element opacity 0-1 (default: 1) ---@field visibility "visible"|"hidden"? -- Element visibility (default: "visible") ---@field backgroundColor Color? -- Background color (default: transparent) ---@field cornerRadius number|{topLeft:number?, topRight:number?, bottomLeft:number?, bottomRight:number?}? -- Corner radius: number (all corners) or table for individual corners (default: 0) ----@field gap number|string? -- Space between children elements (default: 0) ----@field padding number|string|{top:number|string?, right:number|string?, bottom:number|string?, left:number|string?, horizontal:number|string?, vertical:number|string?}? -- Padding around children: single value for all sides or table for individual sides (default: {top=0, right=0, bottom=0, left=0}) ----@field margin number|string|{top:number|string?, right:number|string?, bottom:number|string?, left:number|string?, horizontal:number|string?, vertical:number|string?}? -- Margin around element: single value for all sides or table for individual sides (default: {top=0, right=0, bottom=0, left=0}) +---@field gap number|string|CalcObject? -- Space between children elements: number (px), string ("50%", "10vw"), or CalcObject from FlexLove.calc() (default: 0) +---@field padding number|string|CalcObject|{top:number|string|CalcObject?, right:number|string|CalcObject?, bottom:number|string|CalcObject?, left:number|string|CalcObject?, horizontal:number|string|CalcObject?, vertical:number|string|CalcObject?}? -- Padding around children: single value, string, CalcObject for all sides, or table for individual sides (default: {top=0, right=0, bottom=0, left=0}) +---@field margin number|string|CalcObject|{top:number|string|CalcObject?, right:number|string|CalcObject?, bottom:number|string|CalcObject?, left:number|string|CalcObject?, horizontal:number|string|CalcObject?, vertical:number|string|CalcObject?}? -- Margin around element: single value, string, CalcObject for all sides, or table for individual sides (default: {top=0, right=0, bottom=0, left=0}) ---@field text string? -- Text content to display (default: nil) ---@field textAlign TextAlign? -- Alignment of the text content (default: START) ---@field textColor Color? -- Color of the text content (default: black or theme text color) @@ -84,8 +84,8 @@ local AnimationProps = {} ---@field transition TransitionProps? -- Transition settings for animations ---@field gridRows number? -- Number of rows in the grid (default: 1) ---@field gridColumns number? -- Number of columns in the grid (default: 1) ----@field columnGap number|string? -- Gap between grid columns (default: 0) ----@field rowGap number|string? -- Gap between grid rows (default: 0) +---@field columnGap number|string|CalcObject? -- Gap between grid columns: number (px), string ("50%", "10vw"), or CalcObject from FlexLove.calc() (default: 0) +---@field rowGap number|string|CalcObject? -- Gap between grid rows: number (px), string ("50%", "10vh"), or CalcObject from FlexLove.calc() (default: 0) ---@field theme string? -- Theme name to use (e.g., "space", "metal"). Defaults to theme from flexlove.init() ---@field themeComponent string? -- Theme component to use (e.g., "panel", "button", "input"). If nil, no theme is applied ---@field disabled boolean? -- Whether the element is disabled (default: false) @@ -211,3 +211,74 @@ local FlexLoveConfig = {} ---@field _backdropBlurQuality number? ---@field _contentBlurRadius number? ---@field _contentBlurQuality number? + +--=====================================-- +-- For Calc.lua +--=====================================-- +---@class CalcDependencies +---@field ErrorHandler ErrorHandler? -- Error handler module + +---@class CalcToken +---@field type string -- Token type: "NUMBER", "UNIT", "PLUS", "MINUS", "MULTIPLY", "DIVIDE", "LPAREN", "RPAREN", "EOF" +---@field value number? -- Numeric value (for NUMBER tokens) +---@field unit string? -- Unit type: "px", "%", "vw", "vh", "ew", "eh" (for NUMBER tokens) + +---@class CalcASTNode +---@field type string -- Node type: "number", "add", "subtract", "multiply", "divide" +---@field value number? -- Numeric value (for "number" nodes) +---@field unit string? -- Unit type (for "number" nodes) +---@field left CalcASTNode? -- Left operand (for operator nodes) +---@field right CalcASTNode? -- Right operand (for operator nodes) + +---@class CalcObject +---@field _isCalc boolean -- Marker to identify calc objects (always true) +---@field _expr string -- Original expression string +---@field _ast CalcASTNode? -- Parsed abstract syntax tree (nil if parsing failed) +---@field _error string? -- Error message if parsing failed + +--=====================================-- +-- For FlexLove.lua Internals +--=====================================-- +---@class GCConfig +---@field strategy string -- "auto", "periodic", "manual", or "disabled" +---@field memoryThreshold number -- MB before forcing GC +---@field interval number -- Frames between GC steps (for periodic mode) +---@field stepSize number -- Work units per GC step (higher = more aggressive) + +---@class GCState +---@field framesSinceLastGC number -- Frames elapsed since last GC +---@field lastMemory number -- Last recorded memory usage in MB +---@field gcCount number -- Total number of GC operations performed + +---@class GCStats +---@field gcCount number -- Total number of GC operations performed +---@field framesSinceLastGC number -- Frames elapsed since last GC +---@field currentMemoryMB number -- Current memory usage in MB +---@field strategy string -- Current GC strategy +---@field threshold number -- Memory threshold in MB + +---@class FlexLoveDependencies +---@field Context table -- Context module +---@field Theme Theme? -- Theme module +---@field Color Color -- Color module +---@field Calc Calc -- Calc module +---@field Units table -- Units module +---@field Blur table? -- Blur module +---@field ImageRenderer table? -- ImageRenderer module +---@field ImageScaler table? -- ImageScaler module +---@field NinePatch table? -- NinePatch module +---@field RoundedRect table -- RoundedRect module +---@field ImageCache table? -- ImageCache module +---@field utils table -- Utils module +---@field Grid table -- Grid module +---@field InputEvent table -- InputEvent module +---@field GestureRecognizer table? -- GestureRecognizer module +---@field StateManager StateManager -- StateManager module +---@field TextEditor table -- TextEditor module +---@field LayoutEngine LayoutEngine -- LayoutEngine module +---@field Renderer table -- Renderer module +---@field EventHandler EventHandler -- EventHandler module +---@field ScrollManager table -- ScrollManager module +---@field ErrorHandler ErrorHandler -- ErrorHandler module +---@field Performance Performance? -- Performance module +---@field Transform table? -- Transform module diff --git a/testing/__tests__/calc_test.lua b/testing/__tests__/calc_test.lua index eb7e571..2853ed0 100644 --- a/testing/__tests__/calc_test.lua +++ b/testing/__tests__/calc_test.lua @@ -227,6 +227,435 @@ function TestCalc:testRealWorldCentering() lu.assertEquals(result, 768) end +-- ============================================================================ +-- STRESS TESTS - Complex calculations and deeply nested structures +-- ============================================================================ + +--- Test deeply nested parentheses (3 levels) +function TestCalc:testDeeplyNested3Levels() + local calcObj = Calc.new("(((100px + 50px) * 2) - 100px) / 2") + lu.assertTrue(Calc.isCalc(calcObj)) + -- (((150) * 2) - 100) / 2 = (300 - 100) / 2 = 200 / 2 = 100 + local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil) + lu.assertEquals(result, 100) +end + +--- Test deeply nested parentheses (5 levels) +function TestCalc:testDeeplyNested5Levels() + local calcObj = Calc.new("(((((10px + 5px) * 2) + 10px) * 2) - 20px) / 2") + lu.assertTrue(Calc.isCalc(calcObj)) + -- (((((15) * 2) + 10) * 2) - 20) / 2 + -- ((((30) + 10) * 2) - 20) / 2 + -- (((40) * 2) - 20) / 2 + -- ((80) - 20) / 2 + -- (60) / 2 = 30 + local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil) + lu.assertEquals(result, 30) +end + +--- Test deeply nested parentheses (10 levels) +function TestCalc:testDeeplyNested10Levels() + local calcObj = Calc.new("((((((((((2px * 2) * 2) * 2) * 2) * 2) * 2) * 2) * 2) * 2) * 2)") + lu.assertTrue(Calc.isCalc(calcObj)) + -- 2 * 2^10 = 2 * 1024 = 2048 + local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil) + lu.assertEquals(result, 2048) +end + +--- Test complex multi-operation expression with all operators +function TestCalc:testComplexMultiOperationAllOperators() + local calcObj = Calc.new("100px + 50px - 20px * 2 / 4 + 30px") + lu.assertTrue(Calc.isCalc(calcObj)) + -- Precedence: 20 * 2 = 40, 40 / 4 = 10 + -- Then: 100 + 50 - 10 + 30 = 170 + local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil) + lu.assertEquals(result, 170) +end + +--- Test complex expression with mixed units and all operators +function TestCalc:testComplexMixedUnitsAllOperators() + local calcObj = Calc.new("50% + 10vw - 5vh * 2 / 4") + lu.assertTrue(Calc.isCalc(calcObj)) + -- 50% = 500 (parent 1000), 10vw = 192, 5vh = 54, 54 * 2 = 108, 108 / 4 = 27 + -- 500 + 192 - 27 = 665 + local result = Calc.resolve(calcObj, 1920, 1080, 1000, nil, nil) + lu.assertEquals(result, 665) +end + +--- Test nested parentheses with mixed operations +function TestCalc:testNestedParenthesesMixedOperations() + local calcObj = Calc.new("((100px + 50px) * (200px - 100px)) / 50px") + lu.assertTrue(Calc.isCalc(calcObj)) + -- (150 * 100) / 50 = 15000 / 50 = 300 + local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil) + lu.assertEquals(result, 300) +end + +--- Test extremely long expression with many operations +function TestCalc:testExtremelyLongExpression() + local calcObj = Calc.new("10px + 20px + 30px + 40px + 50px - 5px - 10px - 15px * 2 / 3 + 100px") + lu.assertTrue(Calc.isCalc(calcObj)) + -- 15 * 2 / 3 = 30 / 3 = 10 + -- 10 + 20 + 30 + 40 + 50 - 5 - 10 - 10 + 100 = 225 + local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil) + lu.assertEquals(result, 225) +end + +--- Test alternating operations with parentheses +function TestCalc:testAlternatingOperationsWithParentheses() + local calcObj = Calc.new("(50px + 50px) * (100px - 50px) / (25px + 25px)") + lu.assertTrue(Calc.isCalc(calcObj)) + -- (100) * (50) / (50) = 5000 / 50 = 100 + local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil) + lu.assertEquals(result, 100) +end + +--- Test very large numbers +function TestCalc:testVeryLargeNumbers() + local calcObj = Calc.new("10000px + 50000px * 2") + lu.assertTrue(Calc.isCalc(calcObj)) + -- 50000 * 2 = 100000, 10000 + 100000 = 110000 + local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil) + lu.assertEquals(result, 110000) +end + +--- Test very small decimal numbers +function TestCalc:testVerySmallDecimals() + local calcObj = Calc.new("0.1px + 0.2px + 0.3px") + lu.assertTrue(Calc.isCalc(calcObj)) + local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil) + lu.assertAlmostEquals(result, 0.6, 0.0001) +end + +--- Test negative numbers in complex expression +function TestCalc:testNegativeNumbersInComplexExpression() + local calcObj = Calc.new("(-50px + 100px) * (-2px + 5px)") + lu.assertTrue(Calc.isCalc(calcObj)) + -- (50) * (3) = 150 + local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil) + lu.assertEquals(result, 150) +end + +--- Test multiple negative numbers +function TestCalc:testMultipleNegativeNumbers() + local calcObj = Calc.new("-50px - 30px - 20px") + lu.assertTrue(Calc.isCalc(calcObj)) + -- -50 - 30 - 20 = -100 + local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil) + lu.assertEquals(result, -100) +end + +--- Test negative result from subtraction +function TestCalc:testNegativeResultFromSubtraction() + local calcObj = Calc.new("50px - 100px") + lu.assertTrue(Calc.isCalc(calcObj)) + local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil) + lu.assertEquals(result, -50) +end + +--- Test all unit types in single expression +function TestCalc:testAllUnitTypesInSingleExpression() + local calcObj = Calc.new("100px + 10% + 5vw + 5vh + 10ew + 10eh") + lu.assertTrue(Calc.isCalc(calcObj)) + -- 100 + 100 (10% of 1000) + 96 (5% of 1920) + 54 (5% of 1080) + 50 (10% of 500) + 30 (10% of 300) + -- = 100 + 100 + 96 + 54 + 50 + 30 = 430 + local result = Calc.resolve(calcObj, 1920, 1080, 1000, 500, 300) + lu.assertEquals(result, 430) +end + +--- Test precedence with multiple levels +function TestCalc:testPrecedenceWithMultipleLevels() + local calcObj = Calc.new("100px + 50px * 2 - 30px / 3") + lu.assertTrue(Calc.isCalc(calcObj)) + -- 50 * 2 = 100, 30 / 3 = 10 + -- 100 + 100 - 10 = 190 + local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil) + lu.assertEquals(result, 190) +end + +--- Test parentheses overriding precedence in complex way +function TestCalc:testParenthesesOverridingPrecedenceComplex() + local calcObj = Calc.new("(100px + 50px) * (2px + 3px) - (30px + 20px) / (5px - 3px)") + lu.assertTrue(Calc.isCalc(calcObj)) + -- (150) * (5) - (50) / (2) = 750 - 25 = 725 + local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil) + lu.assertEquals(result, 725) +end + +--- Test percentage calculations with zero parent +function TestCalc:testPercentageWithZeroParent() + local calcObj = Calc.new("50% + 100px") + lu.assertTrue(Calc.isCalc(calcObj)) + -- 50% of 0 = 0, 0 + 100 = 100 + local result = Calc.resolve(calcObj, 1920, 1080, 0, nil, nil) + lu.assertEquals(result, 100) +end + +--- Test element units without element dimensions +function TestCalc:testElementUnitsWithoutDimensions() + local calcObj = Calc.new("100ew + 50eh") + lu.assertTrue(Calc.isCalc(calcObj)) + -- Should return 0 + 0 = 0 due to missing dimensions + local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil) + lu.assertEquals(result, 0) +end + +--- Test mixed parentheses and operations at different levels +function TestCalc:testMixedParenthesesDifferentLevels() + local calcObj = Calc.new("((100px + 50px) * 2) + (200px / (10px + 10px))") + lu.assertTrue(Calc.isCalc(calcObj)) + -- ((150) * 2) + (200 / (20)) = 300 + 10 = 310 + local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil) + lu.assertEquals(result, 310) +end + +--- Test chain multiplication +function TestCalc:testChainMultiplication() + local calcObj = Calc.new("2px * 3 * 4 * 5") + lu.assertTrue(Calc.isCalc(calcObj)) + -- 2 * 3 * 4 * 5 = 120 + local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil) + lu.assertEquals(result, 120) +end + +--- Test chain division +function TestCalc:testChainDivision() + local calcObj = Calc.new("1000px / 2 / 5 / 10") + lu.assertTrue(Calc.isCalc(calcObj)) + -- 1000 / 2 / 5 / 10 = 500 / 5 / 10 = 100 / 10 = 10 + local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil) + lu.assertEquals(result, 10) +end + +--- Test fractional results +function TestCalc:testFractionalResults() + local calcObj = Calc.new("100px / 3") + lu.assertTrue(Calc.isCalc(calcObj)) + local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil) + lu.assertAlmostEquals(result, 33.333333333, 0.0001) +end + +--- Test complex viewport-based layout calculation +function TestCalc:testComplexViewportBasedLayout() + -- Simulate: margin-left = (100vw - element_width) / 2, where element is 30vw + local calcObj = Calc.new("(100vw - 30vw) / 2") + lu.assertTrue(Calc.isCalc(calcObj)) + -- (1920 - 576) / 2 = 1344 / 2 = 672 + local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil) + lu.assertEquals(result, 672) +end + +--- Test complex responsive sizing calculation +function TestCalc:testComplexResponsiveSizing() + -- Simulate: width = 100% - 20px padding on each side - 10vw margin + local calcObj = Calc.new("100% - 40px - 10vw") + lu.assertTrue(Calc.isCalc(calcObj)) + -- 100% of 1000 - 40 - 10% of 1920 = 1000 - 40 - 192 = 768 + local result = Calc.resolve(calcObj, 1920, 1080, 1000, nil, nil) + lu.assertEquals(result, 768) +end + +--- Test expression with leading negative in parentheses +function TestCalc:testLeadingNegativeInParentheses() + local calcObj = Calc.new("100px + (-50px * 2)") + lu.assertTrue(Calc.isCalc(calcObj)) + -- 100 + (-100) = 0 + local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil) + lu.assertEquals(result, 0) +end + +--- Test multiple parentheses groups at same level +function TestCalc:testMultipleParenthesesGroupsSameLevel() + local calcObj = Calc.new("(100px + 50px) + (200px - 100px) + (300px / 3)") + lu.assertTrue(Calc.isCalc(calcObj)) + -- 150 + 100 + 100 = 350 + local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil) + lu.assertEquals(result, 350) +end + +--- Test near-zero division result +function TestCalc:testNearZeroDivisionResult() + local calcObj = Calc.new("1px / 1000") + lu.assertTrue(Calc.isCalc(calcObj)) + local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil) + lu.assertAlmostEquals(result, 0.001, 0.0001) +end + +--- Test expression with only multiplication and division +function TestCalc:testOnlyMultiplicationAndDivision() + local calcObj = Calc.new("100px * 2 / 4 * 3 / 5") + lu.assertTrue(Calc.isCalc(calcObj)) + -- 100 * 2 / 4 * 3 / 5 = 200 / 4 * 3 / 5 = 50 * 3 / 5 = 150 / 5 = 30 + local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil) + lu.assertEquals(result, 30) +end + +--- Test expression with decimal percentages +function TestCalc:testDecimalPercentages() + local calcObj = Calc.new("12.5% + 37.5%") + lu.assertTrue(Calc.isCalc(calcObj)) + -- 12.5% + 37.5% = 50% of 1000 = 500 + local result = Calc.resolve(calcObj, 1920, 1080, 1000, nil, nil) + lu.assertEquals(result, 500) +end + +--- Test unitless numbers in multiplication/division +function TestCalc:testUnitlessNumbersInMultDiv() + local calcObj = Calc.new("100px * 2.5 / 0.5") + lu.assertTrue(Calc.isCalc(calcObj)) + -- 100 * 2.5 / 0.5 = 250 / 0.5 = 500 + local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil) + lu.assertEquals(result, 500) +end + +--- Test deeply nested with negative numbers +function TestCalc:testDeeplyNestedWithNegatives() + local calcObj = Calc.new("((-100px + 200px) * (-2px + 5px)) / (10px - 5px)") + lu.assertTrue(Calc.isCalc(calcObj)) + -- ((100) * (3)) / (5) = 300 / 5 = 60 + local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil) + lu.assertEquals(result, 60) +end + +--- Test asymmetric nested parentheses +function TestCalc:testAsymmetricNestedParentheses() + local calcObj = Calc.new("((100px + 50px) * 2) + 200px / 4") + lu.assertTrue(Calc.isCalc(calcObj)) + -- (150 * 2) + (200 / 4) = 300 + 50 = 350 + local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil) + lu.assertEquals(result, 350) +end + +--- Test maximum nesting with all operations +function TestCalc:testMaximumNestingAllOperations() + local calcObj = Calc.new("((((100px + 50px) - 30px) * 2) / 4)") + lu.assertTrue(Calc.isCalc(calcObj)) + -- ((((150) - 30) * 2) / 4) = (((120) * 2) / 4) = ((240) / 4) = 60 + local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil) + lu.assertEquals(result, 60) +end + +--- Test whitespace in complex expressions +function TestCalc:testWhitespaceInComplexExpression() + local calcObj = Calc.new(" ( 100px + 50px ) * ( 2px + 3px ) ") + lu.assertTrue(Calc.isCalc(calcObj)) + -- (150) * (5) = 750 + local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil) + lu.assertEquals(result, 750) +end + +-- ============================================================================ +-- ERROR CONDITION STRESS TESTS +-- ============================================================================ + +--- Test mismatched parentheses (missing closing) +function TestCalc:testMismatchedParenthesesMissingClosing() + local calcObj = Calc.new("((100px + 50px) * 2") + lu.assertTrue(Calc.isCalc(calcObj)) + -- Should handle gracefully and return 0 + local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil) + lu.assertEquals(result, 0) +end + +--- Test mismatched parentheses (missing opening) +function TestCalc:testMismatchedParenthesesMissingOpening() + local calcObj = Calc.new("100px + 50px) * 2") + lu.assertTrue(Calc.isCalc(calcObj)) + -- Should handle gracefully and return 0 + local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil) + lu.assertEquals(result, 0) +end + +--- Test empty parentheses +function TestCalc:testEmptyParentheses() + local calcObj = Calc.new("100px + ()") + lu.assertTrue(Calc.isCalc(calcObj)) + -- Should handle gracefully and return 0 + local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil) + lu.assertEquals(result, 0) +end + +--- Test consecutive operators +function TestCalc:testConsecutiveOperators() + local calcObj = Calc.new("100px ++ 50px") + lu.assertTrue(Calc.isCalc(calcObj)) + -- Should handle gracefully and return 0 + local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil) + lu.assertEquals(result, 0) +end + +--- Test trailing operator +function TestCalc:testTrailingOperator() + local calcObj = Calc.new("100px + 50px *") + lu.assertTrue(Calc.isCalc(calcObj)) + -- Should handle gracefully and return 0 + local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil) + lu.assertEquals(result, 0) +end + +--- Test leading operator (non-negative) +function TestCalc:testLeadingOperatorNonNegative() + local calcObj = Calc.new("+ 100px") + lu.assertTrue(Calc.isCalc(calcObj)) + -- Should handle gracefully and return 0 + local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil) + lu.assertEquals(result, 0) +end + +--- Test invalid unit +function TestCalc:testInvalidUnit() + local calcObj = Calc.new("100xyz + 50px") + lu.assertTrue(Calc.isCalc(calcObj)) + -- Should handle gracefully and return 0 + local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil) + lu.assertEquals(result, 0) +end + +--- Test mixed invalid syntax +function TestCalc:testMixedInvalidSyntax() + local calcObj = Calc.new("100px + * 50px") + lu.assertTrue(Calc.isCalc(calcObj)) + -- Should handle gracefully and return 0 + local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil) + lu.assertEquals(result, 0) +end + +--- Test special characters +function TestCalc:testSpecialCharacters() + local calcObj = Calc.new("100px + 50px @ 20px") + lu.assertTrue(Calc.isCalc(calcObj)) + -- Should handle gracefully and return 0 + local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil) + lu.assertEquals(result, 0) +end + +--- Test extremely long invalid expression +function TestCalc:testExtremelyLongInvalidExpression() + local calcObj = Calc.new("100px + + + + + + + + + + 50px") + lu.assertTrue(Calc.isCalc(calcObj)) + -- Should handle gracefully and return 0 + local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil) + lu.assertEquals(result, 0) +end + +--- Test division by calculated zero +function TestCalc:testDivisionByCalculatedZero() + local calcObj = Calc.new("100px / (50px - 50px)") + lu.assertTrue(Calc.isCalc(calcObj)) + -- Should handle division by zero and return 0 + local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil) + lu.assertEquals(result, 0) +end + +--- Test nested division by zero +function TestCalc:testNestedDivisionByZero() + local calcObj = Calc.new("((100px + 50px) / 0) * 2") + lu.assertTrue(Calc.isCalc(calcObj)) + -- Should handle division by zero and return 0 + local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil) + lu.assertEquals(result, 0) +end + if not _G.RUNNING_ALL_TESTS then os.exit(lu.LuaUnit.run()) end