flesh out Calc, with lsp support
This commit is contained in:
17
FlexLove.lua
17
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user