current
This commit is contained in:
612
FlexLove.lua
612
FlexLove.lua
@@ -1,4 +1,256 @@
|
|||||||
-- Utility class for color handling
|
--[[
|
||||||
|
================================================================================
|
||||||
|
FlexLove - Flexible UI Library for LÖVE Framework
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
A comprehensive UI library providing flexbox/grid layouts, theming, animations,
|
||||||
|
and event handling for LÖVE2D games.
|
||||||
|
|
||||||
|
ARCHITECTURE OVERVIEW:
|
||||||
|
---------------------
|
||||||
|
1. Color System - RGBA color utilities with hex conversion
|
||||||
|
2. Theme System - 9-slice theming with state support (normal/hover/pressed/disabled)
|
||||||
|
3. Units System - Responsive units (px, %, vw, vh, ew, eh) with viewport scaling
|
||||||
|
4. Layout System - Flexbox, Grid, Absolute, and Relative positioning
|
||||||
|
5. Event System - Mouse/touch events with z-index ordering
|
||||||
|
6. Animation System - Interpolation with easing functions
|
||||||
|
7. GUI Manager - Top-level manager for elements and global state
|
||||||
|
|
||||||
|
API CONVENTIONS:
|
||||||
|
---------------
|
||||||
|
- Constructors: ClassName.new(props) -> instance
|
||||||
|
- Static Methods: ClassName.methodName(args) -> result
|
||||||
|
- Instance Methods: instance:methodName(args) -> result
|
||||||
|
- Getters: instance:getPropertyName() -> value
|
||||||
|
- Internal Fields: _fieldName (private, do not access directly)
|
||||||
|
- Error Handling: Constructors throw errors, utility functions return nil + error string
|
||||||
|
|
||||||
|
NAMING PATTERNS:
|
||||||
|
---------------
|
||||||
|
- Classes: PascalCase (Element, Theme, Color)
|
||||||
|
- Functions: camelCase (resolveImagePath, getViewport)
|
||||||
|
- Properties: camelCase (backgroundColor, textColor, cornerRadius)
|
||||||
|
- Constants: UPPER_SNAKE_CASE (TEXT_SIZE_PRESETS, FONT_CACHE_MAX_SIZE)
|
||||||
|
- Private: _prefixedCamelCase (_pressed, _themeState, _borderBoxWidth)
|
||||||
|
|
||||||
|
PARAMETER ORDERING:
|
||||||
|
------------------
|
||||||
|
- Position: (x, y, width, height) - standard order
|
||||||
|
- Units: (value, unit, viewportW, viewportH, parentSize) - value first
|
||||||
|
- Drawing: (element, position, dimensions, styling, opacity) - element first
|
||||||
|
|
||||||
|
RETURN VALUE PATTERNS:
|
||||||
|
---------------------
|
||||||
|
- Single Success: return value
|
||||||
|
- Success/Failure: return result, errorMessage (nil on success for error)
|
||||||
|
- Multiple Values: return value1, value2 (documented in @return)
|
||||||
|
- Constructors: Always return instance (never nil)
|
||||||
|
|
||||||
|
USAGE EXAMPLE:
|
||||||
|
-------------
|
||||||
|
```lua
|
||||||
|
local FlexLove = require("libs.FlexLove")
|
||||||
|
|
||||||
|
-- Initialize with base scaling and theme
|
||||||
|
FlexLove.Gui.init({
|
||||||
|
baseScale = { width = 1920, height = 1080 },
|
||||||
|
theme = "space"
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Create a button with flexbox layout
|
||||||
|
local button = FlexLove.Element.new({
|
||||||
|
width = "20vw",
|
||||||
|
height = "10vh",
|
||||||
|
backgroundColor = FlexLove.Color.new(0.2, 0.2, 0.8, 1),
|
||||||
|
text = "Click Me",
|
||||||
|
textSize = "md",
|
||||||
|
themeComponent = "button",
|
||||||
|
callback = function(element, event)
|
||||||
|
print("Button clicked!")
|
||||||
|
end
|
||||||
|
})
|
||||||
|
|
||||||
|
-- In your love.update and love.draw:
|
||||||
|
function love.update(dt)
|
||||||
|
FlexLove.Gui.update(dt)
|
||||||
|
end
|
||||||
|
|
||||||
|
function love.draw()
|
||||||
|
FlexLove.Gui.draw()
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
ADDITIONAL EXAMPLES:
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
1. Creating Colors:
|
||||||
|
```lua
|
||||||
|
-- From RGB values (0-1 range)
|
||||||
|
local red = FlexLove.Color.new(1, 0, 0, 1)
|
||||||
|
|
||||||
|
-- From hex string
|
||||||
|
local blue = FlexLove.Color.fromHex("#0000FF")
|
||||||
|
local semiTransparent = FlexLove.Color.fromHex("#FF000080")
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Responsive Units:
|
||||||
|
```lua
|
||||||
|
-- Viewport-relative units
|
||||||
|
local container = FlexLove.Element.new({
|
||||||
|
width = "50vw", -- 50% of viewport width
|
||||||
|
height = "30vh", -- 30% of viewport height
|
||||||
|
padding = { horizontal = "2vw", vertical = "1vh" }
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Percentage units (relative to parent)
|
||||||
|
local child = FlexLove.Element.new({
|
||||||
|
parent = container,
|
||||||
|
width = "80%", -- 80% of parent width
|
||||||
|
height = "50%" -- 50% of parent height
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Flexbox Layout:
|
||||||
|
```lua
|
||||||
|
-- Horizontal flex container
|
||||||
|
local row = FlexLove.Element.new({
|
||||||
|
positioning = FlexLove.Positioning.FLEX,
|
||||||
|
flexDirection = FlexLove.FlexDirection.HORIZONTAL,
|
||||||
|
justifyContent = FlexLove.JustifyContent.SPACE_BETWEEN,
|
||||||
|
alignItems = FlexLove.AlignItems.CENTER,
|
||||||
|
gap = 10,
|
||||||
|
width = "80vw",
|
||||||
|
height = "10vh"
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Add children
|
||||||
|
for i = 1, 3 do
|
||||||
|
FlexLove.Element.new({
|
||||||
|
parent = row,
|
||||||
|
width = "20vw",
|
||||||
|
height = "8vh",
|
||||||
|
text = "Item " .. i
|
||||||
|
})
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Grid Layout:
|
||||||
|
```lua
|
||||||
|
-- 3x3 grid
|
||||||
|
local grid = FlexLove.Element.new({
|
||||||
|
positioning = FlexLove.Positioning.GRID,
|
||||||
|
gridRows = 3,
|
||||||
|
gridColumns = 3,
|
||||||
|
columnGap = 10,
|
||||||
|
rowGap = 10,
|
||||||
|
width = "60vw",
|
||||||
|
height = "60vh"
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Add 9 children (auto-placed in grid)
|
||||||
|
for i = 1, 9 do
|
||||||
|
FlexLove.Element.new({
|
||||||
|
parent = grid,
|
||||||
|
text = "Cell " .. i
|
||||||
|
})
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Theming:
|
||||||
|
```lua
|
||||||
|
-- Load and activate a theme
|
||||||
|
FlexLove.Theme.load("space")
|
||||||
|
FlexLove.Theme.setActive("space")
|
||||||
|
|
||||||
|
-- Use theme component
|
||||||
|
local button = FlexLove.Element.new({
|
||||||
|
themeComponent = "button",
|
||||||
|
text = "Themed Button",
|
||||||
|
callback = function(element, event)
|
||||||
|
print("Clicked!")
|
||||||
|
end
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Access theme resources
|
||||||
|
local primaryColor = FlexLove.Theme.getColor("primary")
|
||||||
|
local headingFont = FlexLove.Theme.getFont("heading")
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Animations:
|
||||||
|
```lua
|
||||||
|
-- Fade animation
|
||||||
|
local fadeIn = FlexLove.Animation.fade(1.0, 0, 1)
|
||||||
|
fadeIn:apply(element)
|
||||||
|
|
||||||
|
-- Scale animation
|
||||||
|
local scaleUp = FlexLove.Animation.scale(0.5,
|
||||||
|
{ width = 100, height = 50 },
|
||||||
|
{ width = 200, height = 100 }
|
||||||
|
)
|
||||||
|
scaleUp:apply(element)
|
||||||
|
|
||||||
|
-- Custom animation with easing
|
||||||
|
local customAnim = FlexLove.Animation.new({
|
||||||
|
duration = 1.0,
|
||||||
|
start = { opacity = 0, width = 100 },
|
||||||
|
final = { opacity = 1, width = 200 },
|
||||||
|
easing = "easeInOutCubic"
|
||||||
|
})
|
||||||
|
customAnim:apply(element)
|
||||||
|
```
|
||||||
|
|
||||||
|
7. Event Handling:
|
||||||
|
```lua
|
||||||
|
local button = FlexLove.Element.new({
|
||||||
|
text = "Interactive",
|
||||||
|
callback = function(element, event)
|
||||||
|
if event.type == "click" then
|
||||||
|
print("Clicked with button:", event.button)
|
||||||
|
print("Position:", event.x, event.y)
|
||||||
|
print("Modifiers:", event.modifiers.shift, event.modifiers.ctrl)
|
||||||
|
elseif event.type == "press" then
|
||||||
|
print("Button pressed")
|
||||||
|
elseif event.type == "release" then
|
||||||
|
print("Button released")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
VERSION: 1.0.0
|
||||||
|
LICENSE: MIT
|
||||||
|
================================================================================
|
||||||
|
]]
|
||||||
|
|
||||||
|
-- ====================
|
||||||
|
-- Error Handling Utilities
|
||||||
|
-- ====================
|
||||||
|
|
||||||
|
--- Standardized error message formatter
|
||||||
|
---@param module string -- Module name (e.g., "Color", "Theme", "Units")
|
||||||
|
---@param message string -- Error message
|
||||||
|
---@return string -- Formatted error message
|
||||||
|
local function formatError(module, message)
|
||||||
|
return string.format("[FlexLove.%s] %s", module, message)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Safe function call wrapper with error handling
|
||||||
|
---@param fn function -- Function to call
|
||||||
|
---@param errorContext string? -- Optional context for error message
|
||||||
|
---@return boolean success, any result -- Returns success status and result or error message
|
||||||
|
local function safecall(fn, errorContext)
|
||||||
|
local success, result = pcall(fn)
|
||||||
|
if not success and errorContext then
|
||||||
|
print(formatError("Core", errorContext .. ": " .. tostring(result)))
|
||||||
|
end
|
||||||
|
return success, result
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ====================
|
||||||
|
-- Color System
|
||||||
|
-- ====================
|
||||||
|
|
||||||
|
--- Utility class for color handling
|
||||||
---@class Color
|
---@class Color
|
||||||
---@field r number -- Red component (0-1)
|
---@field r number -- Red component (0-1)
|
||||||
---@field g number -- Green component (0-1)
|
---@field g number -- Green component (0-1)
|
||||||
@@ -28,8 +280,10 @@ function Color:toRGBA()
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- Convert hex string to color
|
--- Convert hex string to color
|
||||||
|
--- Supports both 6-digit (#RRGGBB) and 8-digit (#RRGGBBAA) hex formats
|
||||||
---@param hexWithTag string -- e.g. "#RRGGBB" or "#RRGGBBAA"
|
---@param hexWithTag string -- e.g. "#RRGGBB" or "#RRGGBBAA"
|
||||||
---@return Color
|
---@return Color
|
||||||
|
---@throws Error if hex string format is invalid
|
||||||
function Color.fromHex(hexWithTag)
|
function Color.fromHex(hexWithTag)
|
||||||
local hex = hexWithTag:gsub("#", "")
|
local hex = hexWithTag:gsub("#", "")
|
||||||
if #hex == 6 then
|
if #hex == 6 then
|
||||||
@@ -44,7 +298,7 @@ function Color.fromHex(hexWithTag)
|
|||||||
local a = tonumber("0x" .. hex:sub(7, 8)) / 255
|
local a = tonumber("0x" .. hex:sub(7, 8)) / 255
|
||||||
return Color.new(r, g, b, a)
|
return Color.new(r, g, b, a)
|
||||||
else
|
else
|
||||||
error("Invalid hex string")
|
error(formatError("Color", string.format("Invalid hex string format: '%s'. Expected #RRGGBB or #RRGGBBAA", hexWithTag)))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -138,10 +392,64 @@ local function resolveImagePath(imagePath)
|
|||||||
return FLEXLOVE_FILESYSTEM_PATH .. "/" .. imagePath
|
return FLEXLOVE_FILESYSTEM_PATH .. "/" .. imagePath
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Safely load an image with error handling
|
||||||
|
---@param imagePath string
|
||||||
|
---@return love.Image?, string? -- Returns image or nil, error message
|
||||||
|
local function safeLoadImage(imagePath)
|
||||||
|
local success, result = pcall(function()
|
||||||
|
return love.graphics.newImage(imagePath)
|
||||||
|
end)
|
||||||
|
|
||||||
|
if success then
|
||||||
|
return result, nil
|
||||||
|
else
|
||||||
|
local errorMsg = string.format("[FlexLove] Failed to load image: %s - %s", imagePath, tostring(result))
|
||||||
|
print(errorMsg)
|
||||||
|
return nil, errorMsg
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
--- Create a new theme instance
|
--- Create a new theme instance
|
||||||
---@param definition ThemeDefinition
|
---@param definition ThemeDefinition
|
||||||
---@return Theme
|
---@return Theme
|
||||||
|
--- Validate theme definition structure
|
||||||
|
---@param definition ThemeDefinition
|
||||||
|
---@return boolean, string? -- Returns true if valid, or false with error message
|
||||||
|
local function validateThemeDefinition(definition)
|
||||||
|
if not definition then
|
||||||
|
return false, "Theme definition is nil"
|
||||||
|
end
|
||||||
|
|
||||||
|
if type(definition) ~= "table" then
|
||||||
|
return false, "Theme definition must be a table"
|
||||||
|
end
|
||||||
|
|
||||||
|
if not definition.name or type(definition.name) ~= "string" then
|
||||||
|
return false, "Theme must have a 'name' field (string)"
|
||||||
|
end
|
||||||
|
|
||||||
|
if definition.components and type(definition.components) ~= "table" then
|
||||||
|
return false, "Theme 'components' must be a table"
|
||||||
|
end
|
||||||
|
|
||||||
|
if definition.colors and type(definition.colors) ~= "table" then
|
||||||
|
return false, "Theme 'colors' must be a table"
|
||||||
|
end
|
||||||
|
|
||||||
|
if definition.fonts and type(definition.fonts) ~= "table" then
|
||||||
|
return false, "Theme 'fonts' must be a table"
|
||||||
|
end
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
end
|
||||||
|
|
||||||
function Theme.new(definition)
|
function Theme.new(definition)
|
||||||
|
-- Validate theme definition
|
||||||
|
local valid, err = validateThemeDefinition(definition)
|
||||||
|
if not valid then
|
||||||
|
error("[FlexLove] Invalid theme definition: " .. tostring(err))
|
||||||
|
end
|
||||||
|
|
||||||
local self = setmetatable({}, Theme)
|
local self = setmetatable({}, Theme)
|
||||||
self.name = definition.name
|
self.name = definition.name
|
||||||
|
|
||||||
@@ -149,7 +457,12 @@ function Theme.new(definition)
|
|||||||
if definition.atlas then
|
if definition.atlas then
|
||||||
if type(definition.atlas) == "string" then
|
if type(definition.atlas) == "string" then
|
||||||
local resolvedPath = resolveImagePath(definition.atlas)
|
local resolvedPath = resolveImagePath(definition.atlas)
|
||||||
self.atlas = love.graphics.newImage(resolvedPath)
|
local image, err = safeLoadImage(resolvedPath)
|
||||||
|
if image then
|
||||||
|
self.atlas = image
|
||||||
|
else
|
||||||
|
print("[FlexLove] Warning: Failed to load global atlas for theme '" .. definition.name .. "'")
|
||||||
|
end
|
||||||
else
|
else
|
||||||
self.atlas = definition.atlas
|
self.atlas = definition.atlas
|
||||||
end
|
end
|
||||||
@@ -164,7 +477,12 @@ function Theme.new(definition)
|
|||||||
if component.atlas then
|
if component.atlas then
|
||||||
if type(component.atlas) == "string" then
|
if type(component.atlas) == "string" then
|
||||||
local resolvedPath = resolveImagePath(component.atlas)
|
local resolvedPath = resolveImagePath(component.atlas)
|
||||||
component._loadedAtlas = love.graphics.newImage(resolvedPath)
|
local image, err = safeLoadImage(resolvedPath)
|
||||||
|
if image then
|
||||||
|
component._loadedAtlas = image
|
||||||
|
else
|
||||||
|
print("[FlexLove] Warning: Failed to load atlas for component '" .. componentName .. "'")
|
||||||
|
end
|
||||||
else
|
else
|
||||||
component._loadedAtlas = component.atlas
|
component._loadedAtlas = component.atlas
|
||||||
end
|
end
|
||||||
@@ -249,9 +567,9 @@ function Theme.getActive()
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- Get a component from the active theme
|
--- Get a component from the active theme
|
||||||
---@param componentName string
|
---@param componentName string -- Name of the component (e.g., "button", "panel")
|
||||||
---@param state string?
|
---@param state string? -- Optional state (e.g., "hover", "pressed", "disabled")
|
||||||
---@return ThemeComponent?
|
---@return ThemeComponent? -- Returns component or nil if not found
|
||||||
function Theme.getComponent(componentName, state)
|
function Theme.getComponent(componentName, state)
|
||||||
if not activeTheme then
|
if not activeTheme then
|
||||||
return nil
|
return nil
|
||||||
@@ -270,6 +588,44 @@ function Theme.getComponent(componentName, state)
|
|||||||
return component
|
return component
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Get a font from the active theme
|
||||||
|
---@param fontName string -- Name of the font family (e.g., "default", "heading")
|
||||||
|
---@return string? -- Returns font path or nil if not found
|
||||||
|
function Theme.getFont(fontName)
|
||||||
|
if not activeTheme then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
return activeTheme.fonts and activeTheme.fonts[fontName]
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Get a color from the active theme
|
||||||
|
---@param colorName string -- Name of the color (e.g., "primary", "secondary")
|
||||||
|
---@return Color? -- Returns Color instance or nil if not found
|
||||||
|
function Theme.getColor(colorName)
|
||||||
|
if not activeTheme then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
return activeTheme.colors and activeTheme.colors[colorName]
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Check if a theme is currently active
|
||||||
|
---@return boolean -- Returns true if a theme is active
|
||||||
|
function Theme.hasActive()
|
||||||
|
return activeTheme ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Get all registered theme names
|
||||||
|
---@return table<string> -- Array of theme names
|
||||||
|
function Theme.getRegisteredThemes()
|
||||||
|
local themeNames = {}
|
||||||
|
for name, _ in pairs(themes) do
|
||||||
|
table.insert(themeNames, name)
|
||||||
|
end
|
||||||
|
return themeNames
|
||||||
|
end
|
||||||
|
|
||||||
-- ====================
|
-- ====================
|
||||||
-- Rounded Rectangle Helper
|
-- Rounded Rectangle Helper
|
||||||
-- ====================
|
-- ====================
|
||||||
@@ -635,18 +991,19 @@ function Units.parse(value)
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- Convert relative units to pixels based on viewport and parent dimensions
|
--- Convert relative units to pixels based on viewport and parent dimensions
|
||||||
---@param value number
|
---@param value number -- Numeric value to convert
|
||||||
---@param unit string
|
---@param unit string -- Unit type ("px", "%", "vw", "vh", "ew", "eh")
|
||||||
---@param viewportWidth number
|
---@param viewportWidth number -- Current viewport width in pixels
|
||||||
---@param viewportHeight number
|
---@param viewportHeight number -- Current viewport height in pixels
|
||||||
---@param parentSize number? -- Required for percentage units
|
---@param parentSize number? -- Required for percentage units (parent dimension)
|
||||||
---@return number -- Pixel value
|
---@return number -- Resolved pixel value
|
||||||
|
---@throws Error if unit type is unknown or percentage used without parent size
|
||||||
function Units.resolve(value, unit, viewportWidth, viewportHeight, parentSize)
|
function Units.resolve(value, unit, viewportWidth, viewportHeight, parentSize)
|
||||||
if unit == "px" then
|
if unit == "px" then
|
||||||
return value
|
return value
|
||||||
elseif unit == "%" then
|
elseif unit == "%" then
|
||||||
if not parentSize then
|
if not parentSize then
|
||||||
error("Percentage units require parent dimension")
|
error(formatError("Units", "Percentage units require parent dimension"))
|
||||||
end
|
end
|
||||||
return (value / 100) * parentSize
|
return (value / 100) * parentSize
|
||||||
elseif unit == "vw" then
|
elseif unit == "vw" then
|
||||||
@@ -654,18 +1011,22 @@ function Units.resolve(value, unit, viewportWidth, viewportHeight, parentSize)
|
|||||||
elseif unit == "vh" then
|
elseif unit == "vh" then
|
||||||
return (value / 100) * viewportHeight
|
return (value / 100) * viewportHeight
|
||||||
else
|
else
|
||||||
error("Unknown unit type: " .. unit)
|
error(formatError("Units", string.format("Unknown unit type: '%s'. Valid units: px, %%, vw, vh, ew, eh", unit)))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
---@return number, number -- width, height
|
---@return number, number -- width, height
|
||||||
function Units.getViewport()
|
function Units.getViewport()
|
||||||
-- Try both functions to be compatible with different love versions and test environments
|
-- Return cached viewport if available (only during resize operations)
|
||||||
|
if Gui and Gui._cachedViewport and Gui._cachedViewport.width > 0 then
|
||||||
|
return Gui._cachedViewport.width, Gui._cachedViewport.height
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Query viewport dimensions normally
|
||||||
if love.graphics and love.graphics.getDimensions then
|
if love.graphics and love.graphics.getDimensions then
|
||||||
return love.graphics.getDimensions()
|
return love.graphics.getDimensions()
|
||||||
else
|
else
|
||||||
local w, h = love.window.getMode()
|
return love.window.getMode()
|
||||||
return w, h
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -737,6 +1098,30 @@ function Units.resolveSpacing(spacingProps, parentWidth, parentHeight)
|
|||||||
return result
|
return result
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Check if a unit string is valid
|
||||||
|
---@param unitStr string -- Unit string to validate (e.g., "10px", "50%", "20vw")
|
||||||
|
---@return boolean -- Returns true if unit string is valid
|
||||||
|
function Units.isValid(unitStr)
|
||||||
|
if type(unitStr) ~= "string" then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local value, unit = Units.parse(unitStr)
|
||||||
|
local validUnits = { px = true, ["%"] = true, vw = true, vh = true, ew = true, eh = true }
|
||||||
|
return validUnits[unit] == true
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Parse and resolve a unit value in one call
|
||||||
|
---@param value string|number -- Value to parse and resolve
|
||||||
|
---@param viewportWidth number -- Current viewport width
|
||||||
|
---@param viewportHeight number -- Current viewport height
|
||||||
|
---@param parentSize number? -- Parent dimension for percentage units
|
||||||
|
---@return number -- Resolved pixel value
|
||||||
|
function Units.parseAndResolve(value, viewportWidth, viewportHeight, parentSize)
|
||||||
|
local numValue, unit = Units.parse(value)
|
||||||
|
return Units.resolve(numValue, unit, viewportWidth, viewportHeight, parentSize)
|
||||||
|
end
|
||||||
|
|
||||||
-- ====================
|
-- ====================
|
||||||
-- Grid System
|
-- Grid System
|
||||||
-- ====================
|
-- ====================
|
||||||
@@ -884,6 +1269,7 @@ local Gui = {
|
|||||||
baseScale = nil,
|
baseScale = nil,
|
||||||
scaleFactors = { x = 1.0, y = 1.0 },
|
scaleFactors = { x = 1.0, y = 1.0 },
|
||||||
defaultTheme = nil,
|
defaultTheme = nil,
|
||||||
|
_cachedViewport = { width = 0, height = 0 }, -- Cached viewport dimensions
|
||||||
}
|
}
|
||||||
|
|
||||||
--- Initialize FlexLove with configuration
|
--- Initialize FlexLove with configuration
|
||||||
@@ -956,10 +1342,63 @@ function Gui.draw()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Find the topmost element at given coordinates (considering z-index)
|
||||||
|
---@param x number
|
||||||
|
---@param y number
|
||||||
|
---@return Element? -- Returns the topmost element or nil
|
||||||
|
function Gui.getElementAtPosition(x, y)
|
||||||
|
local candidates = {}
|
||||||
|
|
||||||
|
-- Recursively collect all elements that contain the point
|
||||||
|
local function collectHits(element)
|
||||||
|
-- Check if point is within element bounds
|
||||||
|
local bx = element.x
|
||||||
|
local by = element.y
|
||||||
|
local bw = element._borderBoxWidth or (element.width + element.padding.left + element.padding.right)
|
||||||
|
local bh = element._borderBoxHeight or (element.height + element.padding.top + element.padding.bottom)
|
||||||
|
|
||||||
|
if x >= bx and x <= bx + bw and y >= by and y <= by + bh then
|
||||||
|
-- Only consider elements with callbacks (interactive elements)
|
||||||
|
if element.callback and not element.disabled then
|
||||||
|
table.insert(candidates, element)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check children
|
||||||
|
for _, child in ipairs(element.children) do
|
||||||
|
collectHits(child)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Collect hits from all top-level elements
|
||||||
|
for _, element in ipairs(Gui.topElements) do
|
||||||
|
collectHits(element)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Sort by z-index (highest first)
|
||||||
|
table.sort(candidates, function(a, b)
|
||||||
|
return a.z > b.z
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Return the topmost element (highest z-index)
|
||||||
|
return candidates[1]
|
||||||
|
end
|
||||||
|
|
||||||
function Gui.update(dt)
|
function Gui.update(dt)
|
||||||
|
-- Reset event handling flags for new frame
|
||||||
|
local mx, my = love.mouse.getPosition()
|
||||||
|
local topElement = Gui.getElementAtPosition(mx, my)
|
||||||
|
|
||||||
|
-- Mark which element should handle events this frame
|
||||||
|
Gui._activeEventElement = topElement
|
||||||
|
|
||||||
|
-- Update all elements
|
||||||
for _, win in ipairs(Gui.topElements) do
|
for _, win in ipairs(Gui.topElements) do
|
||||||
win:update(dt)
|
win:update(dt)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Clear active element for next frame
|
||||||
|
Gui._activeEventElement = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Destroy all elements and their children
|
--- Destroy all elements and their children
|
||||||
@@ -1033,6 +1472,39 @@ end
|
|||||||
---@field elapsed number
|
---@field elapsed number
|
||||||
---@field transform table?
|
---@field transform table?
|
||||||
---@field transition table?
|
---@field transition table?
|
||||||
|
--- Easing functions for animations
|
||||||
|
local Easing = {
|
||||||
|
linear = function(t) return t end,
|
||||||
|
|
||||||
|
easeInQuad = function(t) return t * t end,
|
||||||
|
easeOutQuad = function(t) return t * (2 - t) end,
|
||||||
|
easeInOutQuad = function(t)
|
||||||
|
return t < 0.5 and 2 * t * t or -1 + (4 - 2 * t) * t
|
||||||
|
end,
|
||||||
|
|
||||||
|
easeInCubic = function(t) return t * t * t end,
|
||||||
|
easeOutCubic = function(t)
|
||||||
|
local t1 = t - 1
|
||||||
|
return t1 * t1 * t1 + 1
|
||||||
|
end,
|
||||||
|
easeInOutCubic = function(t)
|
||||||
|
return t < 0.5 and 4 * t * t * t or (t - 1) * (2 * t - 2) * (2 * t - 2) + 1
|
||||||
|
end,
|
||||||
|
|
||||||
|
easeInQuart = function(t) return t * t * t * t end,
|
||||||
|
easeOutQuart = function(t)
|
||||||
|
local t1 = t - 1
|
||||||
|
return 1 - t1 * t1 * t1 * t1
|
||||||
|
end,
|
||||||
|
|
||||||
|
easeInExpo = function(t)
|
||||||
|
return t == 0 and 0 or math.pow(2, 10 * (t - 1))
|
||||||
|
end,
|
||||||
|
easeOutExpo = function(t)
|
||||||
|
return t == 1 and 1 or 1 - math.pow(2, -10 * t)
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
|
||||||
local Animation = {}
|
local Animation = {}
|
||||||
Animation.__index = Animation
|
Animation.__index = Animation
|
||||||
|
|
||||||
@@ -1064,6 +1536,15 @@ function Animation.new(props)
|
|||||||
self.transform = props.transform
|
self.transform = props.transform
|
||||||
self.transition = props.transition
|
self.transition = props.transition
|
||||||
self.elapsed = 0
|
self.elapsed = 0
|
||||||
|
|
||||||
|
-- Set easing function (default to linear)
|
||||||
|
local easingName = props.easing or "linear"
|
||||||
|
self.easing = Easing[easingName] or Easing.linear
|
||||||
|
|
||||||
|
-- Pre-allocate result table to avoid GC pressure
|
||||||
|
self._cachedResult = {}
|
||||||
|
self._resultDirty = true
|
||||||
|
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1071,6 +1552,7 @@ end
|
|||||||
---@return boolean
|
---@return boolean
|
||||||
function Animation:update(dt)
|
function Animation:update(dt)
|
||||||
self.elapsed = self.elapsed + dt
|
self.elapsed = self.elapsed + dt
|
||||||
|
self._resultDirty = true -- Mark cached result as dirty
|
||||||
if self.elapsed >= self.duration then
|
if self.elapsed >= self.duration then
|
||||||
return true -- finished
|
return true -- finished
|
||||||
else
|
else
|
||||||
@@ -1080,8 +1562,19 @@ end
|
|||||||
|
|
||||||
---@return table
|
---@return table
|
||||||
function Animation:interpolate()
|
function Animation:interpolate()
|
||||||
|
-- Return cached result if not dirty (avoids recalculation)
|
||||||
|
if not self._resultDirty then
|
||||||
|
return self._cachedResult
|
||||||
|
end
|
||||||
|
|
||||||
local t = math.min(self.elapsed / self.duration, 1)
|
local t = math.min(self.elapsed / self.duration, 1)
|
||||||
local result = {}
|
t = self.easing(t) -- Apply easing function
|
||||||
|
local result = self._cachedResult -- Reuse existing table
|
||||||
|
|
||||||
|
-- Clear previous values
|
||||||
|
result.width = nil
|
||||||
|
result.height = nil
|
||||||
|
result.opacity = nil
|
||||||
|
|
||||||
-- Handle width and height if present
|
-- Handle width and height if present
|
||||||
if self.start.width and self.final.width then
|
if self.start.width and self.final.width then
|
||||||
@@ -1104,6 +1597,7 @@ function Animation:interpolate()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
self._resultDirty = false -- Mark as clean
|
||||||
return result
|
return result
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1149,6 +1643,8 @@ function Animation.scale(duration, fromScale, toScale)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local FONT_CACHE = {}
|
local FONT_CACHE = {}
|
||||||
|
local FONT_CACHE_MAX_SIZE = 50 -- Limit cache size to prevent unbounded growth
|
||||||
|
local FONT_CACHE_ORDER = {} -- Track access order for LRU eviction
|
||||||
|
|
||||||
--- Create or get a font from cache
|
--- Create or get a font from cache
|
||||||
---@param size number
|
---@param size number
|
||||||
@@ -1175,6 +1671,15 @@ function FONT_CACHE.get(size, fontPath)
|
|||||||
-- Load default font
|
-- Load default font
|
||||||
FONT_CACHE[cacheKey] = love.graphics.newFont(size)
|
FONT_CACHE[cacheKey] = love.graphics.newFont(size)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Add to access order for LRU tracking
|
||||||
|
table.insert(FONT_CACHE_ORDER, cacheKey)
|
||||||
|
|
||||||
|
-- Evict oldest entry if cache is full (LRU eviction)
|
||||||
|
if #FONT_CACHE_ORDER > FONT_CACHE_MAX_SIZE then
|
||||||
|
local oldestKey = table.remove(FONT_CACHE_ORDER, 1)
|
||||||
|
FONT_CACHE[oldestKey] = nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
return FONT_CACHE[cacheKey]
|
return FONT_CACHE[cacheKey]
|
||||||
end
|
end
|
||||||
@@ -1219,6 +1724,33 @@ end
|
|||||||
-- ====================
|
-- ====================
|
||||||
-- Element Object
|
-- Element Object
|
||||||
-- ====================
|
-- ====================
|
||||||
|
|
||||||
|
--[[
|
||||||
|
INTERNAL FIELD NAMING CONVENTIONS:
|
||||||
|
---------------------------------
|
||||||
|
Fields prefixed with underscore (_) are internal/private and should not be accessed directly:
|
||||||
|
|
||||||
|
- _pressed: Internal state tracking for mouse button presses
|
||||||
|
- _lastClickTime: Internal timestamp for double-click detection
|
||||||
|
- _lastClickButton: Internal button tracking for click events
|
||||||
|
- _clickCount: Internal counter for multi-click detection
|
||||||
|
- _touchPressed: Internal touch state tracking
|
||||||
|
- _themeState: Internal current theme state (managed automatically)
|
||||||
|
- _borderBoxWidth: Internal cached border-box width (optimization)
|
||||||
|
- _borderBoxHeight: Internal cached border-box height (optimization)
|
||||||
|
- _explicitlyAbsolute: Internal flag for positioning logic
|
||||||
|
- _originalPositioning: Internal original positioning value
|
||||||
|
- _cachedResult: Internal animation cache (Animation class)
|
||||||
|
- _resultDirty: Internal animation dirty flag (Animation class)
|
||||||
|
- _loadedAtlas: Internal cached atlas image (ThemeComponent)
|
||||||
|
- _cachedViewport: Internal viewport cache (Gui class)
|
||||||
|
|
||||||
|
Public API methods to access internal state:
|
||||||
|
- Element:getBorderBoxWidth() - Get border-box width
|
||||||
|
- Element:getBorderBoxHeight() - Get border-box height
|
||||||
|
- Element:getBounds() - Get element bounds
|
||||||
|
]]
|
||||||
|
|
||||||
---@class Element
|
---@class Element
|
||||||
---@field id string
|
---@field id string
|
||||||
---@field autosizing {width:boolean, height:boolean} -- Whether the element should automatically size to fit its children
|
---@field autosizing {width:boolean, height:boolean} -- Whether the element should automatically size to fit its children
|
||||||
@@ -2488,10 +3020,18 @@ function Element:destroy()
|
|||||||
|
|
||||||
-- Clear animation reference
|
-- Clear animation reference
|
||||||
self.animation = nil
|
self.animation = nil
|
||||||
|
|
||||||
|
-- Clear callback to prevent closure leaks
|
||||||
|
self.callback = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Draw element and its children
|
--- Draw element and its children
|
||||||
function Element:draw()
|
function Element:draw()
|
||||||
|
-- Early exit if element is invisible (optimization)
|
||||||
|
if self.opacity <= 0 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
-- Handle opacity during animation
|
-- Handle opacity during animation
|
||||||
local drawBackgroundColor = self.backgroundColor
|
local drawBackgroundColor = self.backgroundColor
|
||||||
if self.animation then
|
if self.animation then
|
||||||
@@ -2502,6 +3042,10 @@ function Element:draw()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Cache border box dimensions for this draw call (optimization)
|
||||||
|
local borderBoxWidth = self._borderBoxWidth or (self.width + self.padding.left + self.padding.right)
|
||||||
|
local borderBoxHeight = self._borderBoxHeight or (self.height + self.padding.top + self.padding.bottom)
|
||||||
|
|
||||||
-- LAYER 1: Draw backgroundColor first (behind everything)
|
-- LAYER 1: Draw backgroundColor first (behind everything)
|
||||||
-- Apply opacity to all drawing operations
|
-- Apply opacity to all drawing operations
|
||||||
-- (x, y) represents border box, so draw background from (x, y)
|
-- (x, y) represents border box, so draw background from (x, y)
|
||||||
@@ -2513,8 +3057,8 @@ function Element:draw()
|
|||||||
"fill",
|
"fill",
|
||||||
self.x,
|
self.x,
|
||||||
self.y,
|
self.y,
|
||||||
self._borderBoxWidth or (self.width + self.padding.left + self.padding.right),
|
borderBoxWidth,
|
||||||
self._borderBoxHeight or (self.height + self.padding.top + self.padding.bottom),
|
borderBoxHeight,
|
||||||
self.cornerRadius
|
self.cornerRadius
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2551,10 +3095,18 @@ function Element:draw()
|
|||||||
-- Use component-specific atlas if available, otherwise use theme atlas
|
-- Use component-specific atlas if available, otherwise use theme atlas
|
||||||
local atlasToUse = component._loadedAtlas or themeToUse.atlas
|
local atlasToUse = component._loadedAtlas or themeToUse.atlas
|
||||||
|
|
||||||
if atlasToUse then
|
if atlasToUse and component.regions then
|
||||||
NineSlice.draw(component, atlasToUse, self.x, self.y, self.width, self.height, self.padding, self.opacity)
|
-- Validate component has required structure
|
||||||
else
|
local hasAllRegions = component.regions.topLeft and component.regions.topCenter and
|
||||||
print("[FlexLove] No atlas for component: " .. self.themeComponent)
|
component.regions.topRight and component.regions.middleLeft and
|
||||||
|
component.regions.middleCenter and component.regions.middleRight and
|
||||||
|
component.regions.bottomLeft and component.regions.bottomCenter and
|
||||||
|
component.regions.bottomRight
|
||||||
|
if hasAllRegions then
|
||||||
|
NineSlice.draw(component, atlasToUse, self.x, self.y, self.width, self.height, self.padding, self.opacity)
|
||||||
|
else
|
||||||
|
-- Silently skip drawing if component structure is invalid
|
||||||
|
end
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
print("[FlexLove] Component not found: " .. self.themeComponent .. " in theme: " .. themeToUse.name)
|
print("[FlexLove] Component not found: " .. self.themeComponent .. " in theme: " .. themeToUse.name)
|
||||||
@@ -2572,10 +3124,6 @@ function Element:draw()
|
|||||||
-- Check if all borders are enabled
|
-- Check if all borders are enabled
|
||||||
local allBorders = self.border.top and self.border.bottom and self.border.left and self.border.right
|
local allBorders = self.border.top and self.border.bottom and self.border.left and self.border.right
|
||||||
|
|
||||||
-- BORDER-BOX MODEL: Use stored border-box dimensions for drawing
|
|
||||||
local borderBoxWidth = self._borderBoxWidth or (self.width + self.padding.left + self.padding.right)
|
|
||||||
local borderBoxHeight = self._borderBoxHeight or (self.height + self.padding.top + self.padding.bottom)
|
|
||||||
|
|
||||||
if allBorders then
|
if allBorders then
|
||||||
-- Draw complete rounded rectangle border
|
-- Draw complete rounded rectangle border
|
||||||
RoundedRect.draw("line", self.x, self.y, borderBoxWidth, borderBoxHeight, self.cornerRadius)
|
RoundedRect.draw("line", self.x, self.y, borderBoxWidth, borderBoxHeight, self.cornerRadius)
|
||||||
@@ -2774,8 +3322,10 @@ function Element:update(dt)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Only process button events if callback exists and element is not disabled
|
-- Only process button events if callback exists, element is not disabled,
|
||||||
if self.callback and not self.disabled then
|
-- and this is the topmost element at the mouse position (z-index ordering)
|
||||||
|
local isActiveElement = (Gui._activeEventElement == nil or Gui._activeEventElement == self)
|
||||||
|
if self.callback and not self.disabled and isActiveElement then
|
||||||
-- Check all three mouse buttons
|
-- Check all three mouse buttons
|
||||||
local buttons = { 1, 2, 3 } -- left, right, middle
|
local buttons = { 1, 2, 3 } -- left, right, middle
|
||||||
|
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ function TestGridLayout:test_nested_grids()
|
|||||||
-- Inner grid should be stretched to fill outer grid cell (200x200)
|
-- Inner grid should be stretched to fill outer grid cell (200x200)
|
||||||
lu.assertAlmostEquals(innerGrid.width, 200, 1)
|
lu.assertAlmostEquals(innerGrid.width, 200, 1)
|
||||||
lu.assertAlmostEquals(innerGrid.height, 200, 1)
|
lu.assertAlmostEquals(innerGrid.height, 200, 1)
|
||||||
|
|
||||||
-- Items in inner grid should be positioned correctly
|
-- Items in inner grid should be positioned correctly
|
||||||
-- Each cell in inner grid is 100x100
|
-- Each cell in inner grid is 100x100
|
||||||
lu.assertAlmostEquals(item1.x, 0, 1)
|
lu.assertAlmostEquals(item1.x, 0, 1)
|
||||||
@@ -342,4 +342,4 @@ function TestGridLayout:test_single_cell_grid()
|
|||||||
end
|
end
|
||||||
|
|
||||||
print("Running Simplified Grid Layout Tests...")
|
print("Running Simplified Grid Layout Tests...")
|
||||||
os.exit(lu.LuaUnit.run())
|
lu.LuaUnit.run()
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
|
|
||||||
package.path = package.path .. ";?.lua"
|
package.path = package.path .. ";?.lua"
|
||||||
|
|
||||||
local lu = require("testing/luaunit")
|
local lu = require("testing.luaunit")
|
||||||
require("testing/loveStub") -- Required to mock LOVE functions
|
require("testing.loveStub") -- Required to mock LOVE functions
|
||||||
local FlexLove = require("FlexLove")
|
local FlexLove = require("FlexLove")
|
||||||
|
|
||||||
local Gui = FlexLove.GUI
|
local Gui = FlexLove.GUI
|
||||||
@@ -26,7 +26,7 @@ end
|
|||||||
-- Test 1: Event object structure
|
-- Test 1: Event object structure
|
||||||
function TestEventSystem:test_event_object_has_required_fields()
|
function TestEventSystem:test_event_object_has_required_fields()
|
||||||
local eventReceived = nil
|
local eventReceived = nil
|
||||||
|
|
||||||
local button = Gui.new({
|
local button = Gui.new({
|
||||||
x = 100,
|
x = 100,
|
||||||
y = 100,
|
y = 100,
|
||||||
@@ -36,15 +36,15 @@ function TestEventSystem:test_event_object_has_required_fields()
|
|||||||
eventReceived = event
|
eventReceived = event
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
|
|
||||||
-- Simulate mouse press and release
|
-- Simulate mouse press and release
|
||||||
love.mouse.setPosition(150, 150)
|
love.mouse.setPosition(150, 150)
|
||||||
love.mouse.setDown(1, true)
|
love.mouse.setDown(1, true)
|
||||||
button:update(0.016)
|
button:update(0.016)
|
||||||
|
|
||||||
love.mouse.setDown(1, false)
|
love.mouse.setDown(1, false)
|
||||||
button:update(0.016)
|
button:update(0.016)
|
||||||
|
|
||||||
-- Verify event object structure
|
-- Verify event object structure
|
||||||
lu.assertNotNil(eventReceived, "Event should be received")
|
lu.assertNotNil(eventReceived, "Event should be received")
|
||||||
lu.assertNotNil(eventReceived.type, "Event should have type field")
|
lu.assertNotNil(eventReceived.type, "Event should have type field")
|
||||||
@@ -59,28 +59,28 @@ end
|
|||||||
-- Test 2: Left click event
|
-- Test 2: Left click event
|
||||||
function TestEventSystem:test_left_click_generates_click_event()
|
function TestEventSystem:test_left_click_generates_click_event()
|
||||||
local eventsReceived = {}
|
local eventsReceived = {}
|
||||||
|
|
||||||
local button = Gui.new({
|
local button = Gui.new({
|
||||||
x = 100,
|
x = 100,
|
||||||
y = 100,
|
y = 100,
|
||||||
width = 200,
|
width = 200,
|
||||||
height = 100,
|
height = 100,
|
||||||
callback = function(element, event)
|
callback = function(element, event)
|
||||||
table.insert(eventsReceived, {type = event.type, button = event.button})
|
table.insert(eventsReceived, { type = event.type, button = event.button })
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
|
|
||||||
-- Simulate left click
|
-- Simulate left click
|
||||||
love.mouse.setPosition(150, 150)
|
love.mouse.setPosition(150, 150)
|
||||||
love.mouse.setDown(1, true)
|
love.mouse.setDown(1, true)
|
||||||
button:update(0.016)
|
button:update(0.016)
|
||||||
|
|
||||||
love.mouse.setDown(1, false)
|
love.mouse.setDown(1, false)
|
||||||
button:update(0.016)
|
button:update(0.016)
|
||||||
|
|
||||||
-- Should receive press, click, and release events
|
-- Should receive press, click, and release events
|
||||||
lu.assertTrue(#eventsReceived >= 2, "Should receive at least 2 events")
|
lu.assertTrue(#eventsReceived >= 2, "Should receive at least 2 events")
|
||||||
|
|
||||||
-- Check for click event
|
-- Check for click event
|
||||||
local hasClickEvent = false
|
local hasClickEvent = false
|
||||||
for _, evt in ipairs(eventsReceived) do
|
for _, evt in ipairs(eventsReceived) do
|
||||||
@@ -95,25 +95,25 @@ end
|
|||||||
-- Test 3: Right click event
|
-- Test 3: Right click event
|
||||||
function TestEventSystem:test_right_click_generates_rightclick_event()
|
function TestEventSystem:test_right_click_generates_rightclick_event()
|
||||||
local eventsReceived = {}
|
local eventsReceived = {}
|
||||||
|
|
||||||
local button = Gui.new({
|
local button = Gui.new({
|
||||||
x = 100,
|
x = 100,
|
||||||
y = 100,
|
y = 100,
|
||||||
width = 200,
|
width = 200,
|
||||||
height = 100,
|
height = 100,
|
||||||
callback = function(element, event)
|
callback = function(element, event)
|
||||||
table.insert(eventsReceived, {type = event.type, button = event.button})
|
table.insert(eventsReceived, { type = event.type, button = event.button })
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
|
|
||||||
-- Simulate right click
|
-- Simulate right click
|
||||||
love.mouse.setPosition(150, 150)
|
love.mouse.setPosition(150, 150)
|
||||||
love.mouse.setDown(2, true)
|
love.mouse.setDown(2, true)
|
||||||
button:update(0.016)
|
button:update(0.016)
|
||||||
|
|
||||||
love.mouse.setDown(2, false)
|
love.mouse.setDown(2, false)
|
||||||
button:update(0.016)
|
button:update(0.016)
|
||||||
|
|
||||||
-- Check for rightclick event
|
-- Check for rightclick event
|
||||||
local hasRightClickEvent = false
|
local hasRightClickEvent = false
|
||||||
for _, evt in ipairs(eventsReceived) do
|
for _, evt in ipairs(eventsReceived) do
|
||||||
@@ -128,25 +128,25 @@ end
|
|||||||
-- Test 4: Middle click event
|
-- Test 4: Middle click event
|
||||||
function TestEventSystem:test_middle_click_generates_middleclick_event()
|
function TestEventSystem:test_middle_click_generates_middleclick_event()
|
||||||
local eventsReceived = {}
|
local eventsReceived = {}
|
||||||
|
|
||||||
local button = Gui.new({
|
local button = Gui.new({
|
||||||
x = 100,
|
x = 100,
|
||||||
y = 100,
|
y = 100,
|
||||||
width = 200,
|
width = 200,
|
||||||
height = 100,
|
height = 100,
|
||||||
callback = function(element, event)
|
callback = function(element, event)
|
||||||
table.insert(eventsReceived, {type = event.type, button = event.button})
|
table.insert(eventsReceived, { type = event.type, button = event.button })
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
|
|
||||||
-- Simulate middle click
|
-- Simulate middle click
|
||||||
love.mouse.setPosition(150, 150)
|
love.mouse.setPosition(150, 150)
|
||||||
love.mouse.setDown(3, true)
|
love.mouse.setDown(3, true)
|
||||||
button:update(0.016)
|
button:update(0.016)
|
||||||
|
|
||||||
love.mouse.setDown(3, false)
|
love.mouse.setDown(3, false)
|
||||||
button:update(0.016)
|
button:update(0.016)
|
||||||
|
|
||||||
-- Check for middleclick event
|
-- Check for middleclick event
|
||||||
local hasMiddleClickEvent = false
|
local hasMiddleClickEvent = false
|
||||||
for _, evt in ipairs(eventsReceived) do
|
for _, evt in ipairs(eventsReceived) do
|
||||||
@@ -161,7 +161,7 @@ end
|
|||||||
-- Test 5: Modifier keys detection
|
-- Test 5: Modifier keys detection
|
||||||
function TestEventSystem:test_modifier_keys_are_detected()
|
function TestEventSystem:test_modifier_keys_are_detected()
|
||||||
local eventReceived = nil
|
local eventReceived = nil
|
||||||
|
|
||||||
local button = Gui.new({
|
local button = Gui.new({
|
||||||
x = 100,
|
x = 100,
|
||||||
y = 100,
|
y = 100,
|
||||||
@@ -173,16 +173,16 @@ function TestEventSystem:test_modifier_keys_are_detected()
|
|||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
|
|
||||||
-- Simulate shift + click
|
-- Simulate shift + click
|
||||||
love.keyboard.setDown("lshift", true)
|
love.keyboard.setDown("lshift", true)
|
||||||
love.mouse.setPosition(150, 150)
|
love.mouse.setPosition(150, 150)
|
||||||
love.mouse.setDown(1, true)
|
love.mouse.setDown(1, true)
|
||||||
button:update(0.016)
|
button:update(0.016)
|
||||||
|
|
||||||
love.mouse.setDown(1, false)
|
love.mouse.setDown(1, false)
|
||||||
button:update(0.016)
|
button:update(0.016)
|
||||||
|
|
||||||
lu.assertNotNil(eventReceived, "Should receive click event")
|
lu.assertNotNil(eventReceived, "Should receive click event")
|
||||||
lu.assertTrue(eventReceived.modifiers.shift, "Shift modifier should be detected")
|
lu.assertTrue(eventReceived.modifiers.shift, "Shift modifier should be detected")
|
||||||
end
|
end
|
||||||
@@ -190,7 +190,7 @@ end
|
|||||||
-- Test 6: Double click detection
|
-- Test 6: Double click detection
|
||||||
function TestEventSystem:test_double_click_increments_click_count()
|
function TestEventSystem:test_double_click_increments_click_count()
|
||||||
local clickEvents = {}
|
local clickEvents = {}
|
||||||
|
|
||||||
local button = Gui.new({
|
local button = Gui.new({
|
||||||
x = 100,
|
x = 100,
|
||||||
y = 100,
|
y = 100,
|
||||||
@@ -202,21 +202,21 @@ function TestEventSystem:test_double_click_increments_click_count()
|
|||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
|
|
||||||
-- Simulate first click
|
-- Simulate first click
|
||||||
love.mouse.setPosition(150, 150)
|
love.mouse.setPosition(150, 150)
|
||||||
love.mouse.setDown(1, true)
|
love.mouse.setDown(1, true)
|
||||||
button:update(0.016)
|
button:update(0.016)
|
||||||
love.mouse.setDown(1, false)
|
love.mouse.setDown(1, false)
|
||||||
button:update(0.016)
|
button:update(0.016)
|
||||||
|
|
||||||
-- Simulate second click quickly (double-click)
|
-- Simulate second click quickly (double-click)
|
||||||
love.timer.setTime(love.timer.getTime() + 0.1) -- 100ms later
|
love.timer.setTime(love.timer.getTime() + 0.1) -- 100ms later
|
||||||
love.mouse.setDown(1, true)
|
love.mouse.setDown(1, true)
|
||||||
button:update(0.016)
|
button:update(0.016)
|
||||||
love.mouse.setDown(1, false)
|
love.mouse.setDown(1, false)
|
||||||
button:update(0.016)
|
button:update(0.016)
|
||||||
|
|
||||||
lu.assertEquals(#clickEvents, 2, "Should receive 2 click events")
|
lu.assertEquals(#clickEvents, 2, "Should receive 2 click events")
|
||||||
lu.assertEquals(clickEvents[1], 1, "First click should have clickCount = 1")
|
lu.assertEquals(clickEvents[1], 1, "First click should have clickCount = 1")
|
||||||
lu.assertEquals(clickEvents[2], 2, "Second click should have clickCount = 2")
|
lu.assertEquals(clickEvents[2], 2, "Second click should have clickCount = 2")
|
||||||
@@ -225,7 +225,7 @@ end
|
|||||||
-- Test 7: Press and release events
|
-- Test 7: Press and release events
|
||||||
function TestEventSystem:test_press_and_release_events_are_fired()
|
function TestEventSystem:test_press_and_release_events_are_fired()
|
||||||
local eventsReceived = {}
|
local eventsReceived = {}
|
||||||
|
|
||||||
local button = Gui.new({
|
local button = Gui.new({
|
||||||
x = 100,
|
x = 100,
|
||||||
y = 100,
|
y = 100,
|
||||||
@@ -235,25 +235,29 @@ function TestEventSystem:test_press_and_release_events_are_fired()
|
|||||||
table.insert(eventsReceived, event.type)
|
table.insert(eventsReceived, event.type)
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
|
|
||||||
-- Simulate click
|
-- Simulate click
|
||||||
love.mouse.setPosition(150, 150)
|
love.mouse.setPosition(150, 150)
|
||||||
love.mouse.setDown(1, true)
|
love.mouse.setDown(1, true)
|
||||||
button:update(0.016)
|
button:update(0.016)
|
||||||
|
|
||||||
love.mouse.setDown(1, false)
|
love.mouse.setDown(1, false)
|
||||||
button:update(0.016)
|
button:update(0.016)
|
||||||
|
|
||||||
-- Should receive press, click, and release
|
-- Should receive press, click, and release
|
||||||
lu.assertTrue(#eventsReceived >= 2, "Should receive multiple events")
|
lu.assertTrue(#eventsReceived >= 2, "Should receive multiple events")
|
||||||
|
|
||||||
local hasPress = false
|
local hasPress = false
|
||||||
local hasRelease = false
|
local hasRelease = false
|
||||||
for _, eventType in ipairs(eventsReceived) do
|
for _, eventType in ipairs(eventsReceived) do
|
||||||
if eventType == "press" then hasPress = true end
|
if eventType == "press" then
|
||||||
if eventType == "release" then hasRelease = true end
|
hasPress = true
|
||||||
|
end
|
||||||
|
if eventType == "release" then
|
||||||
|
hasRelease = true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
lu.assertTrue(hasPress, "Should receive press event")
|
lu.assertTrue(hasPress, "Should receive press event")
|
||||||
lu.assertTrue(hasRelease, "Should receive release event")
|
lu.assertTrue(hasRelease, "Should receive release event")
|
||||||
end
|
end
|
||||||
@@ -261,7 +265,7 @@ end
|
|||||||
-- Test 8: Mouse position in event
|
-- Test 8: Mouse position in event
|
||||||
function TestEventSystem:test_event_contains_mouse_position()
|
function TestEventSystem:test_event_contains_mouse_position()
|
||||||
local eventReceived = nil
|
local eventReceived = nil
|
||||||
|
|
||||||
local button = Gui.new({
|
local button = Gui.new({
|
||||||
x = 100,
|
x = 100,
|
||||||
y = 100,
|
y = 100,
|
||||||
@@ -273,16 +277,16 @@ function TestEventSystem:test_event_contains_mouse_position()
|
|||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
|
|
||||||
-- Simulate click at specific position
|
-- Simulate click at specific position
|
||||||
local mouseX, mouseY = 175, 125
|
local mouseX, mouseY = 175, 125
|
||||||
love.mouse.setPosition(mouseX, mouseY)
|
love.mouse.setPosition(mouseX, mouseY)
|
||||||
love.mouse.setDown(1, true)
|
love.mouse.setDown(1, true)
|
||||||
button:update(0.016)
|
button:update(0.016)
|
||||||
|
|
||||||
love.mouse.setDown(1, false)
|
love.mouse.setDown(1, false)
|
||||||
button:update(0.016)
|
button:update(0.016)
|
||||||
|
|
||||||
lu.assertNotNil(eventReceived, "Should receive click event")
|
lu.assertNotNil(eventReceived, "Should receive click event")
|
||||||
lu.assertEquals(eventReceived.x, mouseX, "Event should contain correct mouse X position")
|
lu.assertEquals(eventReceived.x, mouseX, "Event should contain correct mouse X position")
|
||||||
lu.assertEquals(eventReceived.y, mouseY, "Event should contain correct mouse Y position")
|
lu.assertEquals(eventReceived.y, mouseY, "Event should contain correct mouse Y position")
|
||||||
@@ -291,7 +295,7 @@ end
|
|||||||
-- Test 9: No callback when mouse outside element
|
-- Test 9: No callback when mouse outside element
|
||||||
function TestEventSystem:test_no_callback_when_clicking_outside_element()
|
function TestEventSystem:test_no_callback_when_clicking_outside_element()
|
||||||
local callbackCalled = false
|
local callbackCalled = false
|
||||||
|
|
||||||
local button = Gui.new({
|
local button = Gui.new({
|
||||||
x = 100,
|
x = 100,
|
||||||
y = 100,
|
y = 100,
|
||||||
@@ -301,22 +305,22 @@ function TestEventSystem:test_no_callback_when_clicking_outside_element()
|
|||||||
callbackCalled = true
|
callbackCalled = true
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
|
|
||||||
-- Click outside element
|
-- Click outside element
|
||||||
love.mouse.setPosition(50, 50)
|
love.mouse.setPosition(50, 50)
|
||||||
love.mouse.setDown(1, true)
|
love.mouse.setDown(1, true)
|
||||||
button:update(0.016)
|
button:update(0.016)
|
||||||
|
|
||||||
love.mouse.setDown(1, false)
|
love.mouse.setDown(1, false)
|
||||||
button:update(0.016)
|
button:update(0.016)
|
||||||
|
|
||||||
lu.assertFalse(callbackCalled, "Callback should not be called when clicking outside element")
|
lu.assertFalse(callbackCalled, "Callback should not be called when clicking outside element")
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Test 10: Multiple modifiers
|
-- Test 10: Multiple modifiers
|
||||||
function TestEventSystem:test_multiple_modifiers_detected()
|
function TestEventSystem:test_multiple_modifiers_detected()
|
||||||
local eventReceived = nil
|
local eventReceived = nil
|
||||||
|
|
||||||
local button = Gui.new({
|
local button = Gui.new({
|
||||||
x = 100,
|
x = 100,
|
||||||
y = 100,
|
y = 100,
|
||||||
@@ -328,20 +332,21 @@ function TestEventSystem:test_multiple_modifiers_detected()
|
|||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
|
|
||||||
-- Simulate shift + ctrl + click
|
-- Simulate shift + ctrl + click
|
||||||
love.keyboard.setDown("lshift", true)
|
love.keyboard.setDown("lshift", true)
|
||||||
love.keyboard.setDown("lctrl", true)
|
love.keyboard.setDown("lctrl", true)
|
||||||
love.mouse.setPosition(150, 150)
|
love.mouse.setPosition(150, 150)
|
||||||
love.mouse.setDown(1, true)
|
love.mouse.setDown(1, true)
|
||||||
button:update(0.016)
|
button:update(0.016)
|
||||||
|
|
||||||
love.mouse.setDown(1, false)
|
love.mouse.setDown(1, false)
|
||||||
button:update(0.016)
|
button:update(0.016)
|
||||||
|
|
||||||
lu.assertNotNil(eventReceived, "Should receive click event")
|
lu.assertNotNil(eventReceived, "Should receive click event")
|
||||||
lu.assertTrue(eventReceived.modifiers.shift, "Shift modifier should be detected")
|
lu.assertTrue(eventReceived.modifiers.shift, "Shift modifier should be detected")
|
||||||
lu.assertTrue(eventReceived.modifiers.ctrl, "Ctrl modifier should be detected")
|
lu.assertTrue(eventReceived.modifiers.ctrl, "Ctrl modifier should be detected")
|
||||||
end
|
end
|
||||||
|
|
||||||
return TestEventSystem
|
print("Running Event System Tests...")
|
||||||
|
lu.LuaUnit.run()
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
-- Test: Sibling Space Reservation in Flex and Grid Layouts
|
-- Test: Sibling Space Reservation in Flex and Grid Layouts
|
||||||
-- Purpose: Verify that absolutely positioned siblings with explicit positioning
|
-- Purpose: Verify that absolutely positioned siblings with explicit positioning
|
||||||
-- properly reserve space in flex and grid containers
|
-- properly reserve space in flex and grid containers
|
||||||
|
package.path = package.path .. ";?.lua"
|
||||||
|
|
||||||
local lu = require("testing.luaunit")
|
local lu = require("testing.luaunit")
|
||||||
local FlexLove = require("libs.FlexLove")
|
require("testing.loveStub") -- Required to mock LOVE functions
|
||||||
|
local FlexLove = require("FlexLove")
|
||||||
local Gui = FlexLove.GUI
|
local Gui = FlexLove.GUI
|
||||||
local Color = FlexLove.Color
|
local Color = FlexLove.Color
|
||||||
|
|
||||||
-- Mock love.graphics and love.window
|
|
||||||
_G.love = require("testing.loveStub")
|
|
||||||
|
|
||||||
TestSiblingSpaceReservation = {}
|
TestSiblingSpaceReservation = {}
|
||||||
|
|
||||||
function TestSiblingSpaceReservation:setUp()
|
function TestSiblingSpaceReservation:setUp()
|
||||||
@@ -106,7 +105,7 @@ function TestSiblingSpaceReservation:test_flex_horizontal_right_positioned_sibli
|
|||||||
-- The flex child (width 100) should fit within this space
|
-- The flex child (width 100) should fit within this space
|
||||||
-- Child should start at x = 0
|
-- Child should start at x = 0
|
||||||
lu.assertEquals(flexChild.x, 0, "Flex child should start at container left edge")
|
lu.assertEquals(flexChild.x, 0, "Flex child should start at container left edge")
|
||||||
|
|
||||||
-- The absolutely positioned sibling should be at the right edge
|
-- The absolutely positioned sibling should be at the right edge
|
||||||
-- x = container.x + container.width + padding.left - right - (width + padding)
|
-- x = container.x + container.width + padding.left - right - (width + padding)
|
||||||
-- = 0 + 1000 + 0 - 10 - 50 = 940
|
-- = 0 + 1000 + 0 - 10 - 50 = 940
|
||||||
@@ -208,7 +207,7 @@ function TestSiblingSpaceReservation:test_flex_horizontal_multiple_positioned_si
|
|||||||
-- Available space: 1000 - 45 - 45 = 910px
|
-- Available space: 1000 - 45 - 45 = 910px
|
||||||
-- First flex child should start at x = 0 + 0 + 45 = 45
|
-- First flex child should start at x = 0 + 0 + 45 = 45
|
||||||
lu.assertEquals(flexChild1.x, 45, "First flex child should start after left sibling")
|
lu.assertEquals(flexChild1.x, 45, "First flex child should start after left sibling")
|
||||||
|
|
||||||
-- Second flex child should start at x = 45 + 100 + gap = 145 (assuming gap=10)
|
-- Second flex child should start at x = 45 + 100 + gap = 145 (assuming gap=10)
|
||||||
lu.assertIsTrue(flexChild2.x >= 145, "Second flex child should be positioned after first")
|
lu.assertIsTrue(flexChild2.x >= 145, "Second flex child should be positioned after first")
|
||||||
end
|
end
|
||||||
@@ -434,4 +433,5 @@ function TestSiblingSpaceReservation:test_absolute_without_positioning_offsets_d
|
|||||||
lu.assertEquals(flexChild.x, 0, "Absolute children without positioning offsets should not reserve space")
|
lu.assertEquals(flexChild.x, 0, "Absolute children without positioning offsets should not reserve space")
|
||||||
end
|
end
|
||||||
|
|
||||||
return TestSiblingSpaceReservation
|
print("Running Sibling Space Reservation Tests...")
|
||||||
|
lu.LuaUnit.run()
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
package.path = package.path .. ";?.lua"
|
||||||
|
|
||||||
local lu = require("testing.luaunit")
|
local lu = require("testing.luaunit")
|
||||||
require("testing.loveStub")
|
require("testing.loveStub") -- Required to mock LOVE functions
|
||||||
local FlexLove = require("FlexLove")
|
local FlexLove = require("FlexLove")
|
||||||
|
|
||||||
TestFontFamilyInheritance = {}
|
TestFontFamilyInheritance = {}
|
||||||
@@ -219,4 +221,5 @@ function TestFontFamilyInheritance:testInheritanceDoesNotAffectSiblings()
|
|||||||
lu.assertNotEquals(child2.fontFamily, child1.fontFamily, "Siblings should have independent fontFamily values")
|
lu.assertNotEquals(child2.fontFamily, child1.fontFamily, "Siblings should have independent fontFamily values")
|
||||||
end
|
end
|
||||||
|
|
||||||
return TestFontFamilyInheritance
|
print("Running Font Family Inheritance Tests...")
|
||||||
|
lu.LuaUnit.run()
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
package.path = package.path .. ";?.lua"
|
||||||
|
|
||||||
local lu = require("testing.luaunit")
|
local lu = require("testing.luaunit")
|
||||||
require("testing.loveStub")
|
require("testing.loveStub") -- Required to mock LOVE functions
|
||||||
local FlexLove = require("FlexLove")
|
local FlexLove = require("FlexLove")
|
||||||
|
|
||||||
TestNegativeMargin = {}
|
TestNegativeMargin = {}
|
||||||
@@ -331,4 +333,5 @@ function TestNegativeMargin:testNegativeMarginInNestedElements()
|
|||||||
lu.assertEquals(child.margin.left, -10)
|
lu.assertEquals(child.margin.left, -10)
|
||||||
end
|
end
|
||||||
|
|
||||||
return TestNegativeMargin
|
print("Running Negative Margin Tests...")
|
||||||
|
lu.LuaUnit.run()
|
||||||
|
|||||||
Reference in New Issue
Block a user