flesh out Calc, with lsp support

This commit is contained in:
Michael Freno
2025-12-07 11:14:12 -05:00
parent 4f60e00b2e
commit 609a54b4f1
5 changed files with 719 additions and 53 deletions

View File

@@ -91,12 +91,14 @@ flexlove._LICENSE = [[
]] ]]
-- GC (Garbage Collection) configuration -- GC (Garbage Collection) configuration
---@type GCConfig
flexlove._gcConfig = { flexlove._gcConfig = {
strategy = "auto", -- "auto", "periodic", "manual", "disabled" strategy = "auto", -- "auto", "periodic", "manual", "disabled"
memoryThreshold = 100, -- MB before forcing GC memoryThreshold = 100, -- MB before forcing GC
interval = 60, -- Frames between GC steps (for periodic mode) interval = 60, -- Frames between GC steps (for periodic mode)
stepSize = 200, -- Work units per GC step (higher = more aggressive) stepSize = 200, -- Work units per GC step (higher = more aggressive)
} }
---@type GCState
flexlove._gcState = { flexlove._gcState = {
framesSinceLastGC = 0, framesSinceLastGC = 0,
lastMemory = 0, lastMemory = 0,
@@ -104,6 +106,7 @@ flexlove._gcState = {
} }
-- Deferred callback queue for operations that cannot run while Canvas is active -- Deferred callback queue for operations that cannot run while Canvas is active
---@type function[]
flexlove._deferredCallbacks = {} flexlove._deferredCallbacks = {}
-- Track accumulated delta time for immediate mode updates -- Track accumulated delta time for immediate mode updates
@@ -474,8 +477,11 @@ function flexlove.endFrame()
flexlove._Performance:resetFrameCounters() flexlove._Performance:resetFrameCounters()
end end
---@type love.Canvas?
flexlove._gameCanvas = nil flexlove._gameCanvas = nil
---@type love.Canvas?
flexlove._backdropCanvas = nil flexlove._backdropCanvas = nil
---@type {width: number, height: number}
flexlove._canvasDimensions = { width = 0, height = 0 } flexlove._canvasDimensions = { width = 0, height = 0 }
--- Render all UI elements with optional backdrop blur support for glassmorphic effects --- 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. -- of love.draw() after ALL canvases have been released.
end end
---@param element Element --- Check if element is an ancestor of target
---@param target Element ---@param element Element The potential ancestor element
---@return boolean ---@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 function isAncestor(element, target)
local current = target.parent local current = target.parent
while current do while current do
@@ -811,7 +818,7 @@ end
--- Monitor memory management behavior to diagnose performance issues and tune GC settings --- Monitor memory management behavior to diagnose performance issues and tune GC settings
--- Use this to identify memory leaks or optimize garbage collection timing --- 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() function flexlove.getGCStats()
return { return {
gcCount = flexlove._gcState.gcCount, gcCount = flexlove._gcState.gcCount,
@@ -1116,7 +1123,7 @@ end
--- height = "10vh", --- height = "10vh",
--- }) --- })
---@param expr string The calc expression (e.g., "50% - 10vw", "100px + 20%") ---@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) function flexlove.calc(expr)
return Calc.new(expr) return Calc.new(expr)
end end

View File

@@ -4,7 +4,7 @@
local Calc = {} local Calc = {}
--- Initialize Calc module with dependencies --- Initialize Calc module with dependencies
---@param deps table Dependencies: { ErrorHandler = table? } ---@param deps CalcDependencies Dependencies: { ErrorHandler = ErrorHandler? }
function Calc.init(deps) function Calc.init(deps)
Calc._ErrorHandler = deps.ErrorHandler Calc._ErrorHandler = deps.ErrorHandler
end end
@@ -24,7 +24,7 @@ local TokenType = {
--- Tokenize a calc expression string into tokens --- Tokenize a calc expression string into tokens
---@param expr string The expression to tokenize (e.g., "50% - 10vw") ---@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 ---@return string? error Error message if tokenization fails
local function tokenize(expr) local function tokenize(expr)
local tokens = {} local tokens = {}
@@ -157,13 +157,13 @@ end
--- Parser for calc expressions using recursive descent --- Parser for calc expressions using recursive descent
---@class Parser ---@class Parser
---@field tokens table Array of tokens ---@field tokens CalcToken[] Array of tokens
---@field pos number Current token position ---@field pos number Current token position
local Parser = {} local Parser = {}
Parser.__index = Parser Parser.__index = Parser
--- Create a new parser --- Create a new parser
---@param tokens table Array of tokens ---@param tokens CalcToken[] Array of tokens
---@return Parser ---@return Parser
function Parser.new(tokens) function Parser.new(tokens)
local self = setmetatable({}, Parser) local self = setmetatable({}, Parser)
@@ -173,7 +173,7 @@ function Parser.new(tokens)
end end
--- Get current token --- Get current token
---@return table token Current token ---@return CalcToken token Current token
function Parser:current() function Parser:current()
return self.tokens[self.pos] return self.tokens[self.pos]
end end
@@ -184,7 +184,7 @@ function Parser:advance()
end end
--- Parse expression (handles + and -) --- Parse expression (handles + and -)
---@return table ast Abstract syntax tree node ---@return CalcASTNode ast Abstract syntax tree node
function Parser:parseExpression() function Parser:parseExpression()
local left = self:parseTerm() local left = self:parseTerm()
@@ -203,7 +203,7 @@ function Parser:parseExpression()
end end
--- Parse term (handles * and /) --- Parse term (handles * and /)
---@return table ast Abstract syntax tree node ---@return CalcASTNode ast Abstract syntax tree node
function Parser:parseTerm() function Parser:parseTerm()
local left = self:parseFactor() local left = self:parseFactor()
@@ -222,7 +222,7 @@ function Parser:parseTerm()
end end
--- Parse factor (handles numbers and parentheses) --- Parse factor (handles numbers and parentheses)
---@return table ast Abstract syntax tree node ---@return CalcASTNode ast Abstract syntax tree node
function Parser:parseFactor() function Parser:parseFactor()
local token = self:current() local token = self:current()
@@ -247,7 +247,7 @@ function Parser:parseFactor()
end end
--- Parse the tokens into an AST --- Parse the tokens into an AST
---@return table ast Abstract syntax tree ---@return CalcASTNode ast Abstract syntax tree
function Parser:parse() function Parser:parse()
local ast = self:parseExpression() local ast = self:parseExpression()
if self:current().type ~= TokenType.EOF then if self:current().type ~= TokenType.EOF then
@@ -259,7 +259,7 @@ end
--- Create a calc expression object that can be resolved later --- Create a calc expression object that can be resolved later
--- This is the main API function that users call --- This is the main API function that users call
---@param expr string The calc expression (e.g., "50% - 10vw") ---@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) function Calc.new(expr)
-- Tokenize -- Tokenize
local tokens, err = tokenize(expr) local tokens, err = tokenize(expr)
@@ -316,7 +316,7 @@ function Calc.isCalc(value)
end end
--- Resolve a calc expression to pixel value --- 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 viewportWidth number Viewport width in pixels
---@param viewportHeight number Viewport height in pixels ---@param viewportHeight number Viewport height in pixels
---@param parentSize number? Parent dimension for percentage units ---@param parentSize number? Parent dimension for percentage units

View File

@@ -162,6 +162,7 @@ function Element.init(deps)
Element._Color = deps.Color Element._Color = deps.Color
Element._Context = deps.Context Element._Context = deps.Context
Element._Units = deps.Units Element._Units = deps.Units
Element._Calc = deps.Calc
Element._utils = deps.utils Element._utils = deps.utils
Element._InputEvent = deps.InputEvent Element._InputEvent = deps.InputEvent
Element._EventHandler = deps.EventHandler Element._EventHandler = deps.EventHandler
@@ -826,11 +827,22 @@ function Element.new(props)
local widthProp = props.width local widthProp = props.width
local tempWidth = 0 -- Temporary width for padding resolution local tempWidth = 0 -- Temporary width for padding resolution
if widthProp then 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) local value, unit = Element._Units.parse(widthProp)
self.units.width = { value = value, unit = unit } self.units.width = { value = value, unit = unit }
local parentWidth = self.parent and self.parent.width or viewportWidth local parentWidth = self.parent and self.parent.width or viewportWidth
tempWidth = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, parentWidth) 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 else
tempWidth = Element._Context.baseScale and (widthProp * scaleX) or widthProp tempWidth = Element._Context.baseScale and (widthProp * scaleX) or widthProp
self.units.width = { value = widthProp, unit = "px" } self.units.width = { value = widthProp, unit = "px" }
@@ -856,11 +868,22 @@ function Element.new(props)
local heightProp = props.height local heightProp = props.height
local tempHeight = 0 -- Temporary height for padding resolution local tempHeight = 0 -- Temporary height for padding resolution
if heightProp then 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) local value, unit = Element._Units.parse(heightProp)
self.units.height = { value = value, unit = unit } self.units.height = { value = value, unit = unit }
local parentHeight = self.parent and self.parent.height or viewportHeight local parentHeight = self.parent and self.parent.height or viewportHeight
tempHeight = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, parentHeight) 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 else
-- Apply base scaling to pixel values -- Apply base scaling to pixel values
tempHeight = Element._Context.baseScale and (heightProp * scaleY) or heightProp tempHeight = Element._Context.baseScale and (heightProp * scaleY) or heightProp
@@ -877,14 +900,25 @@ function Element.new(props)
--- child positioning --- --- child positioning ---
if props.gap then 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) local value, unit = Element._Units.parse(props.gap)
self.units.gap = { value = value, unit = unit } self.units.gap = { value = value, unit = unit }
-- Gap percentages should be relative to the element's own size, not parent -- 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 -- 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 flexDir = props.flexDirection or Element._utils.enums.FlexDirection.HORIZONTAL
local containerSize = (flexDir == Element._utils.enums.FlexDirection.HORIZONTAL) and self.width or self.height 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 else
self.gap = props.gap self.gap = props.gap
self.units.gap = { value = props.gap, unit = "px" } 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 -- For auto-sized elements, this is content width; for explicit sizing, this is border-box width
local tempPadding local tempPadding
if use9PatchPadding then 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 -- Get scaled 9-patch content padding from ThemeManager
local scaledPadding = self._themeManager:getScaledContentPadding(tempWidth, tempHeight) local scaledPadding = self._themeManager:getScaledContentPadding(tempWidth, tempHeight)
if scaledPadding then if scaledPadding then
@@ -1092,10 +1147,19 @@ function Element.new(props)
-- Handle x position with units -- Handle x position with units
if props.x then 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) local value, unit = Element._Units.parse(props.x)
self.units.x = { value = value, unit = unit } 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 else
-- Apply base scaling to pixel positions -- Apply base scaling to pixel positions
self.x = Element._Context.baseScale and (props.x * scaleX) or props.x 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 -- Handle y position with units
if props.y then 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) local value, unit = Element._Units.parse(props.y)
self.units.y = { value = value, unit = unit } 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 else
-- Apply base scaling to pixel positions -- Apply base scaling to pixel positions
self.y = Element._Context.baseScale and (props.y * scaleY) or props.y 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 -- Handle x position with units
if props.x then 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) local value, unit = Element._Units.parse(props.x)
self.units.x = { value = value, unit = unit } self.units.x = { value = value, unit = unit }
local parentWidth = self.parent.width local parentWidth = self.parent.width
local offsetX = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, parentWidth) 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 self.x = baseX + offsetX
else else
-- Apply base scaling to pixel positions -- Apply base scaling to pixel positions
@@ -1200,11 +1281,19 @@ function Element.new(props)
-- Handle y position with units -- Handle y position with units
if props.y then 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) local value, unit = Element._Units.parse(props.y)
self.units.y = { value = value, unit = unit } self.units.y = { value = value, unit = unit }
local parentHeight = self.parent.height local parentHeight = self.parent.height
local offsetY = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, parentHeight) 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 self.y = baseY + offsetY
else else
-- Apply base scaling to pixel positions -- Apply base scaling to pixel positions
@@ -1225,11 +1314,19 @@ function Element.new(props)
local baseY = self.parent.y + self.parent.padding.top local baseY = self.parent.y + self.parent.padding.top
if props.x then 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) local value, unit = Element._Units.parse(props.x)
self.units.x = { value = value, unit = unit } self.units.x = { value = value, unit = unit }
local parentWidth = self.parent.width local parentWidth = self.parent.width
local offsetX = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, parentWidth) 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 self.x = baseX + offsetX
else else
-- Apply base scaling to pixel offsets -- Apply base scaling to pixel offsets
@@ -1243,11 +1340,19 @@ function Element.new(props)
end end
if props.y then 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) local value, unit = Element._Units.parse(props.y)
self.units.y = { value = value, unit = unit } self.units.y = { value = value, unit = unit }
parentHeight = self.parent.height parentHeight = self.parent.height
local offsetY = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, parentHeight) 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 self.y = baseY + offsetY
else else
-- Apply base scaling to pixel offsets -- 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 positioning properties for ALL elements (with or without parent)
-- Handle top positioning with units -- Handle top positioning with units
if props.top then 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) local value, unit = Element._Units.parse(props.top)
self.units.top = { value = value, unit = unit } 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 else
self.top = props.top self.top = props.top
self.units.top = { value = props.top, unit = "px" } self.units.top = { value = props.top, unit = "px" }
@@ -1298,10 +1412,19 @@ function Element.new(props)
-- Handle right positioning with units -- Handle right positioning with units
if props.right then 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) local value, unit = Element._Units.parse(props.right)
self.units.right = { value = value, unit = unit } 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 else
self.right = props.right self.right = props.right
self.units.right = { value = props.right, unit = "px" } self.units.right = { value = props.right, unit = "px" }
@@ -1313,10 +1436,19 @@ function Element.new(props)
-- Handle bottom positioning with units -- Handle bottom positioning with units
if props.bottom then 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) local value, unit = Element._Units.parse(props.bottom)
self.units.bottom = { value = value, unit = unit } 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 else
self.bottom = props.bottom self.bottom = props.bottom
self.units.bottom = { value = props.bottom, unit = "px" } self.units.bottom = { value = props.bottom, unit = "px" }
@@ -1328,10 +1460,19 @@ function Element.new(props)
-- Handle left positioning with units -- Handle left positioning with units
if props.left then 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) local value, unit = Element._Units.parse(props.left)
self.units.left = { value = value, unit = unit } 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 else
self.left = props.left self.left = props.left
self.units.left = { value = props.left, unit = "px" } self.units.left = { value = props.left, unit = "px" }
@@ -1378,9 +1519,18 @@ function Element.new(props)
-- Handle columnGap and rowGap -- Handle columnGap and rowGap
if props.columnGap then 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) 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 else
self.columnGap = props.columnGap self.columnGap = props.columnGap
end end
@@ -1389,9 +1539,18 @@ function Element.new(props)
end end
if props.rowGap then 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) 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 else
self.rowGap = props.rowGap self.rowGap = props.rowGap
end end

View File

@@ -34,24 +34,24 @@ local AnimationProps = {}
---@class ElementProps ---@class ElementProps
---@field id string? -- Unique identifier for the element (auto-generated in immediate mode if not provided) ---@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 parent Element? -- Parent element for hierarchical structure
---@field x number|string? -- X 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? -- Y coordinate of the element (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 z number? -- Z-index for layering (default: 0)
---@field width number|string? -- Width of the element (default: calculated automatically) ---@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? -- Height of the element (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? -- Offset from top edge (CSS-style positioning) ---@field top number|string|CalcObject? -- Offset from top edge: number (px), string ("50%", "10vh"), or CalcObject (CSS-style positioning)
---@field right number|string? -- Offset from right edge (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? -- Offset from bottom edge (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? -- Offset from left edge (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 border Border? -- Border configuration for the element
---@field borderColor Color? -- Color of the border (default: black) ---@field borderColor Color? -- Color of the border (default: black)
---@field opacity number? -- Element opacity 0-1 (default: 1) ---@field opacity number? -- Element opacity 0-1 (default: 1)
---@field visibility "visible"|"hidden"? -- Element visibility (default: "visible") ---@field visibility "visible"|"hidden"? -- Element visibility (default: "visible")
---@field backgroundColor Color? -- Background color (default: transparent) ---@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 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 gap number|string|CalcObject? -- Space between children elements: number (px), string ("50%", "10vw"), or CalcObject from FlexLove.calc() (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 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|{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 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 text string? -- Text content to display (default: nil)
---@field textAlign TextAlign? -- Alignment of the text content (default: START) ---@field textAlign TextAlign? -- Alignment of the text content (default: START)
---@field textColor Color? -- Color of the text content (default: black or theme text color) ---@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 transition TransitionProps? -- Transition settings for animations
---@field gridRows number? -- Number of rows in the grid (default: 1) ---@field gridRows number? -- Number of rows in the grid (default: 1)
---@field gridColumns number? -- Number of columns 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 columnGap number|string|CalcObject? -- Gap between grid columns: number (px), string ("50%", "10vw"), or CalcObject from FlexLove.calc() (default: 0)
---@field rowGap number|string? -- Gap between grid rows (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 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 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) ---@field disabled boolean? -- Whether the element is disabled (default: false)
@@ -211,3 +211,74 @@ local FlexLoveConfig = {}
---@field _backdropBlurQuality number? ---@field _backdropBlurQuality number?
---@field _contentBlurRadius number? ---@field _contentBlurRadius number?
---@field _contentBlurQuality 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

View File

@@ -227,6 +227,435 @@ function TestCalc:testRealWorldCentering()
lu.assertEquals(result, 768) lu.assertEquals(result, 768)
end 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 if not _G.RUNNING_ALL_TESTS then
os.exit(lu.LuaUnit.run()) os.exit(lu.LuaUnit.run())
end end