calc module

This commit is contained in:
Michael Freno
2025-12-07 00:56:55 -05:00
parent f532837cf3
commit 502eeb1e11
23 changed files with 870 additions and 68 deletions

View File

@@ -212,7 +212,7 @@ jobs:
|---------|------|-------------|---------|
| **Minimal** | ~70% | Core functionality only | `flexlove-minimal-v${{ steps.version.outputs.version }}.zip` |
| **Slim** | ~80% | + Animation and Image support | `flexlove-slim-v${{ steps.version.outputs.version }}.zip` |
| **Default** | ~95% | + Theme and Blur effects | `flexlove-default-v${{ steps.version.outputs.version }}.zip` |
| **Default** | ~95% | + Theme, Calc, Blur effects | `flexlove-default-v${{ steps.version.outputs.version }}.zip` |
| **Full** | 100% | All modules including debugging tools | `flexlove-full-v${{ steps.version.outputs.version }}.zip` |
**Choose the profile that matches your needs!** See the [Build Profiles Documentation](https://github.com/${{ github.repository }}/blob/main/docs/BUILD_PROFILES.md) for detailed module listings.

View File

@@ -20,6 +20,7 @@ end
-- Required core modules
local utils = req("utils")
local Calc = req("Calc")
local Units = req("Units")
local Context = req("Context")
---@type StateManager
@@ -176,7 +177,8 @@ function flexlove.init(config)
end
-- Initialize required modules
Units.init({ Context = Context, ErrorHandler = flexlove._ErrorHandler })
Calc.init({ ErrorHandler = flexlove._ErrorHandler })
Units.init({ Context = Context, ErrorHandler = flexlove._ErrorHandler, Calc = Calc })
Color.init({ ErrorHandler = flexlove._ErrorHandler, FFI = flexlove._FFI })
utils.init({ ErrorHandler = flexlove._ErrorHandler })
@@ -197,6 +199,7 @@ function flexlove.init(config)
Context = Context,
Theme = Theme,
Color = Color,
Calc = Calc,
Units = Units,
Blur = Blur,
ImageRenderer = ImageRenderer,
@@ -1103,6 +1106,21 @@ function flexlove.getStateStats()
return StateManager.getStats()
end
--- Create a calc() expression for dynamic CSS-like calculations
--- Use this to create responsive layouts that adapt to viewport and parent dimensions
--- @usage
--- local button = FlexLove.new({
--- x = FlexLove.calc("50% - 10vw"),
--- y = FlexLove.calc("50% - 5vh"),
--- width = "20vw",
--- 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
function flexlove.calc(expr)
return Calc.new(expr)
end
flexlove.Animation = Animation
flexlove.Color = Color
flexlove.Theme = Theme

View File

@@ -110,25 +110,6 @@ FlexLöve supports optional modules to reduce bundle size for different use case
- **Default (~95%)** - Adds themes, blur effects, and gestures
- **Full (100%)** - Everything including performance monitoring
### Example: Minimal Build
For a lightweight build, exclude these optional module files:
```
modules/Animation.lua
modules/Theme.lua
modules/Blur.lua
modules/ImageRenderer.lua
modules/ImageScaler.lua
modules/ImageCache.lua
modules/NinePatch.lua
modules/GestureRecognizer.lua
modules/Performance.lua
```
The library automatically detects missing modules and provides safe no-op stubs. No code changes needed!
📖 **See [BUILD_PROFILES.md](./docs/BUILD_PROFILES.md) and [MODULE_DEPENDENCIES.md](./docs/MODULE_DEPENDENCIES.md) for detailed information.**
## Documentation
📚 **[View Full API Documentation](https://mikefreno.github.io/FlexLove/api.html)**
@@ -574,6 +555,39 @@ local element = FlexLove.new({
})
```
#### Dynamic Calculations with calc()
Use `calc()` for CSS-like dynamic calculations in layout properties:
```lua
-- Center a button horizontally (accounting for its width)
local button = FlexLove.new({
x = FlexLove.calc("50% - 10vw"), -- Centers a 20vw wide button
y = "50vh",
width = "20vw",
height = "10vh",
text = "Centered Button"
})
-- Complex calculations with multiple operations
local sidebar = FlexLove.new({
width = FlexLove.calc("100vw - 300px"), -- Full width minus fixed sidebar
height = FlexLove.calc("100vh - 50px"), -- Full height minus header
x = "300px",
y = "50px"
})
-- Using parentheses for order of operations
local panel = FlexLove.new({
width = FlexLove.calc("(100vw - 40px) / 3"), -- Three equal columns with 40px total padding
padding = { left = "10px", right = "10px" }
})
```
**Supported operations:** `+`, `-`, `*`, `/`
**Supported units:** `px`, `%`, `vw`, `vh`, `ew`, `eh`
**Note:** Element width/height units (`ew`, `eh`) cannot be used in position calculations (`x`, `y`) due to circular dependencies.
### Animations
Create smooth transitions:

View File

@@ -174,10 +174,8 @@ Each profile package includes:
|---------|---------|------------------|
| **Minimal** | 19 core modules | ~70% of full |
| **Slim** | 24 modules | ~80% of full |
| **Default** | 27 modules + themes | ~95% of full |
| **Full** | 29 modules + themes | 100% |
**Note:** All profiles now include UTF8.lua for Lua 5.1+ compatibility.
| **Default** | 28 modules + theme examples | ~95% of full |
| **Full** | 30 modules + theme examples | 100% |
Users who want examples, documentation source, or development tools should clone the full repository.

404
modules/Calc.lua Normal file
View File

@@ -0,0 +1,404 @@
--- Utility module for parsing and evaluating CSS-like calc() expressions
--- Supports arithmetic operations (+, -, *, /) with mixed units (px, %, vw, vh, ew, eh)
---@class Calc
local Calc = {}
--- Initialize Calc module with dependencies
---@param deps table Dependencies: { ErrorHandler = table? }
function Calc.init(deps)
Calc._ErrorHandler = deps.ErrorHandler
end
--- Token types for lexical analysis
local TokenType = {
NUMBER = "NUMBER",
UNIT = "UNIT",
PLUS = "PLUS",
MINUS = "MINUS",
MULTIPLY = "MULTIPLY",
DIVIDE = "DIVIDE",
LPAREN = "LPAREN",
RPAREN = "RPAREN",
EOF = "EOF",
}
--- 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 string? error Error message if tokenization fails
local function tokenize(expr)
local tokens = {}
local i = 1
local len = #expr
while i <= len do
local char = expr:sub(i, i)
-- Skip whitespace
if char:match("%s") then
i = i + 1
-- Number (including decimals, but NOT negative - handled separately below)
elseif char:match("%d") or (char == "." and expr:sub(i + 1, i + 1):match("%d")) then
local numStr = ""
-- Parse integer and decimal parts
while i <= len and (expr:sub(i, i):match("%d") or expr:sub(i, i) == ".") do
numStr = numStr .. expr:sub(i, i)
i = i + 1
end
local num = tonumber(numStr)
if not num then
return nil, "Invalid number: " .. numStr
end
-- Check for unit following the number
local unitStr = ""
while i <= len and expr:sub(i, i):match("[%a%%]") do
unitStr = unitStr .. expr:sub(i, i)
i = i + 1
end
-- Default to px if no unit
if unitStr == "" then
unitStr = "px"
end
-- Validate unit
local validUnits = { px = true, ["%"] = true, vw = true, vh = true, ew = true, eh = true }
if not validUnits[unitStr] then
return nil, "Invalid unit: " .. unitStr
end
table.insert(tokens, {
type = TokenType.NUMBER,
value = num,
unit = unitStr,
})
-- Operators
elseif char == "+" then
table.insert(tokens, { type = TokenType.PLUS })
i = i + 1
elseif char == "-" then
-- Check if this is a negative number or subtraction
-- It's a negative number if previous token is an operator or opening paren
local prevToken = tokens[#tokens]
if not prevToken or prevToken.type == TokenType.PLUS or prevToken.type == TokenType.MINUS
or prevToken.type == TokenType.MULTIPLY or prevToken.type == TokenType.DIVIDE
or prevToken.type == TokenType.LPAREN then
-- This is a negative number, continue to number parsing
local numStr = "-"
i = i + 1
-- Parse integer and decimal parts
while i <= len and (expr:sub(i, i):match("%d") or expr:sub(i, i) == ".") do
numStr = numStr .. expr:sub(i, i)
i = i + 1
end
local num = tonumber(numStr)
if not num then
return nil, "Invalid number: " .. numStr
end
-- Check for unit following the number
local unitStr = ""
while i <= len and expr:sub(i, i):match("[%a%%]") do
unitStr = unitStr .. expr:sub(i, i)
i = i + 1
end
-- Default to px if no unit
if unitStr == "" then
unitStr = "px"
end
-- Validate unit
local validUnits = { px = true, ["%"] = true, vw = true, vh = true, ew = true, eh = true }
if not validUnits[unitStr] then
return nil, "Invalid unit: " .. unitStr
end
table.insert(tokens, {
type = TokenType.NUMBER,
value = num,
unit = unitStr,
})
else
-- This is subtraction operator
table.insert(tokens, { type = TokenType.MINUS })
i = i + 1
end
elseif char == "*" then
table.insert(tokens, { type = TokenType.MULTIPLY })
i = i + 1
elseif char == "/" then
table.insert(tokens, { type = TokenType.DIVIDE })
i = i + 1
elseif char == "(" then
table.insert(tokens, { type = TokenType.LPAREN })
i = i + 1
elseif char == ")" then
table.insert(tokens, { type = TokenType.RPAREN })
i = i + 1
else
return nil, "Unexpected character: " .. char
end
end
table.insert(tokens, { type = TokenType.EOF })
return tokens
end
--- Parser for calc expressions using recursive descent
---@class Parser
---@field tokens table Array of tokens
---@field pos number Current token position
local Parser = {}
Parser.__index = Parser
--- Create a new parser
---@param tokens table Array of tokens
---@return Parser
function Parser.new(tokens)
local self = setmetatable({}, Parser)
self.tokens = tokens
self.pos = 1
return self
end
--- Get current token
---@return table token Current token
function Parser:current()
return self.tokens[self.pos]
end
--- Advance to next token
function Parser:advance()
self.pos = self.pos + 1
end
--- Parse expression (handles + and -)
---@return table ast Abstract syntax tree node
function Parser:parseExpression()
local left = self:parseTerm()
while self:current().type == TokenType.PLUS or self:current().type == TokenType.MINUS do
local op = self:current().type
self:advance()
local right = self:parseTerm()
left = {
type = op == TokenType.PLUS and "add" or "subtract",
left = left,
right = right,
}
end
return left
end
--- Parse term (handles * and /)
---@return table ast Abstract syntax tree node
function Parser:parseTerm()
local left = self:parseFactor()
while self:current().type == TokenType.MULTIPLY or self:current().type == TokenType.DIVIDE do
local op = self:current().type
self:advance()
local right = self:parseFactor()
left = {
type = op == TokenType.MULTIPLY and "multiply" or "divide",
left = left,
right = right,
}
end
return left
end
--- Parse factor (handles numbers and parentheses)
---@return table ast Abstract syntax tree node
function Parser:parseFactor()
local token = self:current()
if token.type == TokenType.NUMBER then
self:advance()
return {
type = "number",
value = token.value,
unit = token.unit,
}
elseif token.type == TokenType.LPAREN then
self:advance()
local expr = self:parseExpression()
if self:current().type ~= TokenType.RPAREN then
error("Expected closing parenthesis")
end
self:advance()
return expr
else
error("Unexpected token: " .. token.type)
end
end
--- Parse the tokens into an AST
---@return table ast Abstract syntax tree
function Parser:parse()
local ast = self:parseExpression()
if self:current().type ~= TokenType.EOF then
error("Unexpected tokens after expression")
end
return ast
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
function Calc.new(expr)
-- Tokenize
local tokens, err = tokenize(expr)
if not tokens then
if Calc._ErrorHandler then
Calc._ErrorHandler:warn("Calc", "VAL_006", {
expression = expr,
error = err,
})
end
-- Return a fallback calc object that resolves to 0
return {
_isCalc = true,
_expr = expr,
_ast = nil,
_error = err,
}
end
-- Parse
local parser = Parser.new(tokens)
local success, ast = pcall(function()
return parser:parse()
end)
if not success then
if Calc._ErrorHandler then
Calc._ErrorHandler:warn("Calc", "VAL_006", {
expression = expr,
error = ast, -- ast contains error message on failure
})
end
-- Return a fallback calc object that resolves to 0
return {
_isCalc = true,
_expr = expr,
_ast = nil,
_error = ast,
}
end
return {
_isCalc = true,
_expr = expr,
_ast = ast,
}
end
--- Check if a value is a calc expression
---@param value any The value to check
---@return boolean isCalc True if value is a calc expression
function Calc.isCalc(value)
return type(value) == "table" and value._isCalc == true
end
--- Resolve a calc expression to pixel value
---@param calcObj table 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
---@param elementWidth number? Element width for ew units
---@param elementHeight number? Element height for eh units
---@return number resolvedValue Resolved pixel value
function Calc.resolve(calcObj, viewportWidth, viewportHeight, parentSize, elementWidth, elementHeight)
if not calcObj._ast then
-- Error during parsing, return 0
return 0
end
--- Evaluate AST node recursively
---@param node table AST node
---@return number value Evaluated value in pixels
local function evaluate(node)
if node.type == "number" then
-- Convert unit to pixels
local value = node.value
local unit = node.unit
if unit == "px" then
return value
elseif unit == "%" then
if not parentSize then
if Calc._ErrorHandler then
Calc._ErrorHandler:warn("Calc", "LAY_003", {
unit = "%",
issue = "parent dimension not available",
})
end
return 0
end
return (value / 100) * parentSize
elseif unit == "vw" then
return (value / 100) * viewportWidth
elseif unit == "vh" then
return (value / 100) * viewportHeight
elseif unit == "ew" then
if not elementWidth then
if Calc._ErrorHandler then
Calc._ErrorHandler:warn("Calc", "LAY_003", {
unit = "ew",
issue = "element width not available",
})
end
return 0
end
return (value / 100) * elementWidth
elseif unit == "eh" then
if not elementHeight then
if Calc._ErrorHandler then
Calc._ErrorHandler:warn("Calc", "LAY_003", {
unit = "eh",
issue = "element height not available",
})
end
return 0
end
return (value / 100) * elementHeight
else
return 0
end
elseif node.type == "add" then
return evaluate(node.left) + evaluate(node.right)
elseif node.type == "subtract" then
return evaluate(node.left) - evaluate(node.right)
elseif node.type == "multiply" then
return evaluate(node.left) * evaluate(node.right)
elseif node.type == "divide" then
local divisor = evaluate(node.right)
if divisor == 0 then
if Calc._ErrorHandler then
Calc._ErrorHandler:warn("Calc", "VAL_006", {
expression = calcObj._expr,
error = "Division by zero",
})
end
return 0
end
return evaluate(node.left) / divisor
else
return 0
end
end
return evaluate(calcObj._ast)
end
return Calc

View File

@@ -1092,7 +1092,7 @@ function Element.new(props)
-- Handle x position with units
if props.x then
if type(props.x) == "string" then
if type(props.x) == "string" or type(props.x) == "table" 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)
@@ -1108,7 +1108,7 @@ function Element.new(props)
-- Handle y position with units
if props.y then
if type(props.y) == "string" then
if type(props.y) == "string" or type(props.y) == "table" 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)
@@ -1181,7 +1181,7 @@ function Element.new(props)
-- Handle x position with units
if props.x then
if type(props.x) == "string" then
if type(props.x) == "string" or type(props.x) == "table" then
local value, unit = Element._Units.parse(props.x)
self.units.x = { value = value, unit = unit }
local parentWidth = self.parent.width
@@ -1200,7 +1200,7 @@ function Element.new(props)
-- Handle y position with units
if props.y then
if type(props.y) == "string" then
if type(props.y) == "string" or type(props.y) == "table" then
local value, unit = Element._Units.parse(props.y)
self.units.y = { value = value, unit = unit }
local parentHeight = self.parent.height
@@ -1225,7 +1225,7 @@ function Element.new(props)
local baseY = self.parent.y + self.parent.padding.top
if props.x then
if type(props.x) == "string" then
if type(props.x) == "string" or type(props.x) == "table" then
local value, unit = Element._Units.parse(props.x)
self.units.x = { value = value, unit = unit }
local parentWidth = self.parent.width
@@ -1243,7 +1243,7 @@ function Element.new(props)
end
if props.y then
if type(props.y) == "string" then
if type(props.y) == "string" or type(props.y) == "table" then
local value, unit = Element._Units.parse(props.y)
self.units.y = { value = value, unit = unit }
parentHeight = self.parent.height

View File

@@ -46,8 +46,8 @@ local ErrorCodes = {
VAL_006 = {
code = "FLEXLOVE_VAL_006",
category = "VAL",
description = "Invalid file path",
suggestion = "Check that the file path is correct and the file exists",
description = "Invalid calc() expression or calculation error",
suggestion = "Check calc() syntax and ensure no division by zero. Format: calc('value1 operator value2') with operators: +, -, *, / and units: px, %, vw, vh, ew, eh",
},
VAL_007 = {
code = "FLEXLOVE_VAL_007",

View File

@@ -3,29 +3,36 @@
---@class Units
---@field _Context table? Context module dependency
---@field _ErrorHandler table? ErrorHandler module dependency
---@field _Calc table? Calc module dependency
local Units = {}
--- Initialize Units module with dependencies
---@param deps table Dependencies: { Context = table?, ErrorHandler = table? }
---@param deps table Dependencies: { Context = table?, ErrorHandler = table?, Calc = table? }
function Units.init(deps)
Units._Context = deps.Context
Units._ErrorHandler = deps.ErrorHandler
Units._Calc = deps.Calc
end
--- Parse a unit value into numeric value and unit type
--- Supports: px (pixels), % (percentage), vw/vh (viewport), ew/eh (element)
---@param value string|number The value to parse (e.g., "50px", "10%", "2vw", 100)
---@return number numericValue The numeric portion of the value
---@return string unitType The unit type ("px", "%", "vw", "vh", "ew", "eh")
--- Supports: px (pixels), % (percentage), vw/vh (viewport), ew/eh (element), and calc() expressions
---@param value string|number|table The value to parse (e.g., "50px", "10%", "2vw", 100, or calc object)
---@return number|table numericValue The numeric portion of the value or calc object
---@return string unitType The unit type ("px", "%", "vw", "vh", "ew", "eh", "calc")
function Units.parse(value)
-- Check if value is a calc expression
if Units._Calc and Units._Calc.isCalc(value) then
return value, "calc"
end
if type(value) == "number" then
return value, "px"
end
if type(value) ~= "string" then
if type(value) ~= "string" and type(value) ~= "table" then
Units._ErrorHandler:warn("Units", "VAL_001", {
property = "unit value",
expected = "string or number",
expected = "string, number, or calc object",
got = type(value),
})
return 0, "px"
@@ -87,15 +94,28 @@ function Units.parse(value)
end
--- Convert relative units to absolute pixel values
--- Resolves %, vw, vh units based on viewport and parent dimensions
---@param value number Numeric value to convert
---@param unit string Unit type ("px", "%", "vw", "vh", "ew", "eh")
--- Resolves %, vw, vh units based on viewport and parent dimensions, and evaluates calc() expressions
---@param value number|table Numeric value to convert or calc object
---@param unit string Unit type ("px", "%", "vw", "vh", "ew", "eh", "calc")
---@param viewportWidth number Current viewport width in pixels
---@param viewportHeight number Current viewport height in pixels
---@param parentSize number? Required for percentage units (parent dimension in pixels)
---@param elementWidth number? Required for ew units in calc expressions (element width in pixels)
---@param elementHeight number? Required for eh units in calc expressions (element height in pixels)
---@return number resolvedValue Resolved pixel value
function Units.resolve(value, unit, viewportWidth, viewportHeight, parentSize)
if unit == "px" then
function Units.resolve(value, unit, viewportWidth, viewportHeight, parentSize, elementWidth, elementHeight)
if unit == "calc" then
-- Resolve calc expression
if Units._Calc then
return Units._Calc.resolve(value, viewportWidth, viewportHeight, parentSize, elementWidth, elementHeight)
else
Units._ErrorHandler:warn("Units", "VAL_006", {
unit = "calc",
issue = "Calc module not available",
})
return 0
end
elseif unit == "px" then
return value
elseif unit == "%" then
if not parentSize then
@@ -113,7 +133,7 @@ function Units.resolve(value, unit, viewportWidth, viewportHeight, parentSize)
else
Units._ErrorHandler:warn("Units", "VAL_005", {
unit = unit,
validUnits = "px, %, vw, vh, ew, eh",
validUnits = "px, %, vw, vh, ew, eh, calc",
})
return 0
end
@@ -169,26 +189,26 @@ function Units.resolveSpacing(spacingProps, parentWidth, parentHeight)
local horizontal = spacingProps.horizontal
if vertical then
if type(vertical) == "string" then
if type(vertical) == "string" or (Units._Calc and Units._Calc.isCalc(vertical)) then
local value, unit = Units.parse(vertical)
vertical = Units.resolve(value, unit, viewportWidth, viewportHeight, parentHeight)
vertical = Units.resolve(value, unit, viewportWidth, viewportHeight, parentHeight, nil, nil)
end
end
if horizontal then
if type(horizontal) == "string" then
if type(horizontal) == "string" or (Units._Calc and Units._Calc.isCalc(horizontal)) then
local value, unit = Units.parse(horizontal)
horizontal = Units.resolve(value, unit, viewportWidth, viewportHeight, parentWidth)
horizontal = Units.resolve(value, unit, viewportWidth, viewportHeight, parentWidth, nil, nil)
end
end
for _, side in ipairs({ "top", "right", "bottom", "left" }) do
local value = spacingProps[side]
if value then
if type(value) == "string" then
if type(value) == "string" or (Units._Calc and Units._Calc.isCalc(value)) then
local numValue, unit = Units.parse(value)
local parentSize = (side == "top" or side == "bottom") and parentHeight or parentWidth
result[side] = Units.resolve(numValue, unit, viewportWidth, viewportHeight, parentSize)
result[side] = Units.resolve(numValue, unit, viewportWidth, viewportHeight, parentSize, nil, nil)
else
result[side] = value
end
@@ -205,10 +225,15 @@ function Units.resolveSpacing(spacingProps, parentWidth, parentHeight)
end
--- Validate a unit string format
--- Checks if the string can be successfully parsed as a valid unit
---@param unitStr string The unit string to validate (e.g., "50px", "10%")
--- Checks if the string can be successfully parsed as a valid unit or calc expression
---@param unitStr string|table The unit string to validate (e.g., "50px", "10%") or calc object
---@return boolean isValid True if the unit string is valid, false otherwise
function Units.isValid(unitStr)
-- Check if it's a calc expression
if Units._Calc and Units._Calc.isCalc(unitStr) then
return true
end
if type(unitStr) ~= "string" then
return false
end

View File

@@ -61,10 +61,10 @@ get_modules() {
echo "utils.lua Units.lua Context.lua StateManager.lua ErrorHandler.lua Color.lua InputEvent.lua TextEditor.lua LayoutEngine.lua Renderer.lua EventHandler.lua ScrollManager.lua Element.lua RoundedRect.lua Grid.lua ModuleLoader.lua types.lua FFI.lua UTF8.lua Animation.lua NinePatch.lua ImageRenderer.lua ImageScaler.lua ImageCache.lua"
;;
default)
echo "utils.lua Units.lua Context.lua StateManager.lua ErrorHandler.lua Color.lua InputEvent.lua TextEditor.lua LayoutEngine.lua Renderer.lua EventHandler.lua ScrollManager.lua Element.lua RoundedRect.lua Grid.lua ModuleLoader.lua types.lua FFI.lua UTF8.lua Animation.lua NinePatch.lua ImageRenderer.lua ImageScaler.lua ImageCache.lua Theme.lua Blur.lua GestureRecognizer.lua"
echo "utils.lua Units.lua Calc.lua Context.lua StateManager.lua ErrorHandler.lua Color.lua InputEvent.lua TextEditor.lua LayoutEngine.lua Renderer.lua EventHandler.lua ScrollManager.lua Element.lua RoundedRect.lua Grid.lua ModuleLoader.lua types.lua FFI.lua UTF8.lua Animation.lua NinePatch.lua ImageRenderer.lua ImageScaler.lua ImageCache.lua Theme.lua Blur.lua GestureRecognizer.lua"
;;
full)
echo "utils.lua Units.lua Context.lua StateManager.lua ErrorHandler.lua Color.lua InputEvent.lua TextEditor.lua LayoutEngine.lua Renderer.lua EventHandler.lua ScrollManager.lua Element.lua RoundedRect.lua Grid.lua ModuleLoader.lua types.lua FFI.lua UTF8.lua Animation.lua NinePatch.lua ImageRenderer.lua ImageScaler.lua ImageCache.lua Theme.lua Blur.lua GestureRecognizer.lua Performance.lua MemoryScanner.lua"
echo "utils.lua Units.lua Calc.lua Context.lua StateManager.lua ErrorHandler.lua Color.lua InputEvent.lua TextEditor.lua LayoutEngine.lua Renderer.lua EventHandler.lua ScrollManager.lua Element.lua RoundedRect.lua Grid.lua ModuleLoader.lua types.lua FFI.lua UTF8.lua Animation.lua NinePatch.lua ImageRenderer.lua ImageScaler.lua ImageCache.lua Theme.lua Blur.lua GestureRecognizer.lua Performance.lua MemoryScanner.lua"
;;
esac
}

View File

@@ -11,6 +11,15 @@ local ErrorHandler = require("modules.ErrorHandler")
-- Initialize ErrorHandler
ErrorHandler.init({})
-- Setup package loader to map FlexLove.modules.X to modules/X
local originalSearchers = package.searchers or package.loaders
table.insert(originalSearchers, 2, function(modname)
if modname:match("^FlexLove%.modules%.") then
local moduleName = modname:gsub("^FlexLove%.modules%.", "")
return function() return require("modules." .. moduleName) end
end
end)
-- Load FlexLove which properly initializes all dependencies
local FlexLove = require("FlexLove")

View File

@@ -0,0 +1,232 @@
---@diagnostic disable: undefined-global, undefined-field
local lu = require("testing.luaunit")
-- Mock love globals for testing environment
_G.love = _G.love or {}
_G.love.graphics = _G.love.graphics or {}
_G.love.graphics.getDimensions = function()
return 1920, 1080
end
_G.love.window = _G.love.window or {}
_G.love.window.getMode = function()
return 1920, 1080
end
-- Load Calc module directly
local Calc = require("modules.Calc")
local ErrorHandler = require("modules.ErrorHandler")
-- Initialize with error handler
ErrorHandler.init({})
Calc.init({ ErrorHandler = ErrorHandler })
---@class TestCalc
TestCalc = {}
function TestCalc:setUp()
-- Fresh initialization for each test
end
function TestCalc:tearDown()
-- Cleanup after each test
end
--- Test basic arithmetic: addition
function TestCalc:testBasicAddition()
local calcObj = Calc.new("100px + 50px")
lu.assertTrue(Calc.isCalc(calcObj))
local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil)
lu.assertEquals(result, 150)
end
--- Test basic arithmetic: subtraction
function TestCalc:testBasicSubtraction()
local calcObj = Calc.new("100px - 30px")
lu.assertTrue(Calc.isCalc(calcObj))
local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil)
lu.assertEquals(result, 70)
end
--- Test basic arithmetic: multiplication
function TestCalc:testBasicMultiplication()
local calcObj = Calc.new("10px * 5")
lu.assertTrue(Calc.isCalc(calcObj))
local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil)
lu.assertEquals(result, 50)
end
--- Test basic arithmetic: division
function TestCalc:testBasicDivision()
local calcObj = Calc.new("100px / 4")
lu.assertTrue(Calc.isCalc(calcObj))
local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil)
lu.assertEquals(result, 25)
end
--- Test negative numbers
function TestCalc:testNegativeNumbers()
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 decimal numbers
function TestCalc:testDecimalNumbers()
local calcObj = Calc.new("10.5px + 5.5px")
lu.assertTrue(Calc.isCalc(calcObj))
local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil)
lu.assertEquals(result, 16)
end
--- Test percentage units
function TestCalc:testPercentageUnits()
local calcObj = Calc.new("50% + 25%")
lu.assertTrue(Calc.isCalc(calcObj))
local result = Calc.resolve(calcObj, 1920, 1080, 1000, nil, nil) -- parent size = 1000
lu.assertEquals(result, 750) -- 50% of 1000 + 25% of 1000 = 500 + 250
end
--- Test viewport width units (vw)
function TestCalc:testViewportWidthUnits()
local calcObj = Calc.new("50vw - 10vw")
lu.assertTrue(Calc.isCalc(calcObj))
local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil)
lu.assertEquals(result, 768) -- 40% of 1920 = 768
end
--- Test viewport height units (vh)
function TestCalc:testViewportHeightUnits()
local calcObj = Calc.new("50vh + 10vh")
lu.assertTrue(Calc.isCalc(calcObj))
local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil)
lu.assertEquals(result, 648) -- 60% of 1080 = 648
end
--- Test mixed units
function TestCalc:testMixedUnits()
local calcObj = Calc.new("50% - 10vw")
lu.assertTrue(Calc.isCalc(calcObj))
local result = Calc.resolve(calcObj, 1920, 1080, 1000, nil, nil)
lu.assertEquals(result, 308) -- 50% of 1000 - 10% of 1920 = 500 - 192 = 308
end
--- Test complex expression with multiple operations
function TestCalc:testComplexExpression()
local calcObj = Calc.new("100px + 50px - 20px")
lu.assertTrue(Calc.isCalc(calcObj))
local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil)
lu.assertEquals(result, 130)
end
--- Test parentheses for precedence
function TestCalc:testParentheses()
local calcObj = Calc.new("(100px + 50px) * 2")
lu.assertTrue(Calc.isCalc(calcObj))
local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil)
lu.assertEquals(result, 300)
end
--- Test nested parentheses
function TestCalc:testNestedParentheses()
local calcObj = Calc.new("((100px + 50px) / 3) * 2")
lu.assertTrue(Calc.isCalc(calcObj))
local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil)
lu.assertEquals(result, 100) -- (150 / 3) * 2 = 50 * 2 = 100
end
--- Test operator precedence (multiplication before addition)
function TestCalc:testOperatorPrecedence()
local calcObj = Calc.new("100px + 50px * 2")
lu.assertTrue(Calc.isCalc(calcObj))
local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil)
lu.assertEquals(result, 200) -- 100 + (50 * 2) = 100 + 100 = 200
end
--- Test centering use case (50% - 10vw)
function TestCalc:testCenteringUseCase()
local calcObj = Calc.new("50% - 10vw")
lu.assertTrue(Calc.isCalc(calcObj))
-- Assuming element width is 20vw (384px) and parent width is 1920px
-- 50% of parent - 10vw should center it
local result = Calc.resolve(calcObj, 1920, 1080, 1920, nil, nil)
lu.assertEquals(result, 768) -- 50% of 1920 - 10% of 1920 = 960 - 192 = 768
end
--- Test element width units (ew)
function TestCalc:testElementWidthUnits()
local calcObj = Calc.new("100ew - 20ew")
lu.assertTrue(Calc.isCalc(calcObj))
local result = Calc.resolve(calcObj, 1920, 1080, nil, 500, nil) -- element width = 500
lu.assertEquals(result, 400) -- 80% of 500 = 400
end
--- Test element height units (eh)
function TestCalc:testElementHeightUnits()
local calcObj = Calc.new("50eh + 25eh")
lu.assertTrue(Calc.isCalc(calcObj))
local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, 300) -- element height = 300
lu.assertEquals(result, 225) -- 75% of 300 = 225
end
--- Test whitespace handling
function TestCalc:testWhitespaceHandling()
local calcObj = Calc.new(" 100px + 50px ")
lu.assertTrue(Calc.isCalc(calcObj))
local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil)
lu.assertEquals(result, 150)
end
--- Test zero value
function TestCalc:testZeroValue()
local calcObj = Calc.new("100px - 100px")
lu.assertTrue(Calc.isCalc(calcObj))
local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil)
lu.assertEquals(result, 0)
end
--- Test single value (no operation)
function TestCalc:testSingleValue()
local calcObj = Calc.new("100px")
lu.assertTrue(Calc.isCalc(calcObj))
local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil)
lu.assertEquals(result, 100)
end
--- Test isCalc function with non-calc values
function TestCalc:testIsCalcWithNonCalcValues()
lu.assertFalse(Calc.isCalc("100px"))
lu.assertFalse(Calc.isCalc(100))
lu.assertFalse(Calc.isCalc(nil))
lu.assertFalse(Calc.isCalc({}))
end
--- Test division by zero error handling
function TestCalc:testDivisionByZeroHandling()
local calcObj = Calc.new("100px / 0")
lu.assertTrue(Calc.isCalc(calcObj))
local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil)
lu.assertEquals(result, 0) -- Should return 0 on division by zero error
end
--- Test invalid expression error handling
function TestCalc:testInvalidExpressionHandling()
local calcObj = Calc.new("100px +") -- Incomplete expression
lu.assertTrue(Calc.isCalc(calcObj))
-- Should return 0 for invalid expressions
local result = Calc.resolve(calcObj, 1920, 1080, nil, nil, nil)
lu.assertEquals(result, 0)
end
--- Test complex real-world centering scenario
function TestCalc:testRealWorldCentering()
-- Button with 20vw width, centered at 50% - 10vw
local xCalc = Calc.new("50% - 10vw")
local result = Calc.resolve(xCalc, 1920, 1080, 1920, nil, nil)
-- Expected: 50% of 1920 - 10% of 1920 = 960 - 192 = 768
lu.assertEquals(result, 768)
end
if not _G.RUNNING_ALL_TESTS then
os.exit(lu.LuaUnit.run())
end

View File

@@ -5,6 +5,16 @@
-- 3. Unsafe input access (nil dereference, division by zero, etc.)
package.path = package.path .. ";./?.lua;./modules/?.lua"
-- Add custom package searcher to handle FlexLove.modules.X imports
local originalSearchers = package.searchers or package.loaders
table.insert(originalSearchers, 2, function(modname)
if modname:match("^FlexLove%.modules%.") then
local moduleName = modname:gsub("^FlexLove%.modules%.", "")
return function() return require("modules." .. moduleName) end
end
end)
require("testing.loveStub")
local luaunit = require("testing.luaunit")
local FlexLove = require("FlexLove")

View File

@@ -12,6 +12,15 @@ local ErrorHandler = require("modules.ErrorHandler")
-- Initialize ErrorHandler
ErrorHandler.init({})
-- Setup package loader to map FlexLove.modules.X to modules/X
local originalSearchers = package.searchers or package.loaders
table.insert(originalSearchers, 2, function(modname)
if modname:match("^FlexLove%.modules%.") then
local moduleName = modname:gsub("^FlexLove%.modules%.", "")
return function() return require("modules." .. moduleName) end
end
end)
-- Load FlexLove which properly initializes all dependencies
local FlexLove = require("FlexLove")
local Element = require("modules.Element")

View File

@@ -1,3 +1,12 @@
-- Add custom package searcher to handle FlexLove.modules.X imports
local originalSearchers = package.searchers or package.loaders
table.insert(originalSearchers, 2, function(modname)
if modname:match("^FlexLove%.modules%.") then
local moduleName = modname:gsub("^FlexLove%.modules%.", "")
return function() return require("modules." .. moduleName) end
end
end)
local luaunit = require("testing.luaunit")
local ErrorHandler = require("modules.ErrorHandler")
require("testing.loveStub")

View File

@@ -4,6 +4,16 @@ package.path = package.path .. ";./?.lua;./modules/?.lua"
require("testing.loveStub")
local luaunit = require("testing.luaunit")
-- Setup package loader to map FlexLove.modules.X to modules/X
local originalSearchers = package.searchers or package.loaders
table.insert(originalSearchers, 2, function(modname)
if modname:match("^FlexLove%.modules%.") then
local moduleName = modname:gsub("^FlexLove%.modules%.", "")
return function() return require("modules." .. moduleName) end
end
end)
local FlexLove = require("FlexLove")
TestGridLayout = {}

View File

@@ -518,6 +518,13 @@ function TestImageRendererElementIntegration:setUp()
local Renderer = require("modules.Renderer")
local EventHandler = require("modules.EventHandler")
local ImageCache = require("modules.ImageCache")
local Context = require("modules.Context")
local StateManager = require("modules.StateManager")
local InputEvent = require("modules.InputEvent")
local Theme = require("modules.Theme")
local TextEditor = require("modules.TextEditor")
local ScrollManager = require("modules.ScrollManager")
local RoundedRect = require("modules.RoundedRect")
self.deps = {
utils = utils,
@@ -529,7 +536,18 @@ function TestImageRendererElementIntegration:setUp()
ImageCache = ImageCache,
ImageRenderer = ImageRenderer,
ErrorHandler = ErrorHandler,
Context = Context,
StateManager = StateManager,
InputEvent = InputEvent,
Theme = Theme,
TextEditor = TextEditor,
ScrollManager = ScrollManager,
RoundedRect = RoundedRect,
}
-- Initialize Element with dependencies
Element.init(self.deps)
self.Element = Element
end

View File

@@ -6,10 +6,9 @@ ErrorHandler.init({})
require("testing.loveStub")
local ImageScaler = require("modules.ImageScaler")
local ErrorHandler = require("modules.ErrorHandler")
-- Initialize ErrorHandler
ErrorHandler.init({})
-- Initialize ImageScaler with ErrorHandler
ImageScaler.init({ ErrorHandler = ErrorHandler })
TestImageScaler = {}

View File

@@ -11,9 +11,19 @@ local luaunit = require("testing.luaunit")
local LayoutEngine = require("modules.LayoutEngine")
local Units = require("modules.Units")
local utils = require("modules.utils")
local FlexLove = require("FlexLove")
local ErrorHandler = require("modules.ErrorHandler")
local Animation = require("modules.Animation")
-- Setup package loader to map FlexLove.modules.X to modules/X
local originalSearchers = package.searchers or package.loaders
table.insert(originalSearchers, 2, function(modname)
if modname:match("^FlexLove%.modules%.") then
local moduleName = modname:gsub("^FlexLove%.modules%.", "")
return function() return require("modules." .. moduleName) end
end
end)
local FlexLove = require("FlexLove")
local Transform = Animation.Transform
-- ============================================================================

View File

@@ -7,6 +7,15 @@ local loveStub = require("testing.loveStub")
-- Set up stub before requiring modules
_G.love = loveStub
-- Setup package loader to map FlexLove.modules.X to modules/X
local originalSearchers = package.searchers or package.loaders
table.insert(originalSearchers, 2, function(modname)
if modname:match("^FlexLove%.modules%.") then
local moduleName = modname:gsub("^FlexLove%.modules%.", "")
return function() return require("modules." .. moduleName) end
end
end)
local FlexLove = require("FlexLove")
local Performance = require("modules.Performance")
local Element = require('modules.Element')

View File

@@ -11,6 +11,16 @@ local Theme = require("modules.Theme")
local Blur = require("modules.Blur")
local utils = require("modules.utils")
local ErrorHandler = require("modules.ErrorHandler")
-- Setup package loader to map FlexLove.modules.X to modules/X
local originalSearchers = package.searchers or package.loaders
table.insert(originalSearchers, 2, function(modname)
if modname:match("^FlexLove%.modules%.") then
local moduleName = modname:gsub("^FlexLove%.modules%.", "")
return function() return require("modules." .. moduleName) end
end
end)
local FlexLove = require("FlexLove")
-- Initialize ErrorHandler

View File

@@ -1,5 +1,14 @@
package.path = package.path .. ";./?.lua;./modules/?.lua"
-- Add custom package searcher to handle FlexLove.modules.X imports
local originalSearchers = package.searchers or package.loaders
table.insert(originalSearchers, 2, function(modname)
if modname:match("^FlexLove%.modules%.") then
local moduleName = modname:gsub("^FlexLove%.modules%.", "")
return function() return require("modules." .. moduleName) end
end
end)
require("testing.loveStub")
local lu = require("testing.luaunit")

View File

@@ -9,9 +9,17 @@ require("testing.loveStub")
local luaunit = require("testing.luaunit")
local Units = require("modules.Units")
local Context = require("modules.Context")
local ErrorHandler = require("modules.ErrorHandler")
local Calc = require("modules.Calc")
-- Initialize Units module with Context
Units.init({ Context = Context })
-- Initialize ErrorHandler
ErrorHandler.init({})
-- Initialize Calc
Calc.init({ ErrorHandler = ErrorHandler })
-- Initialize Units module with dependencies
Units.init({ Context = Context, ErrorHandler = ErrorHandler, Calc = Calc })
-- Mock viewport dimensions for consistent tests
local MOCK_VIEWPORT_WIDTH = 1920

View File

@@ -56,6 +56,7 @@ local testFiles = {
"testing/__tests__/theme_test.lua",
"testing/__tests__/units_test.lua",
"testing/__tests__/utils_test.lua",
"testing/__tests__/calc_test.lua",
-- Feature/Integration tests
"testing/__tests__/critical_failures_test.lua",
"testing/__tests__/flexlove_test.lua",