diff --git a/FlexLove.lua b/FlexLove.lua index 946b1ab..27ca9fd 100644 --- a/FlexLove.lua +++ b/FlexLove.lua @@ -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|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|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, +} diff --git a/examples/ThemeColorAccessDemo.lua b/examples/ThemeColorAccessDemo.lua new file mode 100644 index 0000000..022f532 --- /dev/null +++ b/examples/ThemeColorAccessDemo.lua @@ -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 ===") diff --git a/examples/ThemeColorAccessSimple.lua b/examples/ThemeColorAccessSimple.lua new file mode 100644 index 0000000..6519cdd --- /dev/null +++ b/examples/ThemeColorAccessSimple.lua @@ -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 ===") diff --git a/testing/__tests__/03_flex_direction_horizontal_tests.lua b/testing/__tests__/03_flex_direction_horizontal_tests.lua index ce337ab..c78ded4 100644 --- a/testing/__tests__/03_flex_direction_horizontal_tests.lua +++ b/testing/__tests__/03_flex_direction_horizontal_tests.lua @@ -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 diff --git a/testing/__tests__/04_flex_direction_vertical_tests.lua b/testing/__tests__/04_flex_direction_vertical_tests.lua index 031b7c8..8343f5c 100644 --- a/testing/__tests__/04_flex_direction_vertical_tests.lua +++ b/testing/__tests__/04_flex_direction_vertical_tests.lua @@ -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 diff --git a/testing/__tests__/06_align_items_tests.lua b/testing/__tests__/06_align_items_tests.lua index e1269a6..e7455d6 100644 --- a/testing/__tests__/06_align_items_tests.lua +++ b/testing/__tests__/06_align_items_tests.lua @@ -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 diff --git a/testing/__tests__/07_flex_wrap_tests.lua b/testing/__tests__/07_flex_wrap_tests.lua index 4bd3e00..d0ff319 100644 --- a/testing/__tests__/07_flex_wrap_tests.lua +++ b/testing/__tests__/07_flex_wrap_tests.lua @@ -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 diff --git a/testing/__tests__/08_comprehensive_flex_tests.lua b/testing/__tests__/08_comprehensive_flex_tests.lua index 1727097..9cd08cf 100644 --- a/testing/__tests__/08_comprehensive_flex_tests.lua +++ b/testing/__tests__/08_comprehensive_flex_tests.lua @@ -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() diff --git a/testing/__tests__/11_auxiliary_functions_tests.lua b/testing/__tests__/11_auxiliary_functions_tests.lua index 0da8865..f78a48f 100644 --- a/testing/__tests__/11_auxiliary_functions_tests.lua +++ b/testing/__tests__/11_auxiliary_functions_tests.lua @@ -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 diff --git a/testing/__tests__/12_units_system_tests.lua b/testing/__tests__/12_units_system_tests.lua index aa283a2..8ce1927 100644 --- a/testing/__tests__/12_units_system_tests.lua +++ b/testing/__tests__/12_units_system_tests.lua @@ -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 diff --git a/testing/__tests__/16_event_system_tests.lua b/testing/__tests__/16_event_system_tests.lua index 5ad23fe..51bf2bb 100644 --- a/testing/__tests__/16_event_system_tests.lua +++ b/testing/__tests__/16_event_system_tests.lua @@ -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() diff --git a/testing/__tests__/19_negative_margin_tests.lua b/testing/__tests__/19_negative_margin_tests.lua index a35e978..f3d4987 100644 --- a/testing/__tests__/19_negative_margin_tests.lua +++ b/testing/__tests__/19_negative_margin_tests.lua @@ -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() diff --git a/testing/loveStub.lua b/testing/loveStub.lua index 215e7a4..37d0971 100644 --- a/testing/loveStub.lua +++ b/testing/loveStub.lua @@ -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