calc module
This commit is contained in:
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -212,7 +212,7 @@ jobs:
|
|||||||
|---------|------|-------------|---------|
|
|---------|------|-------------|---------|
|
||||||
| **Minimal** | ~70% | Core functionality only | `flexlove-minimal-v${{ steps.version.outputs.version }}.zip` |
|
| **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` |
|
| **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` |
|
| **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.
|
**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.
|
||||||
|
|||||||
20
FlexLove.lua
20
FlexLove.lua
@@ -20,6 +20,7 @@ end
|
|||||||
|
|
||||||
-- Required core modules
|
-- Required core modules
|
||||||
local utils = req("utils")
|
local utils = req("utils")
|
||||||
|
local Calc = req("Calc")
|
||||||
local Units = req("Units")
|
local Units = req("Units")
|
||||||
local Context = req("Context")
|
local Context = req("Context")
|
||||||
---@type StateManager
|
---@type StateManager
|
||||||
@@ -176,7 +177,8 @@ function flexlove.init(config)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Initialize required modules
|
-- 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 })
|
Color.init({ ErrorHandler = flexlove._ErrorHandler, FFI = flexlove._FFI })
|
||||||
utils.init({ ErrorHandler = flexlove._ErrorHandler })
|
utils.init({ ErrorHandler = flexlove._ErrorHandler })
|
||||||
|
|
||||||
@@ -197,6 +199,7 @@ function flexlove.init(config)
|
|||||||
Context = Context,
|
Context = Context,
|
||||||
Theme = Theme,
|
Theme = Theme,
|
||||||
Color = Color,
|
Color = Color,
|
||||||
|
Calc = Calc,
|
||||||
Units = Units,
|
Units = Units,
|
||||||
Blur = Blur,
|
Blur = Blur,
|
||||||
ImageRenderer = ImageRenderer,
|
ImageRenderer = ImageRenderer,
|
||||||
@@ -1103,6 +1106,21 @@ function flexlove.getStateStats()
|
|||||||
return StateManager.getStats()
|
return StateManager.getStats()
|
||||||
end
|
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.Animation = Animation
|
||||||
flexlove.Color = Color
|
flexlove.Color = Color
|
||||||
flexlove.Theme = Theme
|
flexlove.Theme = Theme
|
||||||
|
|||||||
52
README.md
52
README.md
@@ -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
|
- **Default (~95%)** - Adds themes, blur effects, and gestures
|
||||||
- **Full (100%)** - Everything including performance monitoring
|
- **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
|
## Documentation
|
||||||
|
|
||||||
📚 **[View Full API Documentation](https://mikefreno.github.io/FlexLove/api.html)**
|
📚 **[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
|
### Animations
|
||||||
|
|
||||||
Create smooth transitions:
|
Create smooth transitions:
|
||||||
|
|||||||
@@ -174,10 +174,8 @@ Each profile package includes:
|
|||||||
|---------|---------|------------------|
|
|---------|---------|------------------|
|
||||||
| **Minimal** | 19 core modules | ~70% of full |
|
| **Minimal** | 19 core modules | ~70% of full |
|
||||||
| **Slim** | 24 modules | ~80% of full |
|
| **Slim** | 24 modules | ~80% of full |
|
||||||
| **Default** | 27 modules + themes | ~95% of full |
|
| **Default** | 28 modules + theme examples | ~95% of full |
|
||||||
| **Full** | 29 modules + themes | 100% |
|
| **Full** | 30 modules + theme examples | 100% |
|
||||||
|
|
||||||
**Note:** All profiles now include UTF8.lua for Lua 5.1+ compatibility.
|
|
||||||
|
|
||||||
Users who want examples, documentation source, or development tools should clone the full repository.
|
Users who want examples, documentation source, or development tools should clone the full repository.
|
||||||
|
|
||||||
|
|||||||
404
modules/Calc.lua
Normal file
404
modules/Calc.lua
Normal 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
|
||||||
@@ -1092,7 +1092,7 @@ function Element.new(props)
|
|||||||
|
|
||||||
-- Handle x position with units
|
-- Handle x position with units
|
||||||
if props.x then
|
if props.x then
|
||||||
if type(props.x) == "string" then
|
if type(props.x) == "string" or type(props.x) == "table" then
|
||||||
local value, unit = Element._Units.parse(props.x)
|
local value, unit = Element._Units.parse(props.x)
|
||||||
self.units.x = { value = value, unit = unit }
|
self.units.x = { value = value, unit = unit }
|
||||||
self.x = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, viewportWidth)
|
self.x = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, viewportWidth)
|
||||||
@@ -1108,7 +1108,7 @@ function Element.new(props)
|
|||||||
|
|
||||||
-- Handle y position with units
|
-- Handle y position with units
|
||||||
if props.y then
|
if props.y then
|
||||||
if type(props.y) == "string" then
|
if type(props.y) == "string" or type(props.y) == "table" then
|
||||||
local value, unit = Element._Units.parse(props.y)
|
local value, unit = Element._Units.parse(props.y)
|
||||||
self.units.y = { value = value, unit = unit }
|
self.units.y = { value = value, unit = unit }
|
||||||
self.y = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, viewportHeight)
|
self.y = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, viewportHeight)
|
||||||
@@ -1181,7 +1181,7 @@ function Element.new(props)
|
|||||||
|
|
||||||
-- Handle x position with units
|
-- Handle x position with units
|
||||||
if props.x then
|
if props.x then
|
||||||
if type(props.x) == "string" then
|
if type(props.x) == "string" or type(props.x) == "table" then
|
||||||
local value, unit = Element._Units.parse(props.x)
|
local value, unit = Element._Units.parse(props.x)
|
||||||
self.units.x = { value = value, unit = unit }
|
self.units.x = { value = value, unit = unit }
|
||||||
local parentWidth = self.parent.width
|
local parentWidth = self.parent.width
|
||||||
@@ -1200,7 +1200,7 @@ function Element.new(props)
|
|||||||
|
|
||||||
-- Handle y position with units
|
-- Handle y position with units
|
||||||
if props.y then
|
if props.y then
|
||||||
if type(props.y) == "string" then
|
if type(props.y) == "string" or type(props.y) == "table" then
|
||||||
local value, unit = Element._Units.parse(props.y)
|
local value, unit = Element._Units.parse(props.y)
|
||||||
self.units.y = { value = value, unit = unit }
|
self.units.y = { value = value, unit = unit }
|
||||||
local parentHeight = self.parent.height
|
local parentHeight = self.parent.height
|
||||||
@@ -1225,7 +1225,7 @@ function Element.new(props)
|
|||||||
local baseY = self.parent.y + self.parent.padding.top
|
local baseY = self.parent.y + self.parent.padding.top
|
||||||
|
|
||||||
if props.x then
|
if props.x then
|
||||||
if type(props.x) == "string" then
|
if type(props.x) == "string" or type(props.x) == "table" then
|
||||||
local value, unit = Element._Units.parse(props.x)
|
local value, unit = Element._Units.parse(props.x)
|
||||||
self.units.x = { value = value, unit = unit }
|
self.units.x = { value = value, unit = unit }
|
||||||
local parentWidth = self.parent.width
|
local parentWidth = self.parent.width
|
||||||
@@ -1243,7 +1243,7 @@ function Element.new(props)
|
|||||||
end
|
end
|
||||||
|
|
||||||
if props.y then
|
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)
|
local value, unit = Element._Units.parse(props.y)
|
||||||
self.units.y = { value = value, unit = unit }
|
self.units.y = { value = value, unit = unit }
|
||||||
parentHeight = self.parent.height
|
parentHeight = self.parent.height
|
||||||
|
|||||||
@@ -46,8 +46,8 @@ local ErrorCodes = {
|
|||||||
VAL_006 = {
|
VAL_006 = {
|
||||||
code = "FLEXLOVE_VAL_006",
|
code = "FLEXLOVE_VAL_006",
|
||||||
category = "VAL",
|
category = "VAL",
|
||||||
description = "Invalid file path",
|
description = "Invalid calc() expression or calculation error",
|
||||||
suggestion = "Check that the file path is correct and the file exists",
|
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 = {
|
VAL_007 = {
|
||||||
code = "FLEXLOVE_VAL_007",
|
code = "FLEXLOVE_VAL_007",
|
||||||
|
|||||||
@@ -3,29 +3,36 @@
|
|||||||
---@class Units
|
---@class Units
|
||||||
---@field _Context table? Context module dependency
|
---@field _Context table? Context module dependency
|
||||||
---@field _ErrorHandler table? ErrorHandler module dependency
|
---@field _ErrorHandler table? ErrorHandler module dependency
|
||||||
|
---@field _Calc table? Calc module dependency
|
||||||
local Units = {}
|
local Units = {}
|
||||||
|
|
||||||
--- Initialize Units module with dependencies
|
--- 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)
|
function Units.init(deps)
|
||||||
Units._Context = deps.Context
|
Units._Context = deps.Context
|
||||||
Units._ErrorHandler = deps.ErrorHandler
|
Units._ErrorHandler = deps.ErrorHandler
|
||||||
|
Units._Calc = deps.Calc
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Parse a unit value into numeric value and unit type
|
--- Parse a unit value into numeric value and unit type
|
||||||
--- Supports: px (pixels), % (percentage), vw/vh (viewport), ew/eh (element)
|
--- Supports: px (pixels), % (percentage), vw/vh (viewport), ew/eh (element), and calc() expressions
|
||||||
---@param value string|number The value to parse (e.g., "50px", "10%", "2vw", 100)
|
---@param value string|number|table The value to parse (e.g., "50px", "10%", "2vw", 100, or calc object)
|
||||||
---@return number numericValue The numeric portion of the value
|
---@return number|table numericValue The numeric portion of the value or calc object
|
||||||
---@return string unitType The unit type ("px", "%", "vw", "vh", "ew", "eh")
|
---@return string unitType The unit type ("px", "%", "vw", "vh", "ew", "eh", "calc")
|
||||||
function Units.parse(value)
|
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
|
if type(value) == "number" then
|
||||||
return value, "px"
|
return value, "px"
|
||||||
end
|
end
|
||||||
|
|
||||||
if type(value) ~= "string" then
|
if type(value) ~= "string" and type(value) ~= "table" then
|
||||||
Units._ErrorHandler:warn("Units", "VAL_001", {
|
Units._ErrorHandler:warn("Units", "VAL_001", {
|
||||||
property = "unit value",
|
property = "unit value",
|
||||||
expected = "string or number",
|
expected = "string, number, or calc object",
|
||||||
got = type(value),
|
got = type(value),
|
||||||
})
|
})
|
||||||
return 0, "px"
|
return 0, "px"
|
||||||
@@ -87,15 +94,28 @@ function Units.parse(value)
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- Convert relative units to absolute pixel values
|
--- Convert relative units to absolute pixel values
|
||||||
--- Resolves %, vw, vh units based on viewport and parent dimensions
|
--- Resolves %, vw, vh units based on viewport and parent dimensions, and evaluates calc() expressions
|
||||||
---@param value number Numeric value to convert
|
---@param value number|table Numeric value to convert or calc object
|
||||||
---@param unit string Unit type ("px", "%", "vw", "vh", "ew", "eh")
|
---@param unit string Unit type ("px", "%", "vw", "vh", "ew", "eh", "calc")
|
||||||
---@param viewportWidth number Current viewport width in pixels
|
---@param viewportWidth number Current viewport width in pixels
|
||||||
---@param viewportHeight number Current viewport height in pixels
|
---@param viewportHeight number Current viewport height in pixels
|
||||||
---@param parentSize number? Required for percentage units (parent dimension 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
|
---@return number resolvedValue Resolved pixel value
|
||||||
function Units.resolve(value, unit, viewportWidth, viewportHeight, parentSize)
|
function Units.resolve(value, unit, viewportWidth, viewportHeight, parentSize, elementWidth, elementHeight)
|
||||||
if unit == "px" then
|
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
|
return value
|
||||||
elseif unit == "%" then
|
elseif unit == "%" then
|
||||||
if not parentSize then
|
if not parentSize then
|
||||||
@@ -113,7 +133,7 @@ function Units.resolve(value, unit, viewportWidth, viewportHeight, parentSize)
|
|||||||
else
|
else
|
||||||
Units._ErrorHandler:warn("Units", "VAL_005", {
|
Units._ErrorHandler:warn("Units", "VAL_005", {
|
||||||
unit = unit,
|
unit = unit,
|
||||||
validUnits = "px, %, vw, vh, ew, eh",
|
validUnits = "px, %, vw, vh, ew, eh, calc",
|
||||||
})
|
})
|
||||||
return 0
|
return 0
|
||||||
end
|
end
|
||||||
@@ -169,26 +189,26 @@ function Units.resolveSpacing(spacingProps, parentWidth, parentHeight)
|
|||||||
local horizontal = spacingProps.horizontal
|
local horizontal = spacingProps.horizontal
|
||||||
|
|
||||||
if vertical then
|
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)
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
if horizontal then
|
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)
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
for _, side in ipairs({ "top", "right", "bottom", "left" }) do
|
for _, side in ipairs({ "top", "right", "bottom", "left" }) do
|
||||||
local value = spacingProps[side]
|
local value = spacingProps[side]
|
||||||
if value then
|
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 numValue, unit = Units.parse(value)
|
||||||
local parentSize = (side == "top" or side == "bottom") and parentHeight or parentWidth
|
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
|
else
|
||||||
result[side] = value
|
result[side] = value
|
||||||
end
|
end
|
||||||
@@ -205,10 +225,15 @@ function Units.resolveSpacing(spacingProps, parentWidth, parentHeight)
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- Validate a unit string format
|
--- Validate a unit string format
|
||||||
--- Checks if the string can be successfully parsed as a valid unit
|
--- Checks if the string can be successfully parsed as a valid unit or calc expression
|
||||||
---@param unitStr string The unit string to validate (e.g., "50px", "10%")
|
---@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
|
---@return boolean isValid True if the unit string is valid, false otherwise
|
||||||
function Units.isValid(unitStr)
|
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
|
if type(unitStr) ~= "string" then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -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"
|
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)
|
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)
|
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
|
esac
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,15 @@ local ErrorHandler = require("modules.ErrorHandler")
|
|||||||
-- Initialize ErrorHandler
|
-- Initialize ErrorHandler
|
||||||
ErrorHandler.init({})
|
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
|
-- Load FlexLove which properly initializes all dependencies
|
||||||
local FlexLove = require("FlexLove")
|
local FlexLove = require("FlexLove")
|
||||||
|
|
||||||
|
|||||||
232
testing/__tests__/calc_test.lua
Normal file
232
testing/__tests__/calc_test.lua
Normal 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
|
||||||
@@ -5,6 +5,16 @@
|
|||||||
-- 3. Unsafe input access (nil dereference, division by zero, etc.)
|
-- 3. Unsafe input access (nil dereference, division by zero, etc.)
|
||||||
|
|
||||||
package.path = package.path .. ";./?.lua;./modules/?.lua"
|
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")
|
require("testing.loveStub")
|
||||||
local luaunit = require("testing.luaunit")
|
local luaunit = require("testing.luaunit")
|
||||||
local FlexLove = require("FlexLove")
|
local FlexLove = require("FlexLove")
|
||||||
|
|||||||
@@ -12,6 +12,15 @@ local ErrorHandler = require("modules.ErrorHandler")
|
|||||||
-- Initialize ErrorHandler
|
-- Initialize ErrorHandler
|
||||||
ErrorHandler.init({})
|
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
|
-- Load FlexLove which properly initializes all dependencies
|
||||||
local FlexLove = require("FlexLove")
|
local FlexLove = require("FlexLove")
|
||||||
local Element = require("modules.Element")
|
local Element = require("modules.Element")
|
||||||
|
|||||||
@@ -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 luaunit = require("testing.luaunit")
|
||||||
local ErrorHandler = require("modules.ErrorHandler")
|
local ErrorHandler = require("modules.ErrorHandler")
|
||||||
require("testing.loveStub")
|
require("testing.loveStub")
|
||||||
|
|||||||
@@ -4,6 +4,16 @@ package.path = package.path .. ";./?.lua;./modules/?.lua"
|
|||||||
|
|
||||||
require("testing.loveStub")
|
require("testing.loveStub")
|
||||||
local luaunit = require("testing.luaunit")
|
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")
|
local FlexLove = require("FlexLove")
|
||||||
|
|
||||||
TestGridLayout = {}
|
TestGridLayout = {}
|
||||||
|
|||||||
@@ -518,6 +518,13 @@ function TestImageRendererElementIntegration:setUp()
|
|||||||
local Renderer = require("modules.Renderer")
|
local Renderer = require("modules.Renderer")
|
||||||
local EventHandler = require("modules.EventHandler")
|
local EventHandler = require("modules.EventHandler")
|
||||||
local ImageCache = require("modules.ImageCache")
|
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 = {
|
self.deps = {
|
||||||
utils = utils,
|
utils = utils,
|
||||||
@@ -529,7 +536,18 @@ function TestImageRendererElementIntegration:setUp()
|
|||||||
ImageCache = ImageCache,
|
ImageCache = ImageCache,
|
||||||
ImageRenderer = ImageRenderer,
|
ImageRenderer = ImageRenderer,
|
||||||
ErrorHandler = ErrorHandler,
|
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
|
self.Element = Element
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -6,10 +6,9 @@ ErrorHandler.init({})
|
|||||||
require("testing.loveStub")
|
require("testing.loveStub")
|
||||||
|
|
||||||
local ImageScaler = require("modules.ImageScaler")
|
local ImageScaler = require("modules.ImageScaler")
|
||||||
local ErrorHandler = require("modules.ErrorHandler")
|
|
||||||
|
|
||||||
-- Initialize ErrorHandler
|
-- Initialize ImageScaler with ErrorHandler
|
||||||
ErrorHandler.init({})
|
ImageScaler.init({ ErrorHandler = ErrorHandler })
|
||||||
|
|
||||||
TestImageScaler = {}
|
TestImageScaler = {}
|
||||||
|
|
||||||
|
|||||||
@@ -11,9 +11,19 @@ local luaunit = require("testing.luaunit")
|
|||||||
local LayoutEngine = require("modules.LayoutEngine")
|
local LayoutEngine = require("modules.LayoutEngine")
|
||||||
local Units = require("modules.Units")
|
local Units = require("modules.Units")
|
||||||
local utils = require("modules.utils")
|
local utils = require("modules.utils")
|
||||||
local FlexLove = require("FlexLove")
|
|
||||||
local ErrorHandler = require("modules.ErrorHandler")
|
local ErrorHandler = require("modules.ErrorHandler")
|
||||||
local Animation = require("modules.Animation")
|
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
|
local Transform = Animation.Transform
|
||||||
|
|
||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
|
|||||||
@@ -7,6 +7,15 @@ local loveStub = require("testing.loveStub")
|
|||||||
-- Set up stub before requiring modules
|
-- Set up stub before requiring modules
|
||||||
_G.love = loveStub
|
_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 FlexLove = require("FlexLove")
|
||||||
local Performance = require("modules.Performance")
|
local Performance = require("modules.Performance")
|
||||||
local Element = require('modules.Element')
|
local Element = require('modules.Element')
|
||||||
|
|||||||
@@ -11,6 +11,16 @@ local Theme = require("modules.Theme")
|
|||||||
local Blur = require("modules.Blur")
|
local Blur = require("modules.Blur")
|
||||||
local utils = require("modules.utils")
|
local utils = require("modules.utils")
|
||||||
local ErrorHandler = require("modules.ErrorHandler")
|
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")
|
local FlexLove = require("FlexLove")
|
||||||
|
|
||||||
-- Initialize ErrorHandler
|
-- Initialize ErrorHandler
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
package.path = package.path .. ";./?.lua;./modules/?.lua"
|
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")
|
require("testing.loveStub")
|
||||||
local lu = require("testing.luaunit")
|
local lu = require("testing.luaunit")
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,17 @@ require("testing.loveStub")
|
|||||||
local luaunit = require("testing.luaunit")
|
local luaunit = require("testing.luaunit")
|
||||||
local Units = require("modules.Units")
|
local Units = require("modules.Units")
|
||||||
local Context = require("modules.Context")
|
local Context = require("modules.Context")
|
||||||
|
local ErrorHandler = require("modules.ErrorHandler")
|
||||||
|
local Calc = require("modules.Calc")
|
||||||
|
|
||||||
-- Initialize Units module with Context
|
-- Initialize ErrorHandler
|
||||||
Units.init({ Context = Context })
|
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
|
-- Mock viewport dimensions for consistent tests
|
||||||
local MOCK_VIEWPORT_WIDTH = 1920
|
local MOCK_VIEWPORT_WIDTH = 1920
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ local testFiles = {
|
|||||||
"testing/__tests__/theme_test.lua",
|
"testing/__tests__/theme_test.lua",
|
||||||
"testing/__tests__/units_test.lua",
|
"testing/__tests__/units_test.lua",
|
||||||
"testing/__tests__/utils_test.lua",
|
"testing/__tests__/utils_test.lua",
|
||||||
|
"testing/__tests__/calc_test.lua",
|
||||||
-- Feature/Integration tests
|
-- Feature/Integration tests
|
||||||
"testing/__tests__/critical_failures_test.lua",
|
"testing/__tests__/critical_failures_test.lua",
|
||||||
"testing/__tests__/flexlove_test.lua",
|
"testing/__tests__/flexlove_test.lua",
|
||||||
|
|||||||
Reference in New Issue
Block a user