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)
-- Scale animation
local scaleUp = FlexLove.Animation.scale(0.5,
local scaleUp = FlexLove.Animation.scale(0.5,
{ width = 100, height = 50 },
{ width = 200, height = 100 }
)
@@ -287,18 +287,30 @@ end
function Color.fromHex(hexWithTag)
local hex = hexWithTag:gsub("#", "")
if #hex == 6 then
local r = tonumber("0x" .. hex:sub(1, 2)) or 0
local g = tonumber("0x" .. hex:sub(3, 4)) or 0
local b = tonumber("0x" .. hex:sub(5, 6)) or 0
local r = tonumber("0x" .. hex:sub(1, 2))
local g = tonumber("0x" .. hex:sub(3, 4))
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)
elseif #hex == 8 then
local r = tonumber("0x" .. hex:sub(1, 2)) or 0
local g = tonumber("0x" .. hex:sub(3, 4)) or 0
local b = tonumber("0x" .. hex:sub(5, 6)) or 0
local a = tonumber("0x" .. hex:sub(7, 8)) / 255
return Color.new(r, g, b, a)
local r = tonumber("0x" .. hex:sub(1, 2))
local g = tonumber("0x" .. hex:sub(3, 4))
local b = tonumber("0x" .. hex:sub(5, 6))
local a = tonumber("0x" .. hex:sub(7, 8))
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
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
@@ -399,7 +411,7 @@ local function safeLoadImage(imagePath)
local success, result = pcall(function()
return love.graphics.newImage(imagePath)
end)
if success then
return result, nil
else
@@ -419,27 +431,27 @@ 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
@@ -449,7 +461,7 @@ function Theme.new(definition)
if not valid then
error("[FlexLove] Invalid theme definition: " .. tostring(err))
end
local self = setmetatable({}, Theme)
self.name = definition.name
@@ -595,7 +607,7 @@ function Theme.getFont(fontName)
if not activeTheme then
return nil
end
return activeTheme.fonts and activeTheme.fonts[fontName]
end
@@ -606,7 +618,7 @@ function Theme.getColor(colorName)
if not activeTheme then
return nil
end
return activeTheme.colors and activeTheme.colors[colorName]
end
@@ -626,6 +638,43 @@ function Theme.getRegisteredThemes()
return themeNames
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
-- ====================
@@ -1021,7 +1070,7 @@ function Units.getViewport()
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
return love.graphics.getDimensions()
@@ -1105,7 +1154,7 @@ 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
@@ -1269,7 +1318,7 @@ local Gui = {
baseScale = nil,
scaleFactors = { x = 1.0, y = 1.0 },
defaultTheme = nil,
_cachedViewport = { width = 0, height = 0 }, -- Cached viewport dimensions
_cachedViewport = { width = 0, height = 0 }, -- Cached viewport dimensions
}
--- Initialize FlexLove with configuration
@@ -1348,7 +1397,7 @@ end
---@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
@@ -1356,30 +1405,30 @@ function Gui.getElementAtPosition(x, y)
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
@@ -1388,15 +1437,15 @@ 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
win:update(dt)
end
-- Clear active element for next frame
Gui._activeEventElement = nil
end
@@ -1474,15 +1523,23 @@ end
---@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,
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,
easeInCubic = function(t)
return t * t * t
end,
easeOutCubic = function(t)
local t1 = t - 1
return t1 * t1 * t1 + 1
@@ -1490,13 +1547,15 @@ local Easing = {
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,
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,
@@ -1536,15 +1595,15 @@ function Animation.new(props)
self.transform = props.transform
self.transition = props.transition
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
end
@@ -1552,7 +1611,7 @@ end
---@return boolean
function Animation:update(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
return true -- finished
else
@@ -1566,10 +1625,10 @@ function Animation:interpolate()
if not self._resultDirty then
return self._cachedResult
end
local t = math.min(self.elapsed / self.duration, 1)
t = self.easing(t) -- Apply easing function
local result = self._cachedResult -- Reuse existing table
t = self.easing(t) -- Apply easing function
local result = self._cachedResult -- Reuse existing table
-- Clear previous values
result.width = nil
@@ -1597,7 +1656,7 @@ function Animation:interpolate()
end
end
self._resultDirty = false -- Mark as clean
self._resultDirty = false -- Mark as clean
return result
end
@@ -1643,8 +1702,8 @@ function Animation.scale(duration, fromScale, toScale)
end
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
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
---@param size number
@@ -1671,10 +1730,10 @@ function FONT_CACHE.get(size, fontPath)
-- Load default font
FONT_CACHE[cacheKey] = love.graphics.newFont(size)
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)
@@ -2159,7 +2218,11 @@ function Element.new(props)
-- First, resolve padding using temporary dimensions
-- 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)
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
if self.autosizing.width then
@@ -2878,16 +2941,13 @@ function Element:layoutChildren()
elseif self.justifyContent == JustifyContent.SPACE_BETWEEN then
startPos = 0
if #line > 1 then
-- Gap already accounted for in freeSpace calculation
itemSpacing = self.gap + (freeSpace / (#line - 1))
end
elseif self.justifyContent == JustifyContent.SPACE_AROUND then
-- Gap already accounted for in freeSpace calculation
local spaceAroundEach = freeSpace / #line
startPos = spaceAroundEach / 2
itemSpacing = self.gap + spaceAroundEach
elseif self.justifyContent == JustifyContent.SPACE_EVENLY then
-- Gap already accounted for in freeSpace calculation
local spaceBetween = freeSpace / (#line + 1)
startPos = spaceBetween
itemSpacing = self.gap + spaceBetween
@@ -2923,9 +2983,12 @@ function Element:layoutChildren()
elseif effectiveAlign == AlignItems.FLEX_END then
child.y = self.y + self.padding.top + reservedCrossStart + currentCrossPos + lineHeight - childBorderBoxHeight
elseif effectiveAlign == AlignItems.STRETCH then
-- 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)
-- STRETCH: Only apply if height was not explicitly set
if child.autosizing and child.autosizing.height then
-- 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
end
@@ -2964,9 +3027,12 @@ function Element:layoutChildren()
elseif effectiveAlign == AlignItems.FLEX_END then
child.x = self.x + self.padding.left + reservedCrossStart + currentCrossPos + lineHeight - childBorderBoxWidth
elseif effectiveAlign == AlignItems.STRETCH then
-- 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)
-- STRETCH: Only apply if width was not explicitly set
if child.autosizing and child.autosizing.width then
-- 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
end
@@ -3023,7 +3089,7 @@ function Element:destroy()
-- Clear animation reference
self.animation = nil
-- Clear callback to prevent closure leaks
self.callback = nil
end
@@ -3034,7 +3100,7 @@ function Element:draw()
if self.opacity <= 0 then
return
end
-- Handle opacity during animation
local drawBackgroundColor = self.backgroundColor
if self.animation then
@@ -3048,7 +3114,7 @@ function Element:draw()
-- 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)
-- Apply opacity to all drawing operations
-- (x, y) represents border box, so draw background from (x, y)
@@ -3056,14 +3122,7 @@ function Element:draw()
local backgroundWithOpacity =
Color.new(drawBackgroundColor.r, drawBackgroundColor.g, drawBackgroundColor.b, drawBackgroundColor.a * self.opacity)
love.graphics.setColor(backgroundWithOpacity:toRGBA())
RoundedRect.draw(
"fill",
self.x,
self.y,
borderBoxWidth,
borderBoxHeight,
self.cornerRadius
)
RoundedRect.draw("fill", self.x, self.y, borderBoxWidth, borderBoxHeight, self.cornerRadius)
-- LAYER 2: Draw theme on top of backgroundColor (if theme exists)
if self.themeComponent then
@@ -3100,11 +3159,15 @@ function Element:draw()
if atlasToUse and component.regions then
-- Validate component has required structure
local hasAllRegions = component.regions.topLeft and component.regions.topCenter and
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
local hasAllRegions = component.regions.topLeft
and component.regions.topCenter
and 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
@@ -3659,7 +3722,7 @@ function Element:calculateTextWidth()
fontPath = themeToUse.fonts.default
end
end
local tempFont = FONT_CACHE.get(self.textSize, fontPath)
local width = tempFont:getWidth(self.text)
return width
@@ -3692,7 +3755,7 @@ function Element:calculateTextHeight()
fontPath = themeToUse.fonts.default
end
end
local tempFont = FONT_CACHE.get(self.textSize, fontPath)
local height = tempFont:getHeight()
return height
@@ -3710,19 +3773,34 @@ function Element:calculateAutoWidth()
return contentWidth
end
-- For HORIZONTAL flex: sum children widths + gaps
-- For VERTICAL flex: max of children widths
local isHorizontal = self.flexDirection == "horizontal"
local totalWidth = contentWidth
local maxWidth = contentWidth
local participatingChildren = 0
for _, child in ipairs(self.children) do
-- Skip explicitly absolute positioned children as they don't affect parent auto-sizing
if not child._explicitlyAbsolute then
-- BORDER-BOX MODEL: Use border-box width for auto-sizing calculations
local childBorderBoxWidth = child:getBorderBoxWidth()
totalWidth = totalWidth + childBorderBoxWidth
if isHorizontal then
totalWidth = totalWidth + childBorderBoxWidth
else
maxWidth = math.max(maxWidth, childBorderBoxWidth)
end
participatingChildren = participatingChildren + 1
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
--- Calculate auto height based on children
@@ -3732,19 +3810,34 @@ function Element:calculateAutoHeight()
return height
end
-- For VERTICAL flex: sum children heights + gaps
-- For HORIZONTAL flex: max of children heights
local isVertical = self.flexDirection == "vertical"
local totalHeight = height
local maxHeight = height
local participatingChildren = 0
for _, child in ipairs(self.children) do
-- Skip explicitly absolute positioned children as they don't affect parent auto-sizing
if not child._explicitlyAbsolute then
-- BORDER-BOX MODEL: Use border-box height for auto-sizing calculations
local childBorderBoxHeight = child:getBorderBoxHeight()
totalHeight = totalHeight + childBorderBoxHeight
if isVertical then
totalHeight = totalHeight + childBorderBoxHeight
else
maxHeight = math.max(maxHeight, childBorderBoxHeight)
end
participatingChildren = participatingChildren + 1
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
---@param newText string
@@ -3769,4 +3862,23 @@ Gui.new = Element.new
Gui.Element = Element
Gui.Animation = Animation
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(child2)
-- With align-items stretch in horizontal layout, children should stretch to parent height
luaunit.assertEquals(child1.height, parent.height)
luaunit.assertEquals(child2.height, parent.height)
-- With align-items stretch, children with explicit heights should keep them (CSS flexbox behavior)
luaunit.assertEquals(child1.height, 30)
luaunit.assertEquals(child2.height, 40)
end
-- Test 14: Horizontal layout with align-items center

View File

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

View File

@@ -189,11 +189,11 @@ function TestAlignItems:testHorizontalFlexAlignItemsStretch()
container:addChild(child1)
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(child2.y, 0)
luaunit.assertEquals(child1.height, 100)
luaunit.assertEquals(child2.height, 100)
luaunit.assertEquals(child1.height, 30) -- Keeps explicit height
luaunit.assertEquals(child2.height, 40) -- Keeps explicit height
end
-- Test 5: Vertical Flex with AlignItems.FLEX_START
@@ -357,11 +357,11 @@ function TestAlignItems:testVerticalFlexAlignItemsStretch()
container:addChild(child1)
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(child2.x, 0)
luaunit.assertEquals(child1.width, 200)
luaunit.assertEquals(child2.width, 200)
luaunit.assertEquals(child1.width, 50) -- Keeps explicit width
luaunit.assertEquals(child2.width, 80) -- Keeps explicit width
end
-- Test 9: Default AlignItems value (should be STRETCH)
@@ -386,9 +386,9 @@ function TestAlignItems:testDefaultAlignItems()
container:addChild(child)
-- Default should be STRETCH
-- Default should be STRETCH, but explicit heights are respected
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
-- Test 10: AlignItems with mixed child sizes

View File

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

View File

@@ -186,11 +186,11 @@ function TestComprehensiveFlex:testNestedFlexContainersComplexLayout()
-- Positions are absolute including parent container position
luaunit.assertEquals(inner2Positions[1].x, 20) -- parent x + 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].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
-- 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
luaunit.assertEquals(level2Positions[1].x, 20) -- 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].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
-- Positions are absolute including parent positions
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].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
-- 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 }
end
luaunit.assertEquals(middlePositions[1].x, 300) -- chart panel (300 + 20 padding)
luaunit.assertEquals(middlePositions[2].x, 1000) -- stats panel (300 + 680 + 20)
luaunit.assertEquals(middlePositions[1].x, 300) -- chart panel (280 sidebar + 20 padding)
luaunit.assertEquals(middlePositions[2].x, 1020) -- stats panel (280 + 20 + 680 + 40 gap with SPACE_BETWEEN)
-- Test chart legend wrapping
local chartPanel = middleContent.children[1]
@@ -1699,7 +1699,7 @@ function TestComprehensiveFlex:testComplexDashboardLayout()
end
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
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 }
end
luaunit.assertEquals(rightPositions[1].x, 860) -- alerts panel
luaunit.assertEquals(rightPositions[2].x, 1120) -- progress panel (860 + 240 + 20)
luaunit.assertEquals(rightPositions[1].x, 880) -- alerts panel (same as parent due to SPACE_BETWEEN)
luaunit.assertEquals(rightPositions[2].x, 1140) -- progress panel (880 + 240 + 20)
end
luaunit.LuaUnit.run()

View File

@@ -140,6 +140,7 @@ function TestAuxiliaryFunctions:testCalculateAutoWidthWithChildren()
local parent = Gui.new({
positioning = enums.Positioning.FLEX,
flexDirection = enums.FlexDirection.HORIZONTAL,
gap = 5, -- Add gap to test gap calculation
})
local child1 = Gui.new({
@@ -172,6 +173,7 @@ function TestAuxiliaryFunctions:testCalculateAutoHeightWithChildren()
local parent = Gui.new({
positioning = enums.Positioning.FLEX,
flexDirection = enums.FlexDirection.VERTICAL,
gap = 5, -- Add gap to test gap calculation
})
local child1 = Gui.new({
@@ -491,18 +493,21 @@ function TestAuxiliaryFunctions:testAnimationInterpolationAtBoundaries()
-- At start (elapsed = 0)
scaleAnim.elapsed = 0
scaleAnim._resultDirty = true -- Mark dirty after changing elapsed
local result = scaleAnim:interpolate()
luaunit.assertEquals(result.width, 100)
luaunit.assertEquals(result.height, 50)
-- At end (elapsed = duration)
scaleAnim.elapsed = 1.0
scaleAnim._resultDirty = true -- Mark dirty after changing elapsed
result = scaleAnim:interpolate()
luaunit.assertEquals(result.width, 200)
luaunit.assertEquals(result.height, 100)
-- Beyond end (elapsed > duration) - should clamp to end values
scaleAnim.elapsed = 1.5
scaleAnim._resultDirty = true -- Mark dirty after changing elapsed
result = scaleAnim:interpolate()
luaunit.assertEquals(result.width, 200)
luaunit.assertEquals(result.height, 100)
@@ -594,11 +599,11 @@ function TestAuxiliaryFunctions:testComplexColorManagementSystem()
end
-- 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
color_variations[color_name] = {}
-- Create opacity variations
local opacities = { 0.1, 0.25, 0.5, 0.75, 0.9 }
for _, opacity in ipairs(opacities) do
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
@@ -678,7 +683,13 @@ function TestAuxiliaryFunctions:testComplexColorManagementSystem()
ui_container:layoutChildren()
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
for _, variations in pairs(color_variations) do
@@ -902,6 +913,7 @@ function TestAuxiliaryFunctions:testAdvancedTextAndAutoSizingSystem()
table.insert(main_container.children, nested_container)
-- Create nested structure with auto-sizing children
local prev_container = nested_container
for level = 1, 3 do
local level_container = Gui.new({
width = 750 - (level * 50),
@@ -911,12 +923,9 @@ function TestAuxiliaryFunctions:testAdvancedTextAndAutoSizingSystem()
justifyContent = enums.JustifyContent.SPACE_AROUND,
gap = 5,
})
level_container.parent = level == 1 and nested_container
or main_container.children[#main_container.children].children[level - 1]
table.insert(
(level == 1 and nested_container or main_container.children[#main_container.children].children[level - 1]).children,
level_container
)
level_container.parent = prev_container
table.insert(prev_container.children, level_container)
prev_container = level_container
for item = 1, 4 do
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,
"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(
string.format(
@@ -1607,8 +1622,10 @@ function TestAuxiliaryFunctions:testAdvancedGUIManagementAndCleanup()
end
-- Test opacity management across hierarchy
for i, element_pair in pairs(managed_elements) do
if i % 2 == 0 then
for element_id, element_pair in pairs(managed_elements) do
-- 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)
luaunit.assertEquals(element_pair.opacity, 0.5, "Even elements should have 0.5 opacity")
end

View File

@@ -22,6 +22,10 @@ end
function TestUnitsSystem:tearDown()
Gui.destroy()
-- Restore original viewport size
love.graphics.getDimensions = function()
return 800, 600
end
end
-- ============================================
@@ -245,7 +249,7 @@ function TestUnitsSystem:testMarginUnits()
})
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.left, 24) -- 2% of 1200

View File

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

View File

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

View File

@@ -88,12 +88,59 @@ end
-- Mock mouse functions
love_helper.mouse = {}
-- Mock mouse state
local mockMouseX = 0
local mockMouseY = 0
local mockMouseButtons = {} -- Table to track button states
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
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
-- Mock touch functions