units
This commit is contained in:
547
FlexLove.lua
547
FlexLove.lua
@@ -8,9 +8,9 @@ local Color = {}
|
|||||||
Color.__index = Color
|
Color.__index = Color
|
||||||
|
|
||||||
--- Create a new color instance
|
--- Create a new color instance
|
||||||
---@param r number
|
---@param r number?
|
||||||
---@param g number
|
---@param g number?
|
||||||
---@param b number
|
---@param b number?
|
||||||
---@param a number? -- default 1
|
---@param a number? -- default 1
|
||||||
---@return Color
|
---@return Color
|
||||||
function Color.new(r, g, b, a)
|
function Color.new(r, g, b, a)
|
||||||
@@ -22,27 +22,6 @@ function Color.new(r, g, b, a)
|
|||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Convert hex string to color
|
|
||||||
---@param hexWithTag string -- e.g. "#RRGGBB" or "#RRGGBBAA"
|
|
||||||
---@return Color
|
|
||||||
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
|
|
||||||
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)
|
|
||||||
else
|
|
||||||
error("Invalid hex string")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---@return number r, number g, number b, number a
|
---@return number r, number g, number b, number a
|
||||||
function Color:toRGBA()
|
function Color:toRGBA()
|
||||||
return self.r, self.g, self.b, self.a
|
return self.r, self.g, self.b, self.a
|
||||||
@@ -138,6 +117,144 @@ local Positioning, FlexDirection, JustifyContent, AlignContent, AlignItems, Text
|
|||||||
enums.JustifySelf,
|
enums.JustifySelf,
|
||||||
enums.FlexWrap
|
enums.FlexWrap
|
||||||
|
|
||||||
|
-- ====================
|
||||||
|
-- Units System
|
||||||
|
-- ====================
|
||||||
|
|
||||||
|
--- Unit parsing and viewport calculations
|
||||||
|
local Units = {}
|
||||||
|
|
||||||
|
--- Parse a unit value (string or number) into value and unit type
|
||||||
|
---@param value string|number
|
||||||
|
---@return number, string -- Returns numeric value and unit type ("px", "%", "vw", "vh")
|
||||||
|
function Units.parse(value)
|
||||||
|
if type(value) == "number" then
|
||||||
|
return value, "px"
|
||||||
|
end
|
||||||
|
|
||||||
|
if type(value) ~= "string" then
|
||||||
|
-- Fallback to 0px for invalid types
|
||||||
|
return 0, "px"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Match number followed by optional unit
|
||||||
|
local numStr, unit = value:match("^([%-]?[%d%.]+)(.*)$")
|
||||||
|
if not numStr then
|
||||||
|
-- Fallback to 0px for invalid format
|
||||||
|
return 0, "px"
|
||||||
|
end
|
||||||
|
|
||||||
|
local num = tonumber(numStr)
|
||||||
|
if not num then
|
||||||
|
-- Fallback to 0px for invalid numeric value
|
||||||
|
return 0, "px"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Default to pixels if no unit specified
|
||||||
|
if unit == "" then
|
||||||
|
unit = "px"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Validate unit type (removed vmin/vmax as requested)
|
||||||
|
local validUnits = { px = true, ["%"] = true, vw = true, vh = true }
|
||||||
|
if not validUnits[unit] then
|
||||||
|
-- Fallback to pixels for unsupported units, keeping the numeric value
|
||||||
|
return num, "px"
|
||||||
|
end
|
||||||
|
|
||||||
|
return num, unit
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Convert relative units to pixels based on viewport and parent dimensions
|
||||||
|
---@param value number
|
||||||
|
---@param unit string
|
||||||
|
---@param viewportWidth number
|
||||||
|
---@param viewportHeight number
|
||||||
|
---@param parentSize number? -- Required for percentage units
|
||||||
|
---@return number -- Pixel value
|
||||||
|
function Units.resolve(value, unit, viewportWidth, viewportHeight, parentSize)
|
||||||
|
if unit == "px" then
|
||||||
|
return value
|
||||||
|
elseif unit == "%" then
|
||||||
|
if not parentSize then
|
||||||
|
error("Percentage units require parent dimension")
|
||||||
|
end
|
||||||
|
return (value / 100) * parentSize
|
||||||
|
elseif unit == "vw" then
|
||||||
|
return (value / 100) * viewportWidth
|
||||||
|
elseif unit == "vh" then
|
||||||
|
return (value / 100) * viewportHeight
|
||||||
|
else
|
||||||
|
error("Unknown unit type: " .. unit)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Get current viewport dimensions
|
||||||
|
---@return number, number -- width, height
|
||||||
|
function Units.getViewport()
|
||||||
|
-- Try both functions to be compatible with different love versions and test environments
|
||||||
|
if love.graphics and love.graphics.getDimensions then
|
||||||
|
return love.graphics.getDimensions()
|
||||||
|
else
|
||||||
|
return love.window.getMode()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Resolve units for spacing properties (padding, margin)
|
||||||
|
---@param spacingProps table?
|
||||||
|
---@param parentWidth number
|
||||||
|
---@param parentHeight number
|
||||||
|
---@return table -- Resolved spacing with top, right, bottom, left in pixels
|
||||||
|
function Units.resolveSpacing(spacingProps, parentWidth, parentHeight)
|
||||||
|
if not spacingProps then
|
||||||
|
return { top = 0, right = 0, bottom = 0, left = 0 }
|
||||||
|
end
|
||||||
|
|
||||||
|
local viewportWidth, viewportHeight = Units.getViewport()
|
||||||
|
local result = {}
|
||||||
|
|
||||||
|
-- Handle shorthand properties first
|
||||||
|
local vertical = spacingProps.vertical
|
||||||
|
local horizontal = spacingProps.horizontal
|
||||||
|
|
||||||
|
if vertical then
|
||||||
|
if type(vertical) == "string" then
|
||||||
|
local value, unit = Units.parse(vertical)
|
||||||
|
vertical = Units.resolve(value, unit, viewportWidth, viewportHeight, parentHeight)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if horizontal then
|
||||||
|
if type(horizontal) == "string" then
|
||||||
|
local value, unit = Units.parse(horizontal)
|
||||||
|
horizontal = Units.resolve(value, unit, viewportWidth, viewportHeight, parentWidth)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Handle individual sides
|
||||||
|
for _, side in ipairs({ "top", "right", "bottom", "left" }) do
|
||||||
|
local value = spacingProps[side]
|
||||||
|
if value then
|
||||||
|
if type(value) == "string" then
|
||||||
|
local numValue, unit = Units.parse(value)
|
||||||
|
local parentSize = (side == "top" or side == "bottom") and parentHeight or parentWidth
|
||||||
|
result[side] = Units.resolve(numValue, unit, viewportWidth, viewportHeight, parentSize)
|
||||||
|
else
|
||||||
|
result[side] = value
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-- Use fallbacks
|
||||||
|
if side == "top" or side == "bottom" then
|
||||||
|
result[side] = vertical or 0
|
||||||
|
else
|
||||||
|
result[side] = horizontal or 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
--- Top level GUI manager
|
--- Top level GUI manager
|
||||||
---@class Gui
|
---@class Gui
|
||||||
---@field topElements table<integer, Element>
|
---@field topElements table<integer, Element>
|
||||||
@@ -337,6 +454,7 @@ end
|
|||||||
-- Element Object
|
-- Element Object
|
||||||
-- ====================
|
-- ====================
|
||||||
---@class Element
|
---@class Element
|
||||||
|
---@field id string
|
||||||
---@field autosizing {width:boolean, height:boolean} -- Whether the element should automatically size to fit its children
|
---@field autosizing {width:boolean, height:boolean} -- Whether the element should automatically size to fit its children
|
||||||
---@field x number -- X coordinate of the element
|
---@field x number -- X coordinate of the element
|
||||||
---@field y number -- Y coordinate of the element
|
---@field y number -- Y coordinate of the element
|
||||||
@@ -368,28 +486,30 @@ end
|
|||||||
---@field transform TransformProps -- Transform properties for animations and styling
|
---@field transform TransformProps -- Transform properties for animations and styling
|
||||||
---@field transition TransitionProps -- Transition settings for animations
|
---@field transition TransitionProps -- Transition settings for animations
|
||||||
---@field callback function? -- Callback function for click events
|
---@field callback function? -- Callback function for click events
|
||||||
|
---@field units table -- Original unit specifications for responsive behavior
|
||||||
local Element = {}
|
local Element = {}
|
||||||
Element.__index = Element
|
Element.__index = Element
|
||||||
|
|
||||||
---@class ElementProps
|
---@class ElementProps
|
||||||
|
---@field id string?
|
||||||
---@field parent Element? -- Parent element for hierarchical structure
|
---@field parent Element? -- Parent element for hierarchical structure
|
||||||
---@field x number? -- X coordinate of the element (default: 0)
|
---@field x number|string? -- X coordinate of the element (default: 0)
|
||||||
---@field y number? -- Y coordinate of the element (default: 0)
|
---@field y number|string? -- Y coordinate of the element (default: 0)
|
||||||
---@field z number? -- Z-index for layering (default: 0)
|
---@field z number? -- Z-index for layering (default: 0)
|
||||||
---@field w number? -- Width of the element (default: calculated automatically)
|
---@field w number|string? -- Width of the element (default: calculated automatically)
|
||||||
---@field h number? -- Height of the element (default: calculated automatically)
|
---@field h number|string? -- Height of the element (default: calculated automatically)
|
||||||
---@field border Border? -- Border configuration for the element
|
---@field border Border? -- Border configuration for the element
|
||||||
---@field borderColor Color? -- Color of the border (default: black)
|
---@field borderColor Color? -- Color of the border (default: black)
|
||||||
---@field opacity number?
|
---@field opacity number?
|
||||||
---@field background Color? -- Background color (default: transparent)
|
---@field background Color? -- Background color (default: transparent)
|
||||||
---@field gap number? -- Space between children elements (default: 10)
|
---@field gap number|string? -- Space between children elements (default: 10)
|
||||||
---@field padding {top:number?, right:number?, bottom:number?, left:number?, horizontal: number?, vertical:number?}? -- Padding around children (default: {top=0, right=0, bottom=0, left=0})
|
---@field padding {top:number|string?, right:number|string?, bottom:number|string?, left:number|string?, horizontal: number|string?, vertical:number|string?}? -- Padding around children (default: {top=0, right=0, bottom=0, left=0})
|
||||||
---@field margin {top:number?, right:number?, bottom:number?, left:number?, horizontal: number?, vertical:number?}? -- Margin around children (default: {top=0, right=0, bottom=0, left=0})
|
---@field margin {top:number|string?, right:number|string?, bottom:number|string?, left:number|string?, horizontal: number|string?, vertical:number|string?}? -- Margin around children (default: {top=0, right=0, bottom=0, left=0})
|
||||||
---@field text string? -- Text content to display (default: nil)
|
---@field text string? -- Text content to display (default: nil)
|
||||||
---@field titleColor Color? -- Color of the text content (default: black)
|
---@field titleColor Color? -- Color of the text content (default: black)
|
||||||
---@field textAlign TextAlign? -- Alignment of the text content (default: START)
|
---@field textAlign TextAlign? -- Alignment of the text content (default: START)
|
||||||
---@field textColor Color? -- Color of the text content (default: black)
|
---@field textColor Color? -- Color of the text content (default: black)
|
||||||
---@field textSize number? -- Font size for text content (default: nil)
|
---@field textSize number|string? -- Font size for text content (default: nil)
|
||||||
---@field positioning Positioning? -- Layout positioning mode (default: ABSOLUTE)
|
---@field positioning Positioning? -- Layout positioning mode (default: ABSOLUTE)
|
||||||
---@field flexDirection FlexDirection? -- Direction of flex layout (default: HORIZONTAL)
|
---@field flexDirection FlexDirection? -- Direction of flex layout (default: HORIZONTAL)
|
||||||
---@field justifyContent JustifyContent? -- Alignment of items along main axis (default: FLEX_START)
|
---@field justifyContent JustifyContent? -- Alignment of items along main axis (default: FLEX_START)
|
||||||
@@ -409,6 +529,10 @@ function Element.new(props)
|
|||||||
local self = setmetatable({}, Element)
|
local self = setmetatable({}, Element)
|
||||||
self.children = {}
|
self.children = {}
|
||||||
self.callback = props.callback
|
self.callback = props.callback
|
||||||
|
self.id = props.id or ""
|
||||||
|
|
||||||
|
-- Set parent first so it's available for size calculations
|
||||||
|
self.parent = props.parent
|
||||||
|
|
||||||
------ add non-hereditary ------
|
------ add non-hereditary ------
|
||||||
--- self drawing---
|
--- self drawing---
|
||||||
@@ -430,65 +554,175 @@ function Element.new(props)
|
|||||||
self.opacity = props.opacity or 1
|
self.opacity = props.opacity or 1
|
||||||
|
|
||||||
self.text = props.text
|
self.text = props.text
|
||||||
self.textSize = props.textSize
|
self.textSize = props.textSize or 12
|
||||||
self.textAlign = props.textAlign or TextAlign.START
|
self.textAlign = props.textAlign or TextAlign.START
|
||||||
|
|
||||||
--- self positioning ---
|
--- self positioning ---
|
||||||
self.padding = props.padding
|
local viewportWidth, viewportHeight = Units.getViewport()
|
||||||
and {
|
|
||||||
top = props.padding.top or props.padding.vertical or 0,
|
|
||||||
right = props.padding.right or props.padding.horizontal or 0,
|
|
||||||
bottom = props.padding.bottom or props.padding.vertical or 0,
|
|
||||||
left = props.padding.left or props.padding.horizontal or 0,
|
|
||||||
}
|
|
||||||
or {
|
|
||||||
top = 0,
|
|
||||||
right = 0,
|
|
||||||
bottom = 0,
|
|
||||||
left = 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
self.margin = props.margin
|
|
||||||
and {
|
|
||||||
top = props.margin.top or props.margin.vertical or 0,
|
|
||||||
right = props.margin.right or props.margin.horizontal or 0,
|
|
||||||
bottom = props.margin.bottom or props.margin.vertical or 0,
|
|
||||||
left = props.margin.left or props.margin.horizontal or 0,
|
|
||||||
}
|
|
||||||
or {
|
|
||||||
top = 0,
|
|
||||||
right = 0,
|
|
||||||
bottom = 0,
|
|
||||||
left = 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
---- Sizing ----
|
---- Sizing ----
|
||||||
local gw, gh = love.window.getMode()
|
local gw, gh = love.window.getMode()
|
||||||
self.prevGameSize = { width = gw, height = gh }
|
self.prevGameSize = { width = gw, height = gh }
|
||||||
self.autosizing = { width = false, height = false }
|
self.autosizing = { width = false, height = false }
|
||||||
|
|
||||||
|
-- Store unit specifications for responsive behavior
|
||||||
|
self.units = {
|
||||||
|
width = { value = nil, unit = "px" },
|
||||||
|
height = { value = nil, unit = "px" },
|
||||||
|
x = { value = nil, unit = "px" },
|
||||||
|
y = { value = nil, unit = "px" },
|
||||||
|
textSize = { value = nil, unit = "px" },
|
||||||
|
gap = { value = nil, unit = "px" },
|
||||||
|
padding = {
|
||||||
|
top = { value = nil, unit = "px" },
|
||||||
|
right = { value = nil, unit = "px" },
|
||||||
|
bottom = { value = nil, unit = "px" },
|
||||||
|
left = { value = nil, unit = "px" },
|
||||||
|
},
|
||||||
|
margin = {
|
||||||
|
top = { value = nil, unit = "px" },
|
||||||
|
right = { value = nil, unit = "px" },
|
||||||
|
bottom = { value = nil, unit = "px" },
|
||||||
|
left = { value = nil, unit = "px" },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
if props.w then
|
if props.w then
|
||||||
|
if type(props.w) == "string" then
|
||||||
|
local value, unit = Units.parse(props.w)
|
||||||
|
self.units.width = { value = value, unit = unit }
|
||||||
|
local parentWidth = self.parent and self.parent.width or viewportWidth
|
||||||
|
self.width = Units.resolve(value, unit, viewportWidth, viewportHeight, parentWidth)
|
||||||
|
else
|
||||||
self.width = props.w
|
self.width = props.w
|
||||||
|
self.units.width = { value = props.w, unit = "px" }
|
||||||
|
end
|
||||||
else
|
else
|
||||||
self.autosizing.width = true
|
self.autosizing.width = true
|
||||||
self.width = self:calculateAutoWidth()
|
self.width = self:calculateAutoWidth()
|
||||||
|
self.units.width = { value = nil, unit = "auto" } -- Mark as auto-sized
|
||||||
end
|
end
|
||||||
|
|
||||||
if props.h then
|
if props.h then
|
||||||
|
if type(props.h) == "string" then
|
||||||
|
local value, unit = Units.parse(props.h)
|
||||||
|
self.units.height = { value = value, unit = unit }
|
||||||
|
local parentHeight = self.parent and self.parent.height or viewportHeight
|
||||||
|
self.height = Units.resolve(value, unit, viewportWidth, viewportHeight, parentHeight)
|
||||||
|
else
|
||||||
self.height = props.h
|
self.height = props.h
|
||||||
|
self.units.height = { value = props.h, unit = "px" }
|
||||||
|
end
|
||||||
else
|
else
|
||||||
self.autosizing.height = true
|
self.autosizing.height = true
|
||||||
self.height = self:calculateAutoHeight()
|
self.height = self:calculateAutoHeight()
|
||||||
|
self.units.height = { value = nil, unit = "auto" } -- Mark as auto-sized
|
||||||
end
|
end
|
||||||
|
|
||||||
--- child positioning ---
|
--- child positioning ---
|
||||||
self.gap = props.gap or 10
|
if props.gap then
|
||||||
|
if type(props.gap) == "string" then
|
||||||
|
local value, unit = Units.parse(props.gap)
|
||||||
|
self.units.gap = { value = value, unit = unit }
|
||||||
|
-- Gap percentages should be relative to the element's own size, not parent
|
||||||
|
-- For horizontal flex, gap is based on width; for vertical flex, based on height
|
||||||
|
local flexDir = props.flexDirection or FlexDirection.HORIZONTAL
|
||||||
|
local containerSize = (flexDir == FlexDirection.HORIZONTAL)
|
||||||
|
and self.width
|
||||||
|
or self.height
|
||||||
|
self.gap = Units.resolve(value, unit, viewportWidth, viewportHeight, containerSize)
|
||||||
|
else
|
||||||
|
self.gap = props.gap
|
||||||
|
self.units.gap = { value = props.gap, unit = "px" }
|
||||||
|
end
|
||||||
|
else
|
||||||
|
self.gap = 10
|
||||||
|
self.units.gap = { value = 10, unit = "px" }
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Resolve padding and margin based on element's own size (after width/height are set)
|
||||||
|
self.padding = Units.resolveSpacing(props.padding, self.width, self.height)
|
||||||
|
self.margin = Units.resolveSpacing(props.margin, self.width, self.height)
|
||||||
|
|
||||||
|
-- Store original textSize units
|
||||||
|
if props.textSize then
|
||||||
|
if type(props.textSize) == "string" then
|
||||||
|
local value, unit = Units.parse(props.textSize)
|
||||||
|
self.units.textSize = { value = value, unit = unit }
|
||||||
|
self.textSize = Units.resolve(value, unit, viewportWidth, viewportHeight, nil)
|
||||||
|
else
|
||||||
|
self.units.textSize = { value = props.textSize, unit = "px" }
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-- Initialize with default/nil value
|
||||||
|
self.units.textSize = { value = nil, unit = "px" }
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Store original spacing values for proper resize handling
|
||||||
|
-- Initialize all padding sides
|
||||||
|
for _, side in ipairs({ "top", "right", "bottom", "left" }) do
|
||||||
|
if props.padding and props.padding[side] then
|
||||||
|
if type(props.padding[side]) == "string" then
|
||||||
|
local value, unit = Units.parse(props.padding[side])
|
||||||
|
self.units.padding[side] = { value = value, unit = unit }
|
||||||
|
else
|
||||||
|
self.units.padding[side] = { value = props.padding[side], unit = "px" }
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-- Use resolved padding values from Units.resolveSpacing
|
||||||
|
self.units.padding[side] = { value = self.padding[side], unit = "px" }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Initialize all margin sides
|
||||||
|
for _, side in ipairs({ "top", "right", "bottom", "left" }) do
|
||||||
|
if props.margin and props.margin[side] then
|
||||||
|
if type(props.margin[side]) == "string" then
|
||||||
|
local value, unit = Units.parse(props.margin[side])
|
||||||
|
self.units.margin[side] = { value = value, unit = unit }
|
||||||
|
else
|
||||||
|
self.units.margin[side] = { value = props.margin[side], unit = "px" }
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-- Use resolved margin values from Units.resolveSpacing
|
||||||
|
self.units.margin[side] = { value = self.margin[side], unit = "px" }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
------ add hereditary ------
|
------ add hereditary ------
|
||||||
if props.parent == nil then
|
if props.parent == nil then
|
||||||
table.insert(Gui.topElements, self)
|
table.insert(Gui.topElements, self)
|
||||||
|
|
||||||
self.x = props.x or 0
|
-- Handle x position with units
|
||||||
self.y = props.y or 0
|
if props.x then
|
||||||
|
if type(props.x) == "string" then
|
||||||
|
local value, unit = Units.parse(props.x)
|
||||||
|
self.units.x = { value = value, unit = unit }
|
||||||
|
self.x = Units.resolve(value, unit, viewportWidth, viewportHeight, viewportWidth)
|
||||||
|
else
|
||||||
|
self.x = props.x
|
||||||
|
self.units.x = { value = props.x, unit = "px" }
|
||||||
|
end
|
||||||
|
else
|
||||||
|
self.x = 0
|
||||||
|
self.units.x = { value = 0, unit = "px" }
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Handle y position with units
|
||||||
|
if props.y then
|
||||||
|
if type(props.y) == "string" then
|
||||||
|
local value, unit = Units.parse(props.y)
|
||||||
|
self.units.y = { value = value, unit = unit }
|
||||||
|
self.y = Units.resolve(value, unit, viewportWidth, viewportHeight, viewportHeight)
|
||||||
|
else
|
||||||
|
self.y = props.y
|
||||||
|
self.units.y = { value = props.y, unit = "px" }
|
||||||
|
end
|
||||||
|
else
|
||||||
|
self.y = 0
|
||||||
|
self.units.y = { value = 0, unit = "px" }
|
||||||
|
end
|
||||||
|
|
||||||
self.z = props.z or 0
|
self.z = props.z or 0
|
||||||
|
|
||||||
self.textColor = props.textColor or Color.new(0, 0, 0, 1)
|
self.textColor = props.textColor or Color.new(0, 0, 0, 1)
|
||||||
@@ -504,8 +738,6 @@ function Element.new(props)
|
|||||||
self._explicitlyAbsolute = false
|
self._explicitlyAbsolute = false
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
self.parent = props.parent
|
|
||||||
|
|
||||||
-- Set positioning first and track if explicitly set
|
-- Set positioning first and track if explicitly set
|
||||||
self._originalPositioning = props.positioning -- Track original intent
|
self._originalPositioning = props.positioning -- Track original intent
|
||||||
if props.positioning == Positioning.ABSOLUTE then
|
if props.positioning == Positioning.ABSOLUTE then
|
||||||
@@ -528,13 +760,76 @@ function Element.new(props)
|
|||||||
|
|
||||||
-- Set initial position
|
-- Set initial position
|
||||||
if self.positioning == Positioning.ABSOLUTE then
|
if self.positioning == Positioning.ABSOLUTE then
|
||||||
self.x = props.x or 0
|
-- Handle x position with units
|
||||||
self.y = props.y or 0
|
if props.x then
|
||||||
|
if type(props.x) == "string" then
|
||||||
|
local value, unit = Units.parse(props.x)
|
||||||
|
self.units.x = { value = value, unit = unit }
|
||||||
|
local parentWidth = self.parent.width
|
||||||
|
self.x = Units.resolve(value, unit, viewportWidth, viewportHeight, parentWidth)
|
||||||
|
else
|
||||||
|
self.x = props.x
|
||||||
|
self.units.x = { value = props.x, unit = "px" }
|
||||||
|
end
|
||||||
|
else
|
||||||
|
self.x = 0
|
||||||
|
self.units.x = { value = 0, unit = "px" }
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Handle y position with units
|
||||||
|
if props.y then
|
||||||
|
if type(props.y) == "string" then
|
||||||
|
local value, unit = Units.parse(props.y)
|
||||||
|
self.units.y = { value = value, unit = unit }
|
||||||
|
local parentHeight = self.parent.height
|
||||||
|
self.y = Units.resolve(value, unit, viewportWidth, viewportHeight, parentHeight)
|
||||||
|
else
|
||||||
|
self.y = props.y
|
||||||
|
self.units.y = { value = props.y, unit = "px" }
|
||||||
|
end
|
||||||
|
else
|
||||||
|
self.y = 0
|
||||||
|
self.units.y = { value = 0, unit = "px" }
|
||||||
|
end
|
||||||
|
|
||||||
self.z = props.z or 0
|
self.z = props.z or 0
|
||||||
else
|
else
|
||||||
-- Children in flex containers start at parent position but will be repositioned by layoutChildren
|
-- Children in flex containers start at parent position but will be repositioned by layoutChildren
|
||||||
self.x = self.parent.x + (props.x or 0)
|
local baseX = self.parent.x
|
||||||
self.y = self.parent.y + (props.y or 0)
|
local baseY = self.parent.y
|
||||||
|
|
||||||
|
if props.x then
|
||||||
|
if type(props.x) == "string" then
|
||||||
|
local value, unit = Units.parse(props.x)
|
||||||
|
self.units.x = { value = value, unit = unit }
|
||||||
|
local parentWidth = self.parent.width
|
||||||
|
local offsetX = Units.resolve(value, unit, viewportWidth, viewportHeight, parentWidth)
|
||||||
|
self.x = baseX + offsetX
|
||||||
|
else
|
||||||
|
self.x = baseX + props.x
|
||||||
|
self.units.x = { value = props.x, unit = "px" }
|
||||||
|
end
|
||||||
|
else
|
||||||
|
self.x = baseX
|
||||||
|
self.units.x = { value = 0, unit = "px" }
|
||||||
|
end
|
||||||
|
|
||||||
|
if props.y then
|
||||||
|
if type(props.y) == "string" then
|
||||||
|
local value, unit = Units.parse(props.y)
|
||||||
|
self.units.y = { value = value, unit = unit }
|
||||||
|
local parentHeight = self.parent.height
|
||||||
|
local offsetY = Units.resolve(value, unit, viewportWidth, viewportHeight, parentHeight)
|
||||||
|
self.y = baseY + offsetY
|
||||||
|
else
|
||||||
|
self.y = baseY + props.y
|
||||||
|
self.units.y = { value = props.y, unit = "px" }
|
||||||
|
end
|
||||||
|
else
|
||||||
|
self.y = baseY
|
||||||
|
self.units.y = { value = 0, unit = "px" }
|
||||||
|
end
|
||||||
|
|
||||||
self.z = props.z or self.parent.z or 0
|
self.z = props.z or self.parent.z or 0
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -828,10 +1123,16 @@ function Element:layoutChildren()
|
|||||||
print(string.format("DEBUG [%s]: Final Y position: %.2f", child.debugId, child.y))
|
print(string.format("DEBUG [%s]: Final Y position: %.2f", child.debugId, child.y))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- If child has children, re-layout them after position change
|
||||||
|
if #child.children > 0 then
|
||||||
|
child:layoutChildren()
|
||||||
|
end
|
||||||
|
|
||||||
currentMainPos = currentMainPos + (child.width or 0) + itemSpacing
|
currentMainPos = currentMainPos + (child.width or 0) + itemSpacing
|
||||||
else
|
else
|
||||||
-- Vertical layout: main axis is Y, cross axis is X
|
-- Vertical layout: main axis is Y, cross axis is X
|
||||||
child.y = self.y + self.padding.top + currentMainPos
|
local newY = self.y + self.padding.top + currentMainPos
|
||||||
|
child.y = newY
|
||||||
|
|
||||||
if effectiveAlign == AlignItems.FLEX_START then
|
if effectiveAlign == AlignItems.FLEX_START then
|
||||||
child.x = self.x + self.padding.left + currentCrossPos
|
child.x = self.x + self.padding.left + currentCrossPos
|
||||||
@@ -844,6 +1145,11 @@ function Element:layoutChildren()
|
|||||||
child.x = self.x + self.padding.left + currentCrossPos
|
child.x = self.x + self.padding.left + currentCrossPos
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- If child has children, re-layout them after position change
|
||||||
|
if #child.children > 0 then
|
||||||
|
child:layoutChildren()
|
||||||
|
end
|
||||||
|
|
||||||
currentMainPos = currentMainPos + (child.height or 0) + itemSpacing
|
currentMainPos = currentMainPos + (child.height or 0) + itemSpacing
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -1057,6 +1363,79 @@ function Element:update(dt)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Recalculate units based on new viewport dimensions (for vw, vh, % units)
|
||||||
|
---@param newViewportWidth number
|
||||||
|
---@param newViewportHeight number
|
||||||
|
function Element:recalculateUnits(newViewportWidth, newViewportHeight)
|
||||||
|
-- Recalculate width if using viewport or percentage units (skip auto-sized)
|
||||||
|
if self.units.width.unit ~= "px" and self.units.width.unit ~= "auto" then
|
||||||
|
local parentWidth = self.parent and self.parent.width or newViewportWidth
|
||||||
|
self.width = Units.resolve(self.units.width.value, self.units.width.unit, newViewportWidth, newViewportHeight, parentWidth)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Recalculate height if using viewport or percentage units (skip auto-sized)
|
||||||
|
if self.units.height.unit ~= "px" and self.units.height.unit ~= "auto" then
|
||||||
|
local parentHeight = self.parent and self.parent.height or newViewportHeight
|
||||||
|
self.height = Units.resolve(self.units.height.value, self.units.height.unit, newViewportWidth, newViewportHeight, parentHeight)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Recalculate position if using viewport or percentage units
|
||||||
|
if self.units.x.unit ~= "px" then
|
||||||
|
local parentWidth = self.parent and self.parent.width or newViewportWidth
|
||||||
|
local baseX = self.parent and self.parent.x or 0
|
||||||
|
local offsetX = Units.resolve(self.units.x.value, self.units.x.unit, newViewportWidth, newViewportHeight, parentWidth)
|
||||||
|
self.x = baseX + offsetX
|
||||||
|
end
|
||||||
|
|
||||||
|
if self.units.y.unit ~= "px" then
|
||||||
|
local parentHeight = self.parent and self.parent.height or newViewportHeight
|
||||||
|
local baseY = self.parent and self.parent.y or 0
|
||||||
|
local offsetY = Units.resolve(self.units.y.value, self.units.y.unit, newViewportWidth, newViewportHeight, parentHeight)
|
||||||
|
self.y = baseY + offsetY
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Recalculate textSize if using viewport units
|
||||||
|
if self.units.textSize.unit ~= "px" then
|
||||||
|
self.textSize = Units.resolve(self.units.textSize.value, self.units.textSize.unit, newViewportWidth, newViewportHeight, nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Recalculate gap if using viewport or percentage units
|
||||||
|
if self.units.gap.unit ~= "px" then
|
||||||
|
local containerSize = (self.flexDirection == FlexDirection.HORIZONTAL)
|
||||||
|
and (self.parent and self.parent.width or newViewportWidth)
|
||||||
|
or (self.parent and self.parent.height or newViewportHeight)
|
||||||
|
self.gap = Units.resolve(self.units.gap.value, self.units.gap.unit, newViewportWidth, newViewportHeight, containerSize)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Recalculate spacing (padding/margin) if using viewport or percentage units
|
||||||
|
local containerWidth = self.parent and self.parent.width or newViewportWidth
|
||||||
|
local containerHeight = self.parent and self.parent.height or newViewportHeight
|
||||||
|
|
||||||
|
for _, side in ipairs({ "top", "right", "bottom", "left" }) do
|
||||||
|
if self.units.padding[side].unit ~= "px" then
|
||||||
|
local parentSize = (side == "top" or side == "bottom") and containerHeight or containerWidth
|
||||||
|
self.padding[side] = Units.resolve(
|
||||||
|
self.units.padding[side].value,
|
||||||
|
self.units.padding[side].unit,
|
||||||
|
newViewportWidth,
|
||||||
|
newViewportHeight,
|
||||||
|
parentSize
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
if self.units.margin[side].unit ~= "px" then
|
||||||
|
local parentSize = (side == "top" or side == "bottom") and containerHeight or containerWidth
|
||||||
|
self.margin[side] = Units.resolve(
|
||||||
|
self.units.margin[side].value,
|
||||||
|
self.units.margin[side].unit,
|
||||||
|
newViewportWidth,
|
||||||
|
newViewportHeight,
|
||||||
|
parentSize
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
--- Resize element and its children based on game window size change
|
--- Resize element and its children based on game window size change
|
||||||
---@param newGameWidth number
|
---@param newGameWidth number
|
||||||
---@param newGameHeight number
|
---@param newGameHeight number
|
||||||
@@ -1065,14 +1444,25 @@ function Element:resize(newGameWidth, newGameHeight)
|
|||||||
local prevH = self.prevGameSize.height
|
local prevH = self.prevGameSize.height
|
||||||
local ratioW = newGameWidth / prevW
|
local ratioW = newGameWidth / prevW
|
||||||
local ratioH = newGameHeight / prevH
|
local ratioH = newGameHeight / prevH
|
||||||
-- Update element size
|
|
||||||
self.width = self.width * ratioW
|
-- Handle pixel units: scale proportionally (skip auto-sized dimensions)
|
||||||
self.height = self.height * ratioH
|
-- REMOVED: Pixel units should remain fixed during resize, not scale
|
||||||
self.x = self.x * ratioW
|
-- Only viewport and percentage units should recalculate
|
||||||
self.y = self.y * ratioH
|
|
||||||
-- Update children positions and sizes
|
-- Recalculate viewport and percentage units from original values
|
||||||
|
self:recalculateUnits(newGameWidth, newGameHeight)
|
||||||
|
|
||||||
|
-- Update children
|
||||||
for _, child in ipairs(self.children) do
|
for _, child in ipairs(self.children) do
|
||||||
child:resize(ratioW, ratioH)
|
child:resize(newGameWidth, newGameHeight)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Recalculate auto-sized dimensions after children are resized
|
||||||
|
if self.autosizing.width then
|
||||||
|
self.width = self:calculateAutoWidth()
|
||||||
|
end
|
||||||
|
if self.autosizing.height then
|
||||||
|
self.height = self:calculateAutoHeight()
|
||||||
end
|
end
|
||||||
|
|
||||||
self:layoutChildren()
|
self:layoutChildren()
|
||||||
@@ -1147,7 +1537,8 @@ function Element:calculateAutoHeight()
|
|||||||
-- Skip explicitly absolute positioned children as they don't affect parent auto-sizing
|
-- Skip explicitly absolute positioned children as they don't affect parent auto-sizing
|
||||||
if not child._explicitlyAbsolute then
|
if not child._explicitlyAbsolute then
|
||||||
local paddingAdjustment = (child.padding.top or 0) + (child.padding.bottom or 0)
|
local paddingAdjustment = (child.padding.top or 0) + (child.padding.bottom or 0)
|
||||||
local childOffset = child.height + paddingAdjustment
|
local childHeight = child.height or child:calculateAutoHeight()
|
||||||
|
local childOffset = childHeight + paddingAdjustment
|
||||||
|
|
||||||
totalHeight = totalHeight + childOffset
|
totalHeight = totalHeight + childOffset
|
||||||
participatingChildren = participatingChildren + 1
|
participatingChildren = participatingChildren + 1
|
||||||
|
|||||||
@@ -1130,6 +1130,7 @@ function TestVerticalFlexDirection:testCalendarTimelineLayout()
|
|||||||
eventItem:addChild(eventTitle)
|
eventItem:addChild(eventTitle)
|
||||||
eventItem:addChild(eventDetails)
|
eventItem:addChild(eventDetails)
|
||||||
eventItem.height = 40 -- Adjust height for detailed events
|
eventItem.height = 40 -- Adjust height for detailed events
|
||||||
|
eventItem.units.height = { value = 40, unit = "px" } -- Keep units in sync
|
||||||
else
|
else
|
||||||
eventItem:addChild(eventTime)
|
eventItem:addChild(eventTime)
|
||||||
eventItem:addChild(eventTitle)
|
eventItem:addChild(eventTitle)
|
||||||
|
|||||||
360
testing/__tests__/12_units_system_tests.lua
Normal file
360
testing/__tests__/12_units_system_tests.lua
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
package.path = package.path .. ";?.lua"
|
||||||
|
|
||||||
|
local luaunit = require("testing/luaunit")
|
||||||
|
require("testing/loveStub") -- Required to mock LOVE functions
|
||||||
|
local FlexLove = require("FlexLove")
|
||||||
|
local Gui, enums = FlexLove.GUI, FlexLove.enums
|
||||||
|
|
||||||
|
local Positioning = enums.Positioning
|
||||||
|
local FlexDirection = enums.FlexDirection
|
||||||
|
|
||||||
|
-- Test the Units system functionality
|
||||||
|
TestUnitsSystem = {}
|
||||||
|
|
||||||
|
function TestUnitsSystem:setUp()
|
||||||
|
-- Clear any existing GUI elements and reset viewport
|
||||||
|
Gui.destroy()
|
||||||
|
-- Set a consistent viewport size for testing
|
||||||
|
love.graphics.getDimensions = function() return 1200, 800 end
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestUnitsSystem:tearDown()
|
||||||
|
Gui.destroy()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- Units Parsing Tests
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
function TestUnitsSystem:testUnitsParsePx()
|
||||||
|
-- Test pixel unit parsing
|
||||||
|
local container = Gui.new({
|
||||||
|
id = "container",
|
||||||
|
w = "100px",
|
||||||
|
h = "200px",
|
||||||
|
x = "50px",
|
||||||
|
y = "75px",
|
||||||
|
})
|
||||||
|
|
||||||
|
luaunit.assertEquals(container.width, 100)
|
||||||
|
luaunit.assertEquals(container.height, 200)
|
||||||
|
luaunit.assertEquals(container.x, 50)
|
||||||
|
luaunit.assertEquals(container.y, 75)
|
||||||
|
luaunit.assertEquals(container.units.width.unit, "px")
|
||||||
|
luaunit.assertEquals(container.units.height.unit, "px")
|
||||||
|
luaunit.assertEquals(container.units.x.unit, "px")
|
||||||
|
luaunit.assertEquals(container.units.y.unit, "px")
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestUnitsSystem:testUnitsParsePercentage()
|
||||||
|
-- Test percentage unit parsing
|
||||||
|
local parent = Gui.new({
|
||||||
|
id = "parent",
|
||||||
|
w = 400,
|
||||||
|
h = 300,
|
||||||
|
})
|
||||||
|
|
||||||
|
local child = Gui.new({
|
||||||
|
id = "child",
|
||||||
|
w = "50%",
|
||||||
|
h = "25%",
|
||||||
|
parent = parent,
|
||||||
|
})
|
||||||
|
|
||||||
|
luaunit.assertEquals(child.width, 200) -- 50% of 400
|
||||||
|
luaunit.assertEquals(child.height, 75) -- 25% of 300
|
||||||
|
luaunit.assertEquals(child.units.width.unit, "%")
|
||||||
|
luaunit.assertEquals(child.units.height.unit, "%")
|
||||||
|
luaunit.assertEquals(child.units.width.value, 50)
|
||||||
|
luaunit.assertEquals(child.units.height.value, 25)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestUnitsSystem:testUnitsParseViewportWidth()
|
||||||
|
-- Test viewport width units (1200px viewport)
|
||||||
|
local container = Gui.new({
|
||||||
|
id = "container",
|
||||||
|
w = "50vw",
|
||||||
|
h = "100px",
|
||||||
|
})
|
||||||
|
|
||||||
|
luaunit.assertEquals(container.width, 600) -- 50% of 1200
|
||||||
|
luaunit.assertEquals(container.units.width.unit, "vw")
|
||||||
|
luaunit.assertEquals(container.units.width.value, 50)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestUnitsSystem:testUnitsParseViewportHeight()
|
||||||
|
-- Test viewport height units (800px viewport)
|
||||||
|
local container = Gui.new({
|
||||||
|
id = "container",
|
||||||
|
w = "100px",
|
||||||
|
h = "25vh",
|
||||||
|
})
|
||||||
|
|
||||||
|
luaunit.assertEquals(container.height, 200) -- 25% of 800
|
||||||
|
luaunit.assertEquals(container.units.height.unit, "vh")
|
||||||
|
luaunit.assertEquals(container.units.height.value, 25)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestUnitsSystem:testUnitsAutoSizing()
|
||||||
|
-- Test that auto-sized elements use "auto" unit
|
||||||
|
local autoContainer = Gui.new({
|
||||||
|
id = "autoContainer",
|
||||||
|
positioning = Positioning.FLEX,
|
||||||
|
flexDirection = FlexDirection.VERTICAL,
|
||||||
|
})
|
||||||
|
|
||||||
|
luaunit.assertEquals(autoContainer.units.width.unit, "auto")
|
||||||
|
luaunit.assertEquals(autoContainer.units.height.unit, "auto")
|
||||||
|
luaunit.assertTrue(autoContainer.autosizing.width)
|
||||||
|
luaunit.assertTrue(autoContainer.autosizing.height)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestUnitsSystem:testMixedUnits()
|
||||||
|
-- Test elements with different unit types
|
||||||
|
local container = Gui.new({
|
||||||
|
id = "container",
|
||||||
|
w = "80vw", -- viewport width
|
||||||
|
h = "400px", -- pixels
|
||||||
|
x = "10%", -- percentage of viewport
|
||||||
|
y = "5vh", -- viewport height
|
||||||
|
gap = "2vw", -- viewport width for gap
|
||||||
|
textSize = "16px" -- pixel font size
|
||||||
|
})
|
||||||
|
|
||||||
|
luaunit.assertEquals(container.width, 960) -- 80% of 1200
|
||||||
|
luaunit.assertEquals(container.height, 400) -- 400px
|
||||||
|
luaunit.assertEquals(container.x, 120) -- 10% of 1200
|
||||||
|
luaunit.assertEquals(container.y, 40) -- 5% of 800
|
||||||
|
luaunit.assertEquals(container.gap, 24) -- 2% of 1200
|
||||||
|
luaunit.assertEquals(container.textSize, 16) -- 16px
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- Resize and Unit Recalculation Tests
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
function TestUnitsSystem:testResizeViewportUnits()
|
||||||
|
-- Test that viewport units recalculate on resize
|
||||||
|
local container = Gui.new({
|
||||||
|
id = "container",
|
||||||
|
w = "50vw",
|
||||||
|
h = "25vh",
|
||||||
|
})
|
||||||
|
|
||||||
|
luaunit.assertEquals(container.width, 600) -- 50% of 1200
|
||||||
|
luaunit.assertEquals(container.height, 200) -- 25% of 800
|
||||||
|
|
||||||
|
-- Simulate viewport resize
|
||||||
|
love.graphics.getDimensions = function() return 1600, 1000 end
|
||||||
|
container:resize(1600, 1000)
|
||||||
|
|
||||||
|
luaunit.assertEquals(container.width, 800) -- 50% of 1600
|
||||||
|
luaunit.assertEquals(container.height, 250) -- 25% of 1000
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestUnitsSystem:testResizePercentageUnits()
|
||||||
|
-- Test percentage units during parent resize
|
||||||
|
local parent = Gui.new({
|
||||||
|
id = "parent",
|
||||||
|
w = 400,
|
||||||
|
h = 300,
|
||||||
|
})
|
||||||
|
|
||||||
|
local child = Gui.new({
|
||||||
|
id = "child",
|
||||||
|
w = "75%",
|
||||||
|
h = "50%",
|
||||||
|
parent = parent,
|
||||||
|
})
|
||||||
|
|
||||||
|
luaunit.assertEquals(child.width, 300) -- 75% of 400
|
||||||
|
luaunit.assertEquals(child.height, 150) -- 50% of 300
|
||||||
|
|
||||||
|
-- Resize parent
|
||||||
|
parent.width = 600
|
||||||
|
parent.height = 500
|
||||||
|
child:resize(1200, 800)
|
||||||
|
|
||||||
|
luaunit.assertEquals(child.width, 450) -- 75% of 600
|
||||||
|
luaunit.assertEquals(child.height, 250) -- 50% of 500
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestUnitsSystem:testResizePixelUnitsNoChange()
|
||||||
|
-- Test that pixel units don't change during resize
|
||||||
|
local container = Gui.new({
|
||||||
|
id = "container",
|
||||||
|
w = "300px",
|
||||||
|
h = "200px",
|
||||||
|
})
|
||||||
|
|
||||||
|
luaunit.assertEquals(container.width, 300)
|
||||||
|
luaunit.assertEquals(container.height, 200)
|
||||||
|
|
||||||
|
-- Resize viewport - pixel values should stay the same
|
||||||
|
container:resize(1600, 1000)
|
||||||
|
|
||||||
|
luaunit.assertEquals(container.width, 300)
|
||||||
|
luaunit.assertEquals(container.height, 200)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- Spacing (Padding/Margin) Units Tests
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
function TestUnitsSystem:testPaddingUnits()
|
||||||
|
-- Test different unit types for padding
|
||||||
|
local container = Gui.new({
|
||||||
|
id = "container",
|
||||||
|
w = 400,
|
||||||
|
h = 300,
|
||||||
|
padding = {
|
||||||
|
top = "10px",
|
||||||
|
right = "5%",
|
||||||
|
bottom = "2vh",
|
||||||
|
left = "1vw"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
luaunit.assertEquals(container.padding.top, 10) -- 10px
|
||||||
|
luaunit.assertEquals(container.padding.right, 20) -- 5% of 400
|
||||||
|
luaunit.assertEquals(container.padding.bottom, 16) -- 2% of 800
|
||||||
|
luaunit.assertEquals(container.padding.left, 12) -- 1% of 1200
|
||||||
|
|
||||||
|
luaunit.assertEquals(container.units.padding.top.unit, "px")
|
||||||
|
luaunit.assertEquals(container.units.padding.right.unit, "%")
|
||||||
|
luaunit.assertEquals(container.units.padding.bottom.unit, "vh")
|
||||||
|
luaunit.assertEquals(container.units.padding.left.unit, "vw")
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestUnitsSystem:testMarginUnits()
|
||||||
|
-- Test different unit types for margin
|
||||||
|
local container = Gui.new({
|
||||||
|
id = "container",
|
||||||
|
w = 400,
|
||||||
|
h = 300,
|
||||||
|
margin = {
|
||||||
|
top = "8px",
|
||||||
|
right = "3%",
|
||||||
|
bottom = "1vh",
|
||||||
|
left = "2vw"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
luaunit.assertEquals(container.margin.top, 8) -- 8px
|
||||||
|
luaunit.assertEquals(container.margin.right, 12) -- 3% of 400
|
||||||
|
luaunit.assertEquals(container.margin.bottom, 8) -- 1% of 800
|
||||||
|
luaunit.assertEquals(container.margin.left, 24) -- 2% of 1200
|
||||||
|
|
||||||
|
luaunit.assertEquals(container.units.margin.top.unit, "px")
|
||||||
|
luaunit.assertEquals(container.units.margin.right.unit, "%")
|
||||||
|
luaunit.assertEquals(container.units.margin.bottom.unit, "vh")
|
||||||
|
luaunit.assertEquals(container.units.margin.left.unit, "vw")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- Gap and TextSize Units Tests
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
function TestUnitsSystem:testGapUnits()
|
||||||
|
-- Test gap with different unit types
|
||||||
|
local flexContainer = Gui.new({
|
||||||
|
id = "flexContainer",
|
||||||
|
positioning = Positioning.FLEX,
|
||||||
|
flexDirection = FlexDirection.HORIZONTAL,
|
||||||
|
w = 600,
|
||||||
|
h = 400,
|
||||||
|
gap = "2%", -- 2% of container width
|
||||||
|
})
|
||||||
|
|
||||||
|
luaunit.assertEquals(flexContainer.gap, 12) -- 2% of 600
|
||||||
|
luaunit.assertEquals(flexContainer.units.gap.unit, "%")
|
||||||
|
luaunit.assertEquals(flexContainer.units.gap.value, 2)
|
||||||
|
|
||||||
|
-- Test with viewport units
|
||||||
|
local viewportGapContainer = Gui.new({
|
||||||
|
id = "viewportGapContainer",
|
||||||
|
positioning = Positioning.FLEX,
|
||||||
|
flexDirection = FlexDirection.VERTICAL,
|
||||||
|
w = 400,
|
||||||
|
h = 300,
|
||||||
|
gap = "1vw",
|
||||||
|
})
|
||||||
|
|
||||||
|
luaunit.assertEquals(viewportGapContainer.gap, 12) -- 1% of 1200 viewport width
|
||||||
|
luaunit.assertEquals(viewportGapContainer.units.gap.unit, "vw")
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestUnitsSystem:testTextSizeUnits()
|
||||||
|
-- Test textSize with different units
|
||||||
|
local textElement = Gui.new({
|
||||||
|
id = "textElement",
|
||||||
|
w = 200,
|
||||||
|
h = 100,
|
||||||
|
textSize = "16px"
|
||||||
|
})
|
||||||
|
|
||||||
|
luaunit.assertEquals(textElement.textSize, 16)
|
||||||
|
luaunit.assertEquals(textElement.units.textSize.unit, "px")
|
||||||
|
luaunit.assertEquals(textElement.units.textSize.value, 16)
|
||||||
|
|
||||||
|
-- Test with viewport units
|
||||||
|
local viewportTextElement = Gui.new({
|
||||||
|
id = "viewportTextElement",
|
||||||
|
w = 200,
|
||||||
|
h = 100,
|
||||||
|
textSize = "2vw"
|
||||||
|
})
|
||||||
|
|
||||||
|
luaunit.assertEquals(viewportTextElement.textSize, 24) -- 2% of 1200
|
||||||
|
luaunit.assertEquals(viewportTextElement.units.textSize.unit, "vw")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- Error Handling and Edge Cases
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
function TestUnitsSystem:testInvalidUnits()
|
||||||
|
-- Test handling of invalid unit specifications (should default to pixels)
|
||||||
|
local container = Gui.new({
|
||||||
|
id = "container",
|
||||||
|
w = "100invalid", -- Should be treated as 100px
|
||||||
|
h = "50badunit" -- Should be treated as 50px
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Should fallback to pixel values
|
||||||
|
luaunit.assertEquals(container.width, 100)
|
||||||
|
luaunit.assertEquals(container.height, 50)
|
||||||
|
luaunit.assertEquals(container.units.width.unit, "px")
|
||||||
|
luaunit.assertEquals(container.units.height.unit, "px")
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestUnitsSystem:testZeroAndNegativeValues()
|
||||||
|
-- Test zero and negative values with units
|
||||||
|
local container = Gui.new({
|
||||||
|
id = "container",
|
||||||
|
w = "0px",
|
||||||
|
h = "0vh",
|
||||||
|
x = "-10px",
|
||||||
|
y = "-5%"
|
||||||
|
})
|
||||||
|
|
||||||
|
luaunit.assertEquals(container.width, 0)
|
||||||
|
luaunit.assertEquals(container.height, 0)
|
||||||
|
luaunit.assertEquals(container.x, -10)
|
||||||
|
luaunit.assertEquals(container.y, -40) -- -5% of 800 viewport height for y positioning
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestUnitsSystem:testVeryLargeValues()
|
||||||
|
-- Test very large percentage values
|
||||||
|
local container = Gui.new({
|
||||||
|
id = "container",
|
||||||
|
w = "200%", -- 200% of viewport
|
||||||
|
h = "150vh" -- 150% of viewport height
|
||||||
|
})
|
||||||
|
|
||||||
|
luaunit.assertEquals(container.width, 2400) -- 200% of 1200
|
||||||
|
luaunit.assertEquals(container.height, 1200) -- 150% of 800
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Run the tests
|
||||||
|
os.exit(luaunit.LuaUnit.run())
|
||||||
@@ -11,6 +11,11 @@ end
|
|||||||
|
|
||||||
-- Mock graphics functions
|
-- Mock graphics functions
|
||||||
love_helper.graphics = {}
|
love_helper.graphics = {}
|
||||||
|
|
||||||
|
function love_helper.graphics.getDimensions()
|
||||||
|
return 800, 600 -- Default resolution - same as window.getMode
|
||||||
|
end
|
||||||
|
|
||||||
function love_helper.graphics.newFont(size)
|
function love_helper.graphics.newFont(size)
|
||||||
-- Return a mock font object with basic methods
|
-- Return a mock font object with basic methods
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user