test alignment

This commit is contained in:
Michael Freno
2025-10-14 00:36:41 -04:00
parent 8d6cc58c13
commit 3cc416dcff
13 changed files with 663 additions and 131 deletions

View File

@@ -183,7 +183,7 @@ local fadeIn = FlexLove.Animation.fade(1.0, 0, 1)
fadeIn:apply(element) fadeIn:apply(element)
-- Scale animation -- Scale animation
local scaleUp = FlexLove.Animation.scale(0.5, local scaleUp = FlexLove.Animation.scale(0.5,
{ width = 100, height = 50 }, { width = 100, height = 50 },
{ width = 200, height = 100 } { width = 200, height = 100 }
) )
@@ -287,18 +287,30 @@ end
function Color.fromHex(hexWithTag) function Color.fromHex(hexWithTag)
local hex = hexWithTag:gsub("#", "") local hex = hexWithTag:gsub("#", "")
if #hex == 6 then if #hex == 6 then
local r = tonumber("0x" .. hex:sub(1, 2)) or 0 local r = tonumber("0x" .. hex:sub(1, 2))
local g = tonumber("0x" .. hex:sub(3, 4)) or 0 local g = tonumber("0x" .. hex:sub(3, 4))
local b = tonumber("0x" .. hex:sub(5, 6)) or 0 local b = tonumber("0x" .. hex:sub(5, 6))
if not r or not g or not b then
error(
formatError("Color", string.format("Invalid hex string format: '%s'. Contains invalid hex digits", hexWithTag))
)
end
return Color.new(r, g, b, 1) return Color.new(r, g, b, 1)
elseif #hex == 8 then elseif #hex == 8 then
local r = tonumber("0x" .. hex:sub(1, 2)) or 0 local r = tonumber("0x" .. hex:sub(1, 2))
local g = tonumber("0x" .. hex:sub(3, 4)) or 0 local g = tonumber("0x" .. hex:sub(3, 4))
local b = tonumber("0x" .. hex:sub(5, 6)) or 0 local b = tonumber("0x" .. hex:sub(5, 6))
local a = tonumber("0x" .. hex:sub(7, 8)) / 255 local a = tonumber("0x" .. hex:sub(7, 8))
return Color.new(r, g, b, a) if not r or not g or not b or not a then
error(
formatError("Color", string.format("Invalid hex string format: '%s'. Contains invalid hex digits", hexWithTag))
)
end
return Color.new(r, g, b, a / 255)
else else
error(formatError("Color", string.format("Invalid hex string format: '%s'. Expected #RRGGBB or #RRGGBBAA", hexWithTag))) error(
formatError("Color", string.format("Invalid hex string format: '%s'. Expected #RRGGBB or #RRGGBBAA", hexWithTag))
)
end end
end end
@@ -399,7 +411,7 @@ local function safeLoadImage(imagePath)
local success, result = pcall(function() local success, result = pcall(function()
return love.graphics.newImage(imagePath) return love.graphics.newImage(imagePath)
end) end)
if success then if success then
return result, nil return result, nil
else else
@@ -419,27 +431,27 @@ local function validateThemeDefinition(definition)
if not definition then if not definition then
return false, "Theme definition is nil" return false, "Theme definition is nil"
end end
if type(definition) ~= "table" then if type(definition) ~= "table" then
return false, "Theme definition must be a table" return false, "Theme definition must be a table"
end end
if not definition.name or type(definition.name) ~= "string" then if not definition.name or type(definition.name) ~= "string" then
return false, "Theme must have a 'name' field (string)" return false, "Theme must have a 'name' field (string)"
end end
if definition.components and type(definition.components) ~= "table" then if definition.components and type(definition.components) ~= "table" then
return false, "Theme 'components' must be a table" return false, "Theme 'components' must be a table"
end end
if definition.colors and type(definition.colors) ~= "table" then if definition.colors and type(definition.colors) ~= "table" then
return false, "Theme 'colors' must be a table" return false, "Theme 'colors' must be a table"
end end
if definition.fonts and type(definition.fonts) ~= "table" then if definition.fonts and type(definition.fonts) ~= "table" then
return false, "Theme 'fonts' must be a table" return false, "Theme 'fonts' must be a table"
end end
return true, nil return true, nil
end end
@@ -449,7 +461,7 @@ function Theme.new(definition)
if not valid then if not valid then
error("[FlexLove] Invalid theme definition: " .. tostring(err)) error("[FlexLove] Invalid theme definition: " .. tostring(err))
end end
local self = setmetatable({}, Theme) local self = setmetatable({}, Theme)
self.name = definition.name self.name = definition.name
@@ -595,7 +607,7 @@ function Theme.getFont(fontName)
if not activeTheme then if not activeTheme then
return nil return nil
end end
return activeTheme.fonts and activeTheme.fonts[fontName] return activeTheme.fonts and activeTheme.fonts[fontName]
end end
@@ -606,7 +618,7 @@ function Theme.getColor(colorName)
if not activeTheme then if not activeTheme then
return nil return nil
end end
return activeTheme.colors and activeTheme.colors[colorName] return activeTheme.colors and activeTheme.colors[colorName]
end end
@@ -626,6 +638,43 @@ function Theme.getRegisteredThemes()
return themeNames return themeNames
end end
--- Get all available color names from the active theme
---@return table<string>|nil -- Array of color names, or nil if no theme active
function Theme.getColorNames()
if not activeTheme or not activeTheme.colors then
return nil
end
local colorNames = {}
for name, _ in pairs(activeTheme.colors) do
table.insert(colorNames, name)
end
return colorNames
end
--- Get all colors from the active theme
---@return table<string, Color>|nil -- Table of all colors, or nil if no theme active
function Theme.getAllColors()
if not activeTheme then
return nil
end
return activeTheme.colors
end
--- Get a color with a fallback if not found
---@param colorName string -- Name of the color to retrieve
---@param fallback Color|nil -- Fallback color if not found (default: white)
---@return Color -- The color or fallback
function Theme.getColorOrDefault(colorName, fallback)
local color = Theme.getColor(colorName)
if color then
return color
end
return fallback or Color.new(1, 1, 1, 1)
end
-- ==================== -- ====================
-- Rounded Rectangle Helper -- Rounded Rectangle Helper
-- ==================== -- ====================
@@ -1021,7 +1070,7 @@ function Units.getViewport()
if Gui and Gui._cachedViewport and Gui._cachedViewport.width > 0 then if Gui and Gui._cachedViewport and Gui._cachedViewport.width > 0 then
return Gui._cachedViewport.width, Gui._cachedViewport.height return Gui._cachedViewport.width, Gui._cachedViewport.height
end end
-- Query viewport dimensions normally -- 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()
@@ -1105,7 +1154,7 @@ function Units.isValid(unitStr)
if type(unitStr) ~= "string" then if type(unitStr) ~= "string" then
return false return false
end end
local value, unit = Units.parse(unitStr) local value, unit = Units.parse(unitStr)
local validUnits = { px = true, ["%"] = true, vw = true, vh = true, ew = true, eh = true } local validUnits = { px = true, ["%"] = true, vw = true, vh = true, ew = true, eh = true }
return validUnits[unit] == true return validUnits[unit] == true
@@ -1269,7 +1318,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 _cachedViewport = { width = 0, height = 0 }, -- Cached viewport dimensions
} }
--- Initialize FlexLove with configuration --- Initialize FlexLove with configuration
@@ -1348,7 +1397,7 @@ end
---@return Element? -- Returns the topmost element or nil ---@return Element? -- Returns the topmost element or nil
function Gui.getElementAtPosition(x, y) function Gui.getElementAtPosition(x, y)
local candidates = {} local candidates = {}
-- Recursively collect all elements that contain the point -- Recursively collect all elements that contain the point
local function collectHits(element) local function collectHits(element)
-- Check if point is within element bounds -- Check if point is within element bounds
@@ -1356,30 +1405,30 @@ function Gui.getElementAtPosition(x, y)
local by = element.y local by = element.y
local bw = element._borderBoxWidth or (element.width + element.padding.left + element.padding.right) 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) 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 if x >= bx and x <= bx + bw and y >= by and y <= by + bh then
-- Only consider elements with callbacks (interactive elements) -- Only consider elements with callbacks (interactive elements)
if element.callback and not element.disabled then if element.callback and not element.disabled then
table.insert(candidates, element) table.insert(candidates, element)
end end
-- Check children -- Check children
for _, child in ipairs(element.children) do for _, child in ipairs(element.children) do
collectHits(child) collectHits(child)
end end
end end
end end
-- Collect hits from all top-level elements -- Collect hits from all top-level elements
for _, element in ipairs(Gui.topElements) do for _, element in ipairs(Gui.topElements) do
collectHits(element) collectHits(element)
end end
-- Sort by z-index (highest first) -- Sort by z-index (highest first)
table.sort(candidates, function(a, b) table.sort(candidates, function(a, b)
return a.z > b.z return a.z > b.z
end) end)
-- Return the topmost element (highest z-index) -- Return the topmost element (highest z-index)
return candidates[1] return candidates[1]
end end
@@ -1388,15 +1437,15 @@ function Gui.update(dt)
-- Reset event handling flags for new frame -- Reset event handling flags for new frame
local mx, my = love.mouse.getPosition() local mx, my = love.mouse.getPosition()
local topElement = Gui.getElementAtPosition(mx, my) local topElement = Gui.getElementAtPosition(mx, my)
-- Mark which element should handle events this frame -- Mark which element should handle events this frame
Gui._activeEventElement = topElement Gui._activeEventElement = topElement
-- Update all elements -- 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 -- Clear active element for next frame
Gui._activeEventElement = nil Gui._activeEventElement = nil
end end
@@ -1474,15 +1523,23 @@ end
---@field transition table? ---@field transition table?
--- Easing functions for animations --- Easing functions for animations
local Easing = { local Easing = {
linear = function(t) return t end, linear = function(t)
return t
easeInQuad = function(t) return t * t end, end,
easeOutQuad = function(t) return t * (2 - t) end,
easeInQuad = function(t)
return t * t
end,
easeOutQuad = function(t)
return t * (2 - t)
end,
easeInOutQuad = function(t) easeInOutQuad = function(t)
return t < 0.5 and 2 * t * t or -1 + (4 - 2 * t) * t return t < 0.5 and 2 * t * t or -1 + (4 - 2 * t) * t
end, end,
easeInCubic = function(t) return t * t * t end, easeInCubic = function(t)
return t * t * t
end,
easeOutCubic = function(t) easeOutCubic = function(t)
local t1 = t - 1 local t1 = t - 1
return t1 * t1 * t1 + 1 return t1 * t1 * t1 + 1
@@ -1490,13 +1547,15 @@ local Easing = {
easeInOutCubic = function(t) easeInOutCubic = function(t)
return t < 0.5 and 4 * t * t * t or (t - 1) * (2 * t - 2) * (2 * t - 2) + 1 return t < 0.5 and 4 * t * t * t or (t - 1) * (2 * t - 2) * (2 * t - 2) + 1
end, end,
easeInQuart = function(t) return t * t * t * t end, easeInQuart = function(t)
return t * t * t * t
end,
easeOutQuart = function(t) easeOutQuart = function(t)
local t1 = t - 1 local t1 = t - 1
return 1 - t1 * t1 * t1 * t1 return 1 - t1 * t1 * t1 * t1
end, end,
easeInExpo = function(t) easeInExpo = function(t)
return t == 0 and 0 or math.pow(2, 10 * (t - 1)) return t == 0 and 0 or math.pow(2, 10 * (t - 1))
end, end,
@@ -1536,15 +1595,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) -- Set easing function (default to linear)
local easingName = props.easing or "linear" local easingName = props.easing or "linear"
self.easing = Easing[easingName] or Easing.linear self.easing = Easing[easingName] or Easing.linear
-- Pre-allocate result table to avoid GC pressure -- Pre-allocate result table to avoid GC pressure
self._cachedResult = {} self._cachedResult = {}
self._resultDirty = true self._resultDirty = true
return self return self
end end
@@ -1552,7 +1611,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 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
@@ -1566,10 +1625,10 @@ function Animation:interpolate()
if not self._resultDirty then if not self._resultDirty then
return self._cachedResult return self._cachedResult
end end
local t = math.min(self.elapsed / self.duration, 1) local t = math.min(self.elapsed / self.duration, 1)
t = self.easing(t) -- Apply easing function t = self.easing(t) -- Apply easing function
local result = self._cachedResult -- Reuse existing table local result = self._cachedResult -- Reuse existing table
-- Clear previous values -- Clear previous values
result.width = nil result.width = nil
@@ -1597,7 +1656,7 @@ function Animation:interpolate()
end end
end end
self._resultDirty = false -- Mark as clean self._resultDirty = false -- Mark as clean
return result return result
end end
@@ -1643,8 +1702,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_MAX_SIZE = 50 -- Limit cache size to prevent unbounded growth
local FONT_CACHE_ORDER = {} -- Track access order for LRU eviction 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
@@ -1671,10 +1730,10 @@ 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 -- Add to access order for LRU tracking
table.insert(FONT_CACHE_ORDER, cacheKey) table.insert(FONT_CACHE_ORDER, cacheKey)
-- Evict oldest entry if cache is full (LRU eviction) -- Evict oldest entry if cache is full (LRU eviction)
if #FONT_CACHE_ORDER > FONT_CACHE_MAX_SIZE then if #FONT_CACHE_ORDER > FONT_CACHE_MAX_SIZE then
local oldestKey = table.remove(FONT_CACHE_ORDER, 1) local oldestKey = table.remove(FONT_CACHE_ORDER, 1)
@@ -2159,7 +2218,11 @@ function Element.new(props)
-- First, resolve padding using temporary dimensions -- First, resolve padding using temporary dimensions
-- For auto-sized elements, this is content width; for explicit sizing, this is border-box width -- For auto-sized elements, this is content width; for explicit sizing, this is border-box width
local tempPadding = Units.resolveSpacing(props.padding, self.width, self.height) local tempPadding = Units.resolveSpacing(props.padding, self.width, self.height)
self.margin = Units.resolveSpacing(props.margin, self.width, self.height)
-- Margin percentages are relative to parent's dimensions (CSS spec)
local parentWidth = self.parent and self.parent.width or viewportWidth
local parentHeight = self.parent and self.parent.height or viewportHeight
self.margin = Units.resolveSpacing(props.margin, parentWidth, parentHeight)
-- For auto-sized elements, add padding to get border-box dimensions -- For auto-sized elements, add padding to get border-box dimensions
if self.autosizing.width then if self.autosizing.width then
@@ -2878,16 +2941,13 @@ function Element:layoutChildren()
elseif self.justifyContent == JustifyContent.SPACE_BETWEEN then elseif self.justifyContent == JustifyContent.SPACE_BETWEEN then
startPos = 0 startPos = 0
if #line > 1 then if #line > 1 then
-- Gap already accounted for in freeSpace calculation
itemSpacing = self.gap + (freeSpace / (#line - 1)) itemSpacing = self.gap + (freeSpace / (#line - 1))
end end
elseif self.justifyContent == JustifyContent.SPACE_AROUND then elseif self.justifyContent == JustifyContent.SPACE_AROUND then
-- Gap already accounted for in freeSpace calculation
local spaceAroundEach = freeSpace / #line local spaceAroundEach = freeSpace / #line
startPos = spaceAroundEach / 2 startPos = spaceAroundEach / 2
itemSpacing = self.gap + spaceAroundEach itemSpacing = self.gap + spaceAroundEach
elseif self.justifyContent == JustifyContent.SPACE_EVENLY then elseif self.justifyContent == JustifyContent.SPACE_EVENLY then
-- Gap already accounted for in freeSpace calculation
local spaceBetween = freeSpace / (#line + 1) local spaceBetween = freeSpace / (#line + 1)
startPos = spaceBetween startPos = spaceBetween
itemSpacing = self.gap + spaceBetween itemSpacing = self.gap + spaceBetween
@@ -2923,9 +2983,12 @@ function Element:layoutChildren()
elseif effectiveAlign == AlignItems.FLEX_END then elseif effectiveAlign == AlignItems.FLEX_END then
child.y = self.y + self.padding.top + reservedCrossStart + currentCrossPos + lineHeight - childBorderBoxHeight child.y = self.y + self.padding.top + reservedCrossStart + currentCrossPos + lineHeight - childBorderBoxHeight
elseif effectiveAlign == AlignItems.STRETCH then elseif effectiveAlign == AlignItems.STRETCH then
-- STRETCH: Set border-box height to lineHeight, content area shrinks to fit -- STRETCH: Only apply if height was not explicitly set
child._borderBoxHeight = lineHeight if child.autosizing and child.autosizing.height then
child.height = math.max(0, lineHeight - child.padding.top - child.padding.bottom) -- STRETCH: Set border-box height to lineHeight, content area shrinks to fit
child._borderBoxHeight = lineHeight
child.height = math.max(0, lineHeight - child.padding.top - child.padding.bottom)
end
child.y = self.y + self.padding.top + reservedCrossStart + currentCrossPos child.y = self.y + self.padding.top + reservedCrossStart + currentCrossPos
end end
@@ -2964,9 +3027,12 @@ function Element:layoutChildren()
elseif effectiveAlign == AlignItems.FLEX_END then elseif effectiveAlign == AlignItems.FLEX_END then
child.x = self.x + self.padding.left + reservedCrossStart + currentCrossPos + lineHeight - childBorderBoxWidth child.x = self.x + self.padding.left + reservedCrossStart + currentCrossPos + lineHeight - childBorderBoxWidth
elseif effectiveAlign == AlignItems.STRETCH then elseif effectiveAlign == AlignItems.STRETCH then
-- STRETCH: Set border-box width to lineHeight, content area shrinks to fit -- STRETCH: Only apply if width was not explicitly set
child._borderBoxWidth = lineHeight if child.autosizing and child.autosizing.width then
child.width = math.max(0, lineHeight - child.padding.left - child.padding.right) -- STRETCH: Set border-box width to lineHeight, content area shrinks to fit
child._borderBoxWidth = lineHeight
child.width = math.max(0, lineHeight - child.padding.left - child.padding.right)
end
child.x = self.x + self.padding.left + reservedCrossStart + currentCrossPos child.x = self.x + self.padding.left + reservedCrossStart + currentCrossPos
end end
@@ -3023,7 +3089,7 @@ function Element:destroy()
-- Clear animation reference -- Clear animation reference
self.animation = nil self.animation = nil
-- Clear callback to prevent closure leaks -- Clear callback to prevent closure leaks
self.callback = nil self.callback = nil
end end
@@ -3034,7 +3100,7 @@ function Element:draw()
if self.opacity <= 0 then if self.opacity <= 0 then
return return
end 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
@@ -3048,7 +3114,7 @@ function Element:draw()
-- Cache border box dimensions for this draw call (optimization) -- Cache border box dimensions for this draw call (optimization)
local borderBoxWidth = self._borderBoxWidth or (self.width + self.padding.left + self.padding.right) 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) 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)
@@ -3056,14 +3122,7 @@ function Element:draw()
local backgroundWithOpacity = local backgroundWithOpacity =
Color.new(drawBackgroundColor.r, drawBackgroundColor.g, drawBackgroundColor.b, drawBackgroundColor.a * self.opacity) Color.new(drawBackgroundColor.r, drawBackgroundColor.g, drawBackgroundColor.b, drawBackgroundColor.a * self.opacity)
love.graphics.setColor(backgroundWithOpacity:toRGBA()) love.graphics.setColor(backgroundWithOpacity:toRGBA())
RoundedRect.draw( RoundedRect.draw("fill", self.x, self.y, borderBoxWidth, borderBoxHeight, self.cornerRadius)
"fill",
self.x,
self.y,
borderBoxWidth,
borderBoxHeight,
self.cornerRadius
)
-- LAYER 2: Draw theme on top of backgroundColor (if theme exists) -- LAYER 2: Draw theme on top of backgroundColor (if theme exists)
if self.themeComponent then if self.themeComponent then
@@ -3100,11 +3159,15 @@ function Element:draw()
if atlasToUse and component.regions then if atlasToUse and component.regions then
-- Validate component has required structure -- Validate component has required structure
local hasAllRegions = component.regions.topLeft and component.regions.topCenter and local hasAllRegions = component.regions.topLeft
component.regions.topRight and component.regions.middleLeft and and component.regions.topCenter
component.regions.middleCenter and component.regions.middleRight and and component.regions.topRight
component.regions.bottomLeft and component.regions.bottomCenter and and component.regions.middleLeft
component.regions.bottomRight and component.regions.middleCenter
and component.regions.middleRight
and component.regions.bottomLeft
and component.regions.bottomCenter
and component.regions.bottomRight
if hasAllRegions then if hasAllRegions then
NineSlice.draw(component, atlasToUse, self.x, self.y, self.width, self.height, self.padding, self.opacity) NineSlice.draw(component, atlasToUse, self.x, self.y, self.width, self.height, self.padding, self.opacity)
else else
@@ -3659,7 +3722,7 @@ function Element:calculateTextWidth()
fontPath = themeToUse.fonts.default fontPath = themeToUse.fonts.default
end end
end end
local tempFont = FONT_CACHE.get(self.textSize, fontPath) local tempFont = FONT_CACHE.get(self.textSize, fontPath)
local width = tempFont:getWidth(self.text) local width = tempFont:getWidth(self.text)
return width return width
@@ -3692,7 +3755,7 @@ function Element:calculateTextHeight()
fontPath = themeToUse.fonts.default fontPath = themeToUse.fonts.default
end end
end end
local tempFont = FONT_CACHE.get(self.textSize, fontPath) local tempFont = FONT_CACHE.get(self.textSize, fontPath)
local height = tempFont:getHeight() local height = tempFont:getHeight()
return height return height
@@ -3710,19 +3773,34 @@ function Element:calculateAutoWidth()
return contentWidth return contentWidth
end end
-- For HORIZONTAL flex: sum children widths + gaps
-- For VERTICAL flex: max of children widths
local isHorizontal = self.flexDirection == "horizontal"
local totalWidth = contentWidth local totalWidth = contentWidth
local maxWidth = contentWidth
local participatingChildren = 0 local participatingChildren = 0
for _, child in ipairs(self.children) do for _, child in ipairs(self.children) do
-- Skip explicitly absolute positioned children as they don't affect parent auto-sizing -- Skip explicitly absolute positioned children as they don't affect parent auto-sizing
if not child._explicitlyAbsolute then if not child._explicitlyAbsolute then
-- BORDER-BOX MODEL: Use border-box width for auto-sizing calculations -- BORDER-BOX MODEL: Use border-box width for auto-sizing calculations
local childBorderBoxWidth = child:getBorderBoxWidth() local childBorderBoxWidth = child:getBorderBoxWidth()
totalWidth = totalWidth + childBorderBoxWidth if isHorizontal then
totalWidth = totalWidth + childBorderBoxWidth
else
maxWidth = math.max(maxWidth, childBorderBoxWidth)
end
participatingChildren = participatingChildren + 1 participatingChildren = participatingChildren + 1
end end
end end
return totalWidth + (self.gap * participatingChildren) if isHorizontal then
-- Add gaps between children (n-1 gaps for n children)
local gapCount = math.max(0, participatingChildren - 1)
return totalWidth + (self.gap * gapCount)
else
return maxWidth
end
end end
--- Calculate auto height based on children --- Calculate auto height based on children
@@ -3732,19 +3810,34 @@ function Element:calculateAutoHeight()
return height return height
end end
-- For VERTICAL flex: sum children heights + gaps
-- For HORIZONTAL flex: max of children heights
local isVertical = self.flexDirection == "vertical"
local totalHeight = height local totalHeight = height
local maxHeight = height
local participatingChildren = 0 local participatingChildren = 0
for _, child in ipairs(self.children) do for _, child in ipairs(self.children) do
-- Skip explicitly absolute positioned children as they don't affect parent auto-sizing -- Skip explicitly absolute positioned children as they don't affect parent auto-sizing
if not child._explicitlyAbsolute then if not child._explicitlyAbsolute then
-- BORDER-BOX MODEL: Use border-box height for auto-sizing calculations -- BORDER-BOX MODEL: Use border-box height for auto-sizing calculations
local childBorderBoxHeight = child:getBorderBoxHeight() local childBorderBoxHeight = child:getBorderBoxHeight()
totalHeight = totalHeight + childBorderBoxHeight if isVertical then
totalHeight = totalHeight + childBorderBoxHeight
else
maxHeight = math.max(maxHeight, childBorderBoxHeight)
end
participatingChildren = participatingChildren + 1 participatingChildren = participatingChildren + 1
end end
end end
return totalHeight + (self.gap * participatingChildren) if isVertical then
-- Add gaps between children (n-1 gaps for n children)
local gapCount = math.max(0, participatingChildren - 1)
return totalHeight + (self.gap * gapCount)
else
return maxHeight
end
end end
---@param newText string ---@param newText string
@@ -3769,4 +3862,23 @@ Gui.new = Element.new
Gui.Element = Element Gui.Element = Element
Gui.Animation = Animation Gui.Animation = Animation
Gui.Theme = Theme Gui.Theme = Theme
return { GUI = Gui, Gui = Gui, Element = Element, Color = Color, Theme = Theme, Animation = Animation, enums = enums }
-- Export individual enums for convenience
return {
GUI = Gui,
Gui = Gui,
Element = Element,
Color = Color,
Theme = Theme,
Animation = Animation,
enums = enums,
-- Export individual enums at top level
Positioning = Positioning,
FlexDirection = FlexDirection,
JustifyContent = JustifyContent,
AlignItems = AlignItems,
AlignSelf = AlignSelf,
AlignContent = AlignContent,
FlexWrap = FlexWrap,
TextAlign = TextAlign,
}

View File

@@ -0,0 +1,170 @@
-- Theme Color Access Demo
-- Demonstrates various ways to access and use theme colors
package.path = package.path .. ";./?.lua;../?.lua"
local FlexLove = require("FlexLove")
local Theme = FlexLove.Theme
local Gui = FlexLove.Gui
local Color = FlexLove.Color
-- Initialize love stubs for testing
love = {
graphics = {
newFont = function(size) return { getHeight = function() return size end } end,
getFont = function() return { getHeight = function() return 12 end } end,
getWidth = function() return 1920 end,
getHeight = function() return 1080 end,
newImage = function() return {} end,
newQuad = function() return {} end,
},
}
print("=== Theme Color Access Demo ===\n")
-- Load and activate the space theme
Theme.load("space")
Theme.setActive("space")
print("1. Basic Color Access")
print("---------------------")
-- Method 1: Using Theme.getColor() (Recommended)
local primaryColor = Theme.getColor("primary")
local secondaryColor = Theme.getColor("secondary")
local textColor = Theme.getColor("text")
local textDarkColor = Theme.getColor("textDark")
print(string.format("Primary: r=%.2f, g=%.2f, b=%.2f", primaryColor.r, primaryColor.g, primaryColor.b))
print(string.format("Secondary: r=%.2f, g=%.2f, b=%.2f", secondaryColor.r, secondaryColor.g, secondaryColor.b))
print(string.format("Text: r=%.2f, g=%.2f, b=%.2f", textColor.r, textColor.g, textColor.b))
print(string.format("Text Dark: r=%.2f, g=%.2f, b=%.2f", textDarkColor.r, textDarkColor.g, textDarkColor.b))
print("\n2. Get All Available Colors")
print("----------------------------")
-- Method 2: Get all color names
local colorNames = Theme.getColorNames()
if colorNames then
print("Available colors in theme:")
for _, name in ipairs(colorNames) do
print(" - " .. name)
end
end
print("\n3. Get All Colors at Once")
print("-------------------------")
-- Method 3: Get all colors as a table
local allColors = Theme.getAllColors()
if allColors then
print("All colors:")
for name, color in pairs(allColors) do
print(string.format(" %s: r=%.2f, g=%.2f, b=%.2f, a=%.2f", name, color.r, color.g, color.b, color.a))
end
end
print("\n4. Safe Color Access with Fallback")
print("-----------------------------------")
-- Method 4: Get color with fallback
local accentColor = Theme.getColorOrDefault("accent", Color.new(1, 0, 0, 1)) -- Falls back to red
local primaryColor2 = Theme.getColorOrDefault("primary", Color.new(1, 0, 0, 1)) -- Uses theme color
print(string.format("Accent (fallback): r=%.2f, g=%.2f, b=%.2f", accentColor.r, accentColor.g, accentColor.b))
print(string.format("Primary (theme): r=%.2f, g=%.2f, b=%.2f", primaryColor2.r, primaryColor2.g, primaryColor2.b))
print("\n5. Using Colors in GUI Elements")
print("--------------------------------")
-- Create a container with theme colors
local container = Gui.new({
width = 400,
height = 300,
backgroundColor = Theme.getColor("secondary"),
positioning = FlexLove.enums.Positioning.FLEX,
flexDirection = FlexLove.enums.FlexDirection.VERTICAL,
gap = 10,
padding = { top = 20, right = 20, bottom = 20, left = 20 },
})
-- Create a button with primary color
local button = Gui.new({
parent = container,
width = 360,
height = 50,
backgroundColor = Theme.getColor("primary"),
textColor = Theme.getColor("text"),
text = "Click Me!",
textSize = 18,
})
-- Create a text label with dark text
local label = Gui.new({
parent = container,
width = 360,
height = 30,
backgroundColor = Theme.getColorOrDefault("background", Color.new(0.2, 0.2, 0.2, 1)),
textColor = Theme.getColor("textDark"),
text = "This is a label with dark text",
textSize = 14,
})
print("Created GUI elements with theme colors:")
print(string.format(" Container: %d children", #container.children))
print(string.format(" Button background: r=%.2f, g=%.2f, b=%.2f", button.backgroundColor.r, button.backgroundColor.g, button.backgroundColor.b))
print(string.format(" Label text color: r=%.2f, g=%.2f, b=%.2f", label.textColor.r, label.textColor.g, label.textColor.b))
print("\n6. Creating Color Variations")
print("-----------------------------")
-- Create variations of theme colors
local primaryDark = Color.new(
primaryColor.r * 0.7,
primaryColor.g * 0.7,
primaryColor.b * 0.7,
primaryColor.a
)
local primaryLight = Color.new(
math.min(1, primaryColor.r * 1.3),
math.min(1, primaryColor.g * 1.3),
math.min(1, primaryColor.b * 1.3),
primaryColor.a
)
local primaryTransparent = Color.new(
primaryColor.r,
primaryColor.g,
primaryColor.b,
0.5
)
print(string.format("Primary (original): r=%.2f, g=%.2f, b=%.2f, a=%.2f", primaryColor.r, primaryColor.g, primaryColor.b, primaryColor.a))
print(string.format("Primary (dark): r=%.2f, g=%.2f, b=%.2f, a=%.2f", primaryDark.r, primaryDark.g, primaryDark.b, primaryDark.a))
print(string.format("Primary (light): r=%.2f, g=%.2f, b=%.2f, a=%.2f", primaryLight.r, primaryLight.g, primaryLight.b, primaryLight.a))
print(string.format("Primary (50%% alpha): r=%.2f, g=%.2f, b=%.2f, a=%.2f", primaryTransparent.r, primaryTransparent.g, primaryTransparent.b, primaryTransparent.a))
print("\n7. Quick Reference")
print("------------------")
print([[
// Basic usage:
local color = Theme.getColor("primary")
// With fallback:
local color = Theme.getColorOrDefault("accent", Color.new(1, 0, 0, 1))
// Get all colors:
local colors = Theme.getAllColors()
// Get color names:
local names = Theme.getColorNames()
// Use in elements:
local button = Gui.new({
backgroundColor = Theme.getColor("primary"),
textColor = Theme.getColor("text"),
})
]])
print("\n=== Demo Complete ===")

View File

@@ -0,0 +1,181 @@
-- Simple Theme Color Access Demo
-- Shows how to access theme colors without creating GUI elements
package.path = package.path .. ";./?.lua;../?.lua"
local FlexLove = require("FlexLove")
local Theme = FlexLove.Theme
local Color = FlexLove.Color
-- Initialize minimal love stubs
love = {
graphics = {
newFont = function(size) return { getHeight = function() return size end } end,
newImage = function() return {} end,
newQuad = function() return {} end,
},
}
print("=== Theme Color Access - Simple Demo ===\n")
-- Load and activate the space theme
Theme.load("space")
Theme.setActive("space")
print("✓ Theme 'space' loaded and activated\n")
-- ============================================
-- METHOD 1: Basic Color Access (Recommended)
-- ============================================
print("METHOD 1: Theme.getColor(colorName)")
print("------------------------------------")
local primaryColor = Theme.getColor("primary")
local secondaryColor = Theme.getColor("secondary")
local textColor = Theme.getColor("text")
local textDarkColor = Theme.getColor("textDark")
print(string.format("primary = Color(r=%.2f, g=%.2f, b=%.2f, a=%.2f)",
primaryColor.r, primaryColor.g, primaryColor.b, primaryColor.a))
print(string.format("secondary = Color(r=%.2f, g=%.2f, b=%.2f, a=%.2f)",
secondaryColor.r, secondaryColor.g, secondaryColor.b, secondaryColor.a))
print(string.format("text = Color(r=%.2f, g=%.2f, b=%.2f, a=%.2f)",
textColor.r, textColor.g, textColor.b, textColor.a))
print(string.format("textDark = Color(r=%.2f, g=%.2f, b=%.2f, a=%.2f)",
textDarkColor.r, textDarkColor.g, textDarkColor.b, textDarkColor.a))
-- ============================================
-- METHOD 2: Get All Color Names
-- ============================================
print("\nMETHOD 2: Theme.getColorNames()")
print("--------------------------------")
local colorNames = Theme.getColorNames()
print("Available colors:")
for i, name in ipairs(colorNames) do
print(string.format(" %d. %s", i, name))
end
-- ============================================
-- METHOD 3: Get All Colors at Once
-- ============================================
print("\nMETHOD 3: Theme.getAllColors()")
print("-------------------------------")
local allColors = Theme.getAllColors()
print("All colors with values:")
for name, color in pairs(allColors) do
print(string.format(" %-10s = (%.2f, %.2f, %.2f, %.2f)",
name, color.r, color.g, color.b, color.a))
end
-- ============================================
-- METHOD 4: Safe Access with Fallback
-- ============================================
print("\nMETHOD 4: Theme.getColorOrDefault(colorName, fallback)")
print("-------------------------------------------------------")
-- Try to get a color that exists
local existingColor = Theme.getColorOrDefault("primary", Color.new(1, 0, 0, 1))
print(string.format("Existing color 'primary': (%.2f, %.2f, %.2f) ✓",
existingColor.r, existingColor.g, existingColor.b))
-- Try to get a color that doesn't exist (will use fallback)
local missingColor = Theme.getColorOrDefault("accent", Color.new(1, 0, 0, 1))
print(string.format("Missing color 'accent' (fallback): (%.2f, %.2f, %.2f) ✓",
missingColor.r, missingColor.g, missingColor.b))
-- ============================================
-- PRACTICAL EXAMPLES
-- ============================================
print("\n=== Practical Usage Examples ===\n")
print("Example 1: Using colors in element creation")
print("--------------------------------------------")
print([[
local button = Gui.new({
width = 200,
height = 50,
backgroundColor = Theme.getColor("primary"),
textColor = Theme.getColor("text"),
text = "Click Me!"
})
]])
print("\nExample 2: Creating color variations")
print("-------------------------------------")
print([[
local primary = Theme.getColor("primary")
-- Darker version (70% brightness)
local primaryDark = Color.new(
primary.r * 0.7,
primary.g * 0.7,
primary.b * 0.7,
primary.a
)
-- Lighter version (130% brightness)
local primaryLight = Color.new(
math.min(1, primary.r * 1.3),
math.min(1, primary.g * 1.3),
math.min(1, primary.b * 1.3),
primary.a
)
-- Semi-transparent version
local primaryTransparent = Color.new(
primary.r,
primary.g,
primary.b,
0.5 -- 50% opacity
)
]])
print("\nExample 3: Safe color access")
print("-----------------------------")
print([[
-- With fallback to white if color doesn't exist
local bgColor = Theme.getColorOrDefault("background", Color.new(1, 1, 1, 1))
-- With fallback to theme's secondary color
local borderColor = Theme.getColorOrDefault(
"border",
Theme.getColor("secondary")
)
]])
print("\nExample 4: Dynamic color selection")
print("-----------------------------------")
print([[
-- Get all available colors
local colors = Theme.getAllColors()
-- Pick a random color
local colorNames = {}
for name in pairs(colors) do
table.insert(colorNames, name)
end
local randomColorName = colorNames[math.random(#colorNames)]
local randomColor = colors[randomColorName]
]])
print("\n=== Quick Reference ===\n")
print("Theme.getColor(name) -- Get a specific color")
print("Theme.getColorOrDefault(n, fb) -- Get color with fallback")
print("Theme.getAllColors() -- Get all colors as table")
print("Theme.getColorNames() -- Get array of color names")
print("Theme.hasActive() -- Check if theme is active")
print("Theme.getActive() -- Get active theme object")
print("\n=== Available Colors in 'space' Theme ===\n")
for i, name in ipairs(colorNames) do
local color = allColors[name]
print(string.format("%-10s RGB(%.0f, %.0f, %.0f)",
name,
color.r * 255,
color.g * 255,
color.b * 255))
end
print("\n=== Demo Complete ===")

View File

@@ -442,9 +442,9 @@ function TestHorizontalFlexDirection:testHorizontalLayoutAlignItemsStretch()
parent:addChild(child1) parent:addChild(child1)
parent:addChild(child2) parent:addChild(child2)
-- With align-items stretch in horizontal layout, children should stretch to parent height -- With align-items stretch, children with explicit heights should keep them (CSS flexbox behavior)
luaunit.assertEquals(child1.height, parent.height) luaunit.assertEquals(child1.height, 30)
luaunit.assertEquals(child2.height, parent.height) luaunit.assertEquals(child2.height, 40)
end end
-- Test 14: Horizontal layout with align-items center -- Test 14: Horizontal layout with align-items center

View File

@@ -402,9 +402,9 @@ function TestVerticalFlexDirection:testVerticalLayoutAlignItemsStretch()
parent:addChild(child1) parent:addChild(child1)
parent:addChild(child2) parent:addChild(child2)
-- Children should be stretched to fill parent width -- Children with explicit widths should keep them (CSS flexbox behavior)
luaunit.assertEquals(child1.width, parent.width) luaunit.assertEquals(child1.width, 80)
luaunit.assertEquals(child2.width, parent.width) luaunit.assertEquals(child2.width, 60)
end end
-- Test 13: Vertical layout with space-between -- Test 13: Vertical layout with space-between

View File

@@ -189,11 +189,11 @@ function TestAlignItems:testHorizontalFlexAlignItemsStretch()
container:addChild(child1) container:addChild(child1)
container:addChild(child2) container:addChild(child2)
-- Children should be stretched to fill container height -- Children with explicit heights should NOT be stretched (CSS flexbox behavior)
luaunit.assertEquals(child1.y, 0) luaunit.assertEquals(child1.y, 0)
luaunit.assertEquals(child2.y, 0) luaunit.assertEquals(child2.y, 0)
luaunit.assertEquals(child1.height, 100) luaunit.assertEquals(child1.height, 30) -- Keeps explicit height
luaunit.assertEquals(child2.height, 100) luaunit.assertEquals(child2.height, 40) -- Keeps explicit height
end end
-- Test 5: Vertical Flex with AlignItems.FLEX_START -- Test 5: Vertical Flex with AlignItems.FLEX_START
@@ -357,11 +357,11 @@ function TestAlignItems:testVerticalFlexAlignItemsStretch()
container:addChild(child1) container:addChild(child1)
container:addChild(child2) container:addChild(child2)
-- Children should be stretched to fill container width -- Children with explicit widths should NOT be stretched (CSS flexbox behavior)
luaunit.assertEquals(child1.x, 0) luaunit.assertEquals(child1.x, 0)
luaunit.assertEquals(child2.x, 0) luaunit.assertEquals(child2.x, 0)
luaunit.assertEquals(child1.width, 200) luaunit.assertEquals(child1.width, 50) -- Keeps explicit width
luaunit.assertEquals(child2.width, 200) luaunit.assertEquals(child2.width, 80) -- Keeps explicit width
end end
-- Test 9: Default AlignItems value (should be STRETCH) -- Test 9: Default AlignItems value (should be STRETCH)
@@ -386,9 +386,9 @@ function TestAlignItems:testDefaultAlignItems()
container:addChild(child) container:addChild(child)
-- Default should be STRETCH -- Default should be STRETCH, but explicit heights are respected
luaunit.assertEquals(container.alignItems, AlignItems.STRETCH) luaunit.assertEquals(container.alignItems, AlignItems.STRETCH)
luaunit.assertEquals(child.height, 100) -- Should be stretched luaunit.assertEquals(child.height, 30) -- Keeps explicit height (CSS flexbox behavior)
end end
-- Test 10: AlignItems with mixed child sizes -- Test 10: AlignItems with mixed child sizes

View File

@@ -280,12 +280,12 @@ function TestFlexWrap07_WrapWithStretchAlignItems()
local positions = layoutAndGetPositions(container) local positions = layoutAndGetPositions(container)
-- All children in first line should stretch to tallest (35) -- Children with explicit heights should keep them (CSS flexbox behavior)
luaunit.assertEquals(positions[1].height, 35) -- child1 stretched luaunit.assertEquals(positions[1].height, 20) -- child1 keeps explicit height
luaunit.assertEquals(positions[2].height, 35) -- child2 keeps height luaunit.assertEquals(positions[2].height, 35) -- child2 keeps explicit height
-- Child in second line should keep its height (no other children to stretch to) -- Child in second line should keep its height
luaunit.assertEquals(positions[3].height, 25) -- child3 original height luaunit.assertEquals(positions[3].height, 25) -- child3 keeps explicit height
-- Verify positions -- Verify positions
luaunit.assertEquals(positions[1].y, 0) -- First line luaunit.assertEquals(positions[1].y, 0) -- First line

View File

@@ -186,11 +186,11 @@ function TestComprehensiveFlex:testNestedFlexContainersComplexLayout()
-- Positions are absolute including parent container position -- Positions are absolute including parent container position
luaunit.assertEquals(inner2Positions[1].x, 20) -- parent x + 0 luaunit.assertEquals(inner2Positions[1].x, 20) -- parent x + 0
luaunit.assertEquals(inner2Positions[1].y, 95) -- parent y + 0 luaunit.assertEquals(inner2Positions[1].y, 95) -- parent y + 0
luaunit.assertEquals(inner2Positions[1].height, 50) -- stretched to full container height luaunit.assertEquals(inner2Positions[1].height, 25) -- explicit height, not stretched (CSS spec compliance)
luaunit.assertEquals(inner2Positions[2].x, 60) -- parent x + 40 luaunit.assertEquals(inner2Positions[2].x, 60) -- parent x + 40
luaunit.assertEquals(inner2Positions[2].y, 95) -- parent y + 0 luaunit.assertEquals(inner2Positions[2].y, 95) -- parent y + 0
luaunit.assertEquals(inner2Positions[2].height, 50) -- stretched to full container height luaunit.assertEquals(inner2Positions[2].height, 25) -- explicit height, not stretched (CSS spec compliance)
end end
-- Test 3: All flex properties combined with absolute positioning -- Test 3: All flex properties combined with absolute positioning
@@ -390,19 +390,19 @@ function TestComprehensiveFlex:testDeeplyNestedFlexContainers()
-- These positions are relative to level 1 container position -- These positions are relative to level 1 container position
luaunit.assertEquals(level2Positions[1].x, 20) -- positioned by level 1 luaunit.assertEquals(level2Positions[1].x, 20) -- positioned by level 1
luaunit.assertEquals(level2Positions[1].y, 25) -- positioned by level 1 luaunit.assertEquals(level2Positions[1].y, 25) -- positioned by level 1
luaunit.assertEquals(level2Positions[1].height, 100) -- stretched to full cross-axis height luaunit.assertEquals(level2Positions[1].height, 80) -- explicit height, not stretched (CSS spec compliance)
luaunit.assertEquals(level2Positions[2].x, 110) -- positioned by level 1 + space-between luaunit.assertEquals(level2Positions[2].x, 110) -- positioned by level 1 + space-between
luaunit.assertEquals(level2Positions[2].y, 25) -- positioned by level 1 luaunit.assertEquals(level2Positions[2].y, 25) -- positioned by level 1
luaunit.assertEquals(level2Positions[2].height, 100) -- stretched to full cross-axis height luaunit.assertEquals(level2Positions[2].height, 80) -- explicit height, not stretched (CSS spec compliance)
-- Level 3a: flex-end justification, center alignment -- Level 3a: flex-end justification, center alignment
-- Positions are absolute including parent positions -- Positions are absolute including parent positions
luaunit.assertEquals(level3aPositions[1].x, 40) -- absolute position luaunit.assertEquals(level3aPositions[1].x, 40) -- absolute position
luaunit.assertEquals(level3aPositions[1].y, 90) -- flex-end: positioned at bottom of stretched container luaunit.assertEquals(level3aPositions[1].y, 70) -- flex-end: 25 (level2.y) + 45 (80 - 35 total children)
luaunit.assertEquals(level3aPositions[2].x, 42.5) -- absolute position luaunit.assertEquals(level3aPositions[2].x, 42.5) -- absolute position
luaunit.assertEquals(level3aPositions[2].y, 110) -- second item: 90 + 20 = 110 luaunit.assertEquals(level3aPositions[2].y, 90) -- second item: 70 + 20 = 90
-- Level 3b: flex-start justification, flex-end alignment -- Level 3b: flex-start justification, flex-end alignment
-- Positions are absolute including parent positions -- Positions are absolute including parent positions
@@ -1656,8 +1656,8 @@ function TestComprehensiveFlex:testComplexDashboardLayout()
middlePositions[i] = { x = child.x, y = child.y, width = child.width, height = child.height } middlePositions[i] = { x = child.x, y = child.y, width = child.width, height = child.height }
end end
luaunit.assertEquals(middlePositions[1].x, 300) -- chart panel (300 + 20 padding) luaunit.assertEquals(middlePositions[1].x, 300) -- chart panel (280 sidebar + 20 padding)
luaunit.assertEquals(middlePositions[2].x, 1000) -- stats panel (300 + 680 + 20) luaunit.assertEquals(middlePositions[2].x, 1020) -- stats panel (280 + 20 + 680 + 40 gap with SPACE_BETWEEN)
-- Test chart legend wrapping -- Test chart legend wrapping
local chartPanel = middleContent.children[1] local chartPanel = middleContent.children[1]
@@ -1699,7 +1699,7 @@ function TestComprehensiveFlex:testComplexDashboardLayout()
end end
luaunit.assertEquals(bottomPositions[1].x, 300) -- table panel luaunit.assertEquals(bottomPositions[1].x, 300) -- table panel
luaunit.assertEquals(bottomPositions[2].x, 860) -- right panels (300 + 540 + 20) luaunit.assertEquals(bottomPositions[2].x, 880) -- right panels (280 + 20 + 540 + 40 gap with SPACE_BETWEEN)
-- Test right panels layout -- Test right panels layout
local rightPanels = bottomContent.children[2] local rightPanels = bottomContent.children[2]
@@ -1710,8 +1710,8 @@ function TestComprehensiveFlex:testComplexDashboardLayout()
rightPositions[i] = { x = child.x, y = child.y, width = child.width, height = child.height } rightPositions[i] = { x = child.x, y = child.y, width = child.width, height = child.height }
end end
luaunit.assertEquals(rightPositions[1].x, 860) -- alerts panel luaunit.assertEquals(rightPositions[1].x, 880) -- alerts panel (same as parent due to SPACE_BETWEEN)
luaunit.assertEquals(rightPositions[2].x, 1120) -- progress panel (860 + 240 + 20) luaunit.assertEquals(rightPositions[2].x, 1140) -- progress panel (880 + 240 + 20)
end end
luaunit.LuaUnit.run() luaunit.LuaUnit.run()

View File

@@ -140,6 +140,7 @@ function TestAuxiliaryFunctions:testCalculateAutoWidthWithChildren()
local parent = Gui.new({ local parent = Gui.new({
positioning = enums.Positioning.FLEX, positioning = enums.Positioning.FLEX,
flexDirection = enums.FlexDirection.HORIZONTAL, flexDirection = enums.FlexDirection.HORIZONTAL,
gap = 5, -- Add gap to test gap calculation
}) })
local child1 = Gui.new({ local child1 = Gui.new({
@@ -172,6 +173,7 @@ function TestAuxiliaryFunctions:testCalculateAutoHeightWithChildren()
local parent = Gui.new({ local parent = Gui.new({
positioning = enums.Positioning.FLEX, positioning = enums.Positioning.FLEX,
flexDirection = enums.FlexDirection.VERTICAL, flexDirection = enums.FlexDirection.VERTICAL,
gap = 5, -- Add gap to test gap calculation
}) })
local child1 = Gui.new({ local child1 = Gui.new({
@@ -491,18 +493,21 @@ function TestAuxiliaryFunctions:testAnimationInterpolationAtBoundaries()
-- At start (elapsed = 0) -- At start (elapsed = 0)
scaleAnim.elapsed = 0 scaleAnim.elapsed = 0
scaleAnim._resultDirty = true -- Mark dirty after changing elapsed
local result = scaleAnim:interpolate() local result = scaleAnim:interpolate()
luaunit.assertEquals(result.width, 100) luaunit.assertEquals(result.width, 100)
luaunit.assertEquals(result.height, 50) luaunit.assertEquals(result.height, 50)
-- At end (elapsed = duration) -- At end (elapsed = duration)
scaleAnim.elapsed = 1.0 scaleAnim.elapsed = 1.0
scaleAnim._resultDirty = true -- Mark dirty after changing elapsed
result = scaleAnim:interpolate() result = scaleAnim:interpolate()
luaunit.assertEquals(result.width, 200) luaunit.assertEquals(result.width, 200)
luaunit.assertEquals(result.height, 100) luaunit.assertEquals(result.height, 100)
-- Beyond end (elapsed > duration) - should clamp to end values -- Beyond end (elapsed > duration) - should clamp to end values
scaleAnim.elapsed = 1.5 scaleAnim.elapsed = 1.5
scaleAnim._resultDirty = true -- Mark dirty after changing elapsed
result = scaleAnim:interpolate() result = scaleAnim:interpolate()
luaunit.assertEquals(result.width, 200) luaunit.assertEquals(result.width, 200)
luaunit.assertEquals(result.height, 100) luaunit.assertEquals(result.height, 100)
@@ -594,11 +599,11 @@ function TestAuxiliaryFunctions:testComplexColorManagementSystem()
end end
-- Test color variations (opacity, brightness adjustments) -- Test color variations (opacity, brightness adjustments)
local opacities = { 0.1, 0.25, 0.5, 0.75, 0.9 }
for color_name, color_set in pairs(theme_colors) do for color_name, color_set in pairs(theme_colors) do
color_variations[color_name] = {} color_variations[color_name] = {}
-- Create opacity variations -- Create opacity variations
local opacities = { 0.1, 0.25, 0.5, 0.75, 0.9 }
for _, opacity in ipairs(opacities) do for _, opacity in ipairs(opacities) do
local variant_color = Color.new(color_set.manual.r, color_set.manual.g, color_set.manual.b, opacity) local variant_color = Color.new(color_set.manual.r, color_set.manual.g, color_set.manual.b, opacity)
color_variations[color_name]["alpha_" .. tostring(opacity)] = variant_color color_variations[color_name]["alpha_" .. tostring(opacity)] = variant_color
@@ -678,7 +683,13 @@ function TestAuxiliaryFunctions:testComplexColorManagementSystem()
ui_container:layoutChildren() ui_container:layoutChildren()
luaunit.assertEquals(#ui_container.children, 5, "Should have 5 themed components") luaunit.assertEquals(#ui_container.children, 5, "Should have 5 themed components")
luaunit.assertEquals(#theme_colors, 5, "Should have 5 base theme colors")
-- Count theme_colors (it's a table with string keys, not an array)
local theme_color_count = 0
for _ in pairs(theme_colors) do
theme_color_count = theme_color_count + 1
end
luaunit.assertEquals(theme_color_count, 5, "Should have 5 base theme colors")
local total_variations = 0 local total_variations = 0
for _, variations in pairs(color_variations) do for _, variations in pairs(color_variations) do
@@ -902,6 +913,7 @@ function TestAuxiliaryFunctions:testAdvancedTextAndAutoSizingSystem()
table.insert(main_container.children, nested_container) table.insert(main_container.children, nested_container)
-- Create nested structure with auto-sizing children -- Create nested structure with auto-sizing children
local prev_container = nested_container
for level = 1, 3 do for level = 1, 3 do
local level_container = Gui.new({ local level_container = Gui.new({
width = 750 - (level * 50), width = 750 - (level * 50),
@@ -911,12 +923,9 @@ function TestAuxiliaryFunctions:testAdvancedTextAndAutoSizingSystem()
justifyContent = enums.JustifyContent.SPACE_AROUND, justifyContent = enums.JustifyContent.SPACE_AROUND,
gap = 5, gap = 5,
}) })
level_container.parent = level == 1 and nested_container level_container.parent = prev_container
or main_container.children[#main_container.children].children[level - 1] table.insert(prev_container.children, level_container)
table.insert( prev_container = level_container
(level == 1 and nested_container or main_container.children[#main_container.children].children[level - 1]).children,
level_container
)
for item = 1, 4 do for item = 1, 4 do
local item_text = string.format("L%d-Item%d: %s", level, item, string.rep("Text ", level)) local item_text = string.format("L%d-Item%d: %s", level, item, string.rep("Text ", level))
@@ -943,7 +952,13 @@ function TestAuxiliaryFunctions:testAdvancedTextAndAutoSizingSystem()
#text_scenarios + 1, #text_scenarios + 1,
"Should have scenario containers plus nested container" "Should have scenario containers plus nested container"
) )
luaunit.assertTrue(#content_manager.text_metrics >= #text_scenarios, "Should have metrics for all scenarios")
-- Count text_metrics (it's a table with string keys, not an array)
local metrics_count = 0
for _ in pairs(content_manager.text_metrics) do
metrics_count = metrics_count + 1
end
luaunit.assertTrue(metrics_count >= #text_scenarios, "Should have metrics for all scenarios")
print( print(
string.format( string.format(
@@ -1607,8 +1622,10 @@ function TestAuxiliaryFunctions:testAdvancedGUIManagementAndCleanup()
end end
-- Test opacity management across hierarchy -- Test opacity management across hierarchy
for i, element_pair in pairs(managed_elements) do for element_id, element_pair in pairs(managed_elements) do
if i % 2 == 0 then -- Extract number from "element_N" key
local num = tonumber(element_id:match("%d+"))
if num and num % 2 == 0 then
element_pair:updateOpacity(0.5) element_pair:updateOpacity(0.5)
luaunit.assertEquals(element_pair.opacity, 0.5, "Even elements should have 0.5 opacity") luaunit.assertEquals(element_pair.opacity, 0.5, "Even elements should have 0.5 opacity")
end end

View File

@@ -22,6 +22,10 @@ end
function TestUnitsSystem:tearDown() function TestUnitsSystem:tearDown()
Gui.destroy() Gui.destroy()
-- Restore original viewport size
love.graphics.getDimensions = function()
return 800, 600
end
end end
-- ============================================ -- ============================================
@@ -245,7 +249,7 @@ function TestUnitsSystem:testMarginUnits()
}) })
luaunit.assertEquals(container.margin.top, 8) -- 8px luaunit.assertEquals(container.margin.top, 8) -- 8px
luaunit.assertEquals(container.margin.right, 12) -- 3% of 400 luaunit.assertEquals(container.margin.right, 36) -- 3% of viewport width (1200) - CSS spec: % margins relative to containing block
luaunit.assertEquals(container.margin.bottom, 8) -- 1% of 800 luaunit.assertEquals(container.margin.bottom, 8) -- 1% of 800
luaunit.assertEquals(container.margin.left, 24) -- 2% of 1200 luaunit.assertEquals(container.margin.left, 24) -- 2% of 1200

View File

@@ -16,6 +16,7 @@ function TestEventSystem:setUp()
-- Initialize GUI before each test -- Initialize GUI before each test
Gui.init({ baseScale = { width = 1920, height = 1080 } }) Gui.init({ baseScale = { width = 1920, height = 1080 } })
love.window.setMode(1920, 1080) love.window.setMode(1920, 1080)
Gui.resize(1920, 1080) -- Recalculate scale factors after setMode
end end
function TestEventSystem:tearDown() function TestEventSystem:tearDown()

View File

@@ -8,7 +8,7 @@ TestNegativeMargin = {}
function TestNegativeMargin:setUp() function TestNegativeMargin:setUp()
FlexLove.Gui.destroy() FlexLove.Gui.destroy()
FlexLove.Gui.init({ baseScale = { width = 1920, height = 1080 } }) -- Don't call init to use 1:1 scaling (like other tests)
end end
function TestNegativeMargin:tearDown() function TestNegativeMargin:tearDown()

View File

@@ -88,12 +88,59 @@ end
-- Mock mouse functions -- Mock mouse functions
love_helper.mouse = {} love_helper.mouse = {}
-- Mock mouse state
local mockMouseX = 0
local mockMouseY = 0
local mockMouseButtons = {} -- Table to track button states
function love_helper.mouse.getPosition() function love_helper.mouse.getPosition()
return 0, 0 -- Default position return mockMouseX, mockMouseY
end
function love_helper.mouse.setPosition(x, y)
mockMouseX = x
mockMouseY = y
end end
function love_helper.mouse.isDown(button) function love_helper.mouse.isDown(button)
return false -- Default not pressed return mockMouseButtons[button] or false
end
function love_helper.mouse.setDown(button, isDown)
mockMouseButtons[button] = isDown
end
-- Mock timer functions
love_helper.timer = {}
-- Mock time state
local mockTime = 0
function love_helper.timer.getTime()
return mockTime
end
function love_helper.timer.setTime(time)
mockTime = time
end
function love_helper.timer.step(dt)
mockTime = mockTime + dt
end
-- Mock keyboard functions
love_helper.keyboard = {}
-- Mock keyboard state
local mockKeyboardKeys = {} -- Table to track key states
function love_helper.keyboard.isDown(key)
return mockKeyboardKeys[key] or false
end
function love_helper.keyboard.setDown(key, isDown)
mockKeyboardKeys[key] = isDown
end end
-- Mock touch functions -- Mock touch functions